AssemblyScript - HTTP 203

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
SURMA: Are you ready, Jake, to get your acting face on, and-- [LAUGHS] JAKE: I'm going to pretend to be surprised by all the content, and it's totally not the third time we've tried recording this. SURMA: Technology. [MUSIC PLAYING] So AssemblyScript. We've actually talked about it before. We mentioned it and kind of introduced a little bit. JAKE: That was in our episode about loops. SURMA: Loop tiling. Yeah, I think that's where we talked about it. JAKE: Loop tiling and rotating, yes. SURMA: This is a WebAssembly topic, as AssemblyScript is a language for WebAssembly. And we've talked about WebAssembly in general as well before, but I think most of the times, I showed WebAssembly a bit like this, which is the WebAssembly human-readable text format. And I think you pointed out that many humans might not enjoy reading this at all, even though it's, you know, letters. JAKE: The common file extension for this file format is what? SURMA: And it's very appropriate. [LAUGHS] I mean, to be fair, as far as assembly languages go, it is a good one. I actually-- I think it's more readable, and-- JAKE: That's like saying, as far as the Kray twins go, Ronnie is the best. You know, that's a low bar. SURMA: It's a low bar, sure. But if you can get away with it. It's that. It can be-- I agree. This is not how you want to write code. It's not how you want to read code. This is not usually where you're going to put everything in production, like this. And I think the folks behind AssemblyScript kind of agree, because the alternatives currently out are to use C and C++ or Rust-- I mean, there's more languages now compiling to WebAssembly, but I would argue C and Rust are the most predominant ones, the most well-known ones. And I think part of that is that as a web developer, we live our lives. We live and breathe JavaScript and TypeScript, and none of these languages, which are like C and Rust, are necessarily in our daily work. I mean, maybe you know them, but if you don't and you're a web developer and you only use JavaScript and CSS and HTML, WebAssembly seems a bit inaccessible. And that's exactly, I think, what the AssemblyScript folks wanted to change, because it's a language designed for WebAssembly. So in contrast to like C and Rust, which are languages that already existed and that then added support for WebAssembly, AssemblyScript was designed for WebAssembly specifically. It is still fairly low level. That's why they called it AssemblyScript. And as you can see here on the landing page of the website, they're saying they're using the familiar TypeScript syntax. Which is really nice, because I think most of us are familiar with the TypeScript syntax, because it is pretty much the JavaScript syntax with a couple of additions here and there. JAKE: Yeah, and I would say, even if you're watching this and you're not familiar with TypeScript, yeah, the important bit there is it's additions to JavaScript. So you will look at TypeScript and understand most of it, and you'll be able to see-- there's just a couple of extra bits. Yeah. SURMA: Exactly. Although one thing we should be clear about-- and they are very clear about this on their repository-- it is definitely not a TypeScript to WebAssembly compiler. It just reuses the syntax and the language and, as far as they can, the semantics. The code that you read in a AssemblyScript looks like TypeScript. You'd think you know what it does. It is very, very likely that that's also what it actually does once you compile it. But it is not the case you can take just existing TypeScript, throw it into AssemblyScript, and get working WebAssembly out the other side. JAKE: And when you say that, it sounds like you're going to have this huge uncanny valley problem, because ooh, it looks like TypeScript, but it's not TypeScript. It's not going to work like you expect TypeScript to work. It's going to work like something else. But in practice, it's fine. It actually works really well. SURMA: Yeah, and I think that's the thing. They tried to mimic the semantics of TypeScript as far as they can, so that if it looks like TypeScript, it will behave like TypeScript, as far as WebAssembly allows. It's just TypeScript, in general, has things that just don't work in WebAssembly. Like the same variable cannot hold a string, a number, an object, a class. You don't know until runtime those things don't work in WebAssembly. So they had to add some restrictions to the language and also a different type system. And that's kind of what I wanted to talk about a little bit. But before, I think it's kind of important to talk about why would you even use WebAssembly. And I think there's four major reasons why you would be looking into WebAssembly. And the main reason we have seen so far in the WebAssembly ecosystem is making use of existing code. So we have Emscripten, the compiler that brings C and C++ to WebAssembly. Rust supports WebAssembly. And so that allows you to make use of the entire ecosystem of these two languages and bring them to the web, even though those libraries might not have been written with the web in mind. And that's really cool, because not everything is available in a web-compatible language. And I think that's something that we kind of exploited, almost, with Squoosh. Because yeah, there might be a JPEG encoder written in JavaScript. There might be a PNG encoder in JavaScript. But I don't think anyone has written down and sat down yet and wrote an AVIF encoder in JavaScript or a JPEG XL encoder. And so we were at a loss until either the browser shipped those or we sat down and wrote them. But instead, with WebAssembly, we can make use of these libraries written in C and C++ and just bring them to the web. And that's really cool. However, as I just said, with AssemblyScript, that's kind of not the case, because there's now some AssemblyScript-specific libraries out there, but it wasn't targeting an existing language. So the whole bring-legacy-code-to-the-web thing wasn't really an argument for AssemblyScript to begin with. But it is one of the major use cases that we see for people to look into WebAssembly. The next point, which many people bring up, is performance, even though I've been very, very vocal about the fact that WebAssembly and JavaScript, for the longest time, had the same peak performance. They were optimized by the same engine. They both generated machine code under the hood. And so really, they have the same peak performance they could reach. The difference is that for JavaScript to get optimized, it needs to run. It needs to be observed, because just from the code, you cannot tell what kind of type a variable has, which you need to generate machine code. While with WebAssembly, that is already encoded in the WebAssembly file. So the Engine for JavaScript needs to run the JavaScript, observe it, and then starts optimizing as it runs the code. And many JavaScript optimization experts have experienced this warm-up phase, as they often call it, when your code runs slower at the start and then gets faster, which is often also seen and benchmarked. Then they'll run a couple of loops, which they don't measure, because warm up. In WebAssembly, the optimizing compiler kicks in immediately. So we have a fast compiler, which is called Liftoff, which generates code really, really fast at the cost of not generating optimal code. And then the WebAssembly can start running as fast as possible, but the second that compiler is done, the optimizing compiler called TurboFan kicks in and starts optimizing one function at a time and switches them out bit by bit, even if you haven't even run your WebAssembly. So just with a bit of waiting, you will just get optimized WebAssembly code under the hood. JAKE: Yeah, I think it was actually Williams who said the difference between JavaScript and WebAssembly is that the performance is reliable with WebAssembly, as in you're in control of when things like garbage collection happen, that sort of thing. And yeah, a lot of the optimizations happen up front. So it's easier to measure the performance of WebAssembly code, and it's not going to change in between runs. SURMA: Exactly. I think that's really well put. WebAssembly is just much more predictable in how it will behave. Because in JavaScript, you also have this phenomenon that you can call the same function with a string and with an object and with an array. As I said, the engine observes-- if this function gets called with a string all the time, then it generates machine code for handling a string. But the first time you call it with a different type, it then has to fall back to interpreting, because the machine code is wrong now, if you call it with an array all of a sudden. And that's called de-optimization, a de-opt, which will slow you back down. And so while you can reach peak performance with JavaScript, it can also kind of fall back to becoming slower. With WebAssembly, that's not possible, because everything is strongly typed. The third point-- and there's actually something that you already kind of mentioned just now, which is there's more performance. On the one hand, WebAssembly is getting increasing access to things that JavaScript doesn't have access to, like actual shared memory concurrency with threads, SIMD, which there were proposals back in the day for JavaScript, but they expanded them all in favor of WebAssembly. And what you just said. Because the language decides what part of the runtime to ship, things like garbage control can be under your control. So in JavaScript, there is a garbage collector. And it will run, and sometimes you don't want it to run. But there's not much you can do about it, except bend your code backwards to prevent garbage collection in the first place by keeping references around. But in WebAssembly, you can actually change the garbage collector for something that works more in how you like it. And that is something I think game engines, for example, would really, really like. Because they often like to decide when there is a good time for garbage collection and when there is not a good time for garbage collection. And lastly, and this is going to be interesting, is binary size. Because the spec says that one of the design goals is that the WebAssembly binary format, the bytecode file is a very small binary representation of the code that it contains. JAKE: But then the codex we have in Squoosh are not small, but-- SURMA: Yeah. JAKE: --is that just because they're a big code, or is that down to more of the standard library sort of stuff that C and Rust need? SURMA: It's all of the above, really. On the one hand, an image encoder will have a lot of logic. And I think if you wrote it in JavaScript, it would also be quite big. But at the same time, it is what you said, that WebAssembly is a pure abstract VM. And in JavaScript, we already have-- people often say that JavaScript doesn't have a standard library, but the web platform does provide a huge amount of code that is just in the browser. And you can handle strings, you can handle arrays, you can iterate over them, you can map them, you can filter them, you can split strings, join strings, filter strings. All these things are just there. If you do use that logic in WebAssembly or in a language compiling to WebAssembly, that code needs to get compiled and put into the binary as well. And then lastly, there's also the aspect that WebAssembly has a strictly numerical interface to JavaScript. So whenever you want to pass anything that is not a number-- an object, an array, a string-- from JavaScript to WebAssembly and back, there needs to be a bit of JavaScript, which is called the "glue code" often, that converts between what the WebAssembly file understands and what JavaScript understands. That is also code that we actually ship over and over in Squoosh. So we have actually not made great experience with binary size. Although I will say, A, that WebAssembly compresses really, really well with Brotli and gzip. It is very compressible. Often 80% of the file just goes away, and you're left with 20% of the original size. But also that you can make these modules very, very small at times. But with tools like Emscripten and Rust, it is quite hard to remove. It just assumes that you need a lot of standard libraries, even if you don't, and it sometimes takes a lot of work to get rid of all these bits that you could sometimes do in a different way. But we've seen-- and I think we talked about this in our loop tiling episode, where we made the WebAssembly actually be smaller than the comparable JavaScript code. JAKE: Yes, yes, we did. SURMA: All right, I think that was enough waffling. It is time for some code-y bits, at least something that is close to code, which is the install command. It's closer. It will lead us to the code, Jake. You don't worry. You have seen the slide deck before three times. You know you don't have to worry. So with this command you install assembly. But this is actually kind of nice that's it's just on NPM. It uses WebAssembly itself under the hood, which I thought is kind of interesting, but you don't really notice when you use it. If this is too much of a commitment for any of our viewers, you can also just go to webassembly.studio, where you can try out some Emscripten with C and C++, Rust, and AssemblyScript in a little IDE in the browser. It compiles in the browser, and you can just play around without having to install anything. So both of these paths are completely valid to play around with WebAssembly a little bit. All right, time for our first AssemblyScript code. This is a TypeScript file. In this case, it's an AssemblyScript file. It exports a function called main, and it returns 42. Anyone who knows TypeScript should be fairly comfortable with reading this. We can compile it. So the AssemblyScript compiler is called asc, because it's the AssemblyScript compiler, very intuitive. And we pass it our main.ts file, and with dash b, we say the binary output file should be main.wasm. And now the only thing left to do is really load this file somehow in the browser, probably with an HTML page, and that would look like this. We just use WebAssembly.instantiateStreaming and throw the fetch in there, of the wasm file. It will give us back an instance, and every exported function will be on instance.exports. So you really get a WebAssembly module instance, and that module has exports, just like the modules we have been writing in that AssemblyScript file. And so that's a really nice, clear mapping between WebAssembly modules and code modules, if you want to talk about them that way. And this would pretty much exactly log the number 42 into your console. JAKE: Success. SURMA: If you're into it, the compiler can also generate this human-readable text format I showed at the start that can be helpful sometimes to see if certain exports are present, or suddenly you have a different name, or maybe if you want to see what the function looks like. It can sometimes be helpful, but it's definitely not required to use this at all. What is important is that the AssemblyScript compiler by default uses no optimizations to make it A, fast, but also to make source maps work really clearly. There's a very clear mapping from WebAssembly code blocks to individual commands in the AssemblyScript file, which means you can step through your AssemblyScript code in DevTools, which is really, really nice for a debugging experience. Even though, one thing I should note, something that doesn't work is like inspecting variables. You can't hover over a variable to see its current value, because source maps cannot contain that kind of mapping. But there is something in the works there, as far as I know. But if you end up shipping your AssemblyScript WebAssembly to production, make sure you use the dash O flag, which is an optimization pass which will not only make your AssemblyScript code even faster, but probably also significantly smaller. So this is quite important. But this is pretty much it. This is how you use AssemblyScript. And as you already pointed out, the return type is a bit weird. Because if you know TypeScript, you also know that this is not a TypeScript type. And I think this is-- so VSCode, if you use it, will probably put a nice, red squiggly line under it. But this is one of the differences between AssemblyScript and TypeScript, in that they changed the types from-- JavaScript has numbers and strings, while WebAssembly only has unsigned 32-bit integers, which is what this is. It has a couple more. I won't get into that, but these types are a direct mapping from what WebAssembly supports to the language that you're writing, which is actually quite nice. JAKE: So this is where it's different to TypeScript, because in TypeScript, the types are used as like validation as part of the compilation step. But in AssemblyScript here, these are runtime types. SURMA: Exactly. Well, they're both-- they're also validation at compile time, but yeah, that's exactly right, that these carry over to the runtime and actually impact what kind of code is being generated. As I said, VSCode, by default, will probably put a red squiggly line under it. To fix that, they do provide a default tsconfig.json that you can just extend, and then the red squiggly lines will go away. And VSCode will think that this is kind of valid TypeScript. So things like refactoring, jump to definition, all those things will work, and that's actually really, really neat. All right, let's make our AssemblyScript code a bit more interesting. We are now going to use an import. We're going to import a function from a different module. You can split your code into multiple modules and import and export stuff, like you're used to from JavaScript and TypeScript, and the compiler will take care of like bundling it all together and linking and recognizing which function you're actually calling. So that shouldn't be very surprising. So in this case, I also create a utils.ts file. And now we want to look at the implementation of alert, obviously, but there is none. And this is really interesting, because the "declare" keyword also exists in TypeScript, and it tells the TypeScript validator basically that, I assure you, this function exists once you run this code. I just haven't implemented it myself. JAKE: Yeah, so it's common to use this if there's an API that the browser supports but TypeScript doesn't know about it yet. You use "declare" for that, to say, look, it's here. It looks like this. SURMA: Exactly. And so you were saying, we're declaring this function exists and that this module will export it, which kind of doesn't make sense. But we're just saying, this is what reality will look like in the end. And so what this means is that now the AssemblyScript compiler will say, all right, I'm just going to note down this function takes a u32, and I will be provided a function for this later. And so if we now used our instantiateStreaming call, this would actually throw, because it says, hey, the utils module expected an alert function, but you didn't give me one. I can't run this. And so what you need to do is you actually need to provide a so-called imports object for the instantiate function. And it looks like this. We're saying, OK, it's an object of modules, and we can just provide functions. And what you see here-- I'm literally just passing through window.alert, which we all know and love from our good old debugging days. So this way, it is really cool that we can not only call from JavaScript into WebAssembly or our main function, but our main function can now call back into JavaScript through this imports object. JAKE: That's amazing. So here, we're passing a JavaScript function into AssemblyScript, which we can call. But presumably, you could compile some WebAssembly from C++ or Rust and take its exports and make them imports into AssemblyScript. So you're using AssemblyScript to call Rust, which is calling C++, which is calling JavaScript, and that just all works. SURMA: Exactly. You could do that, and that's actually kind of fascinating. I think it's something that even we in Squoosh don't do yet, and we could look into that. But yeah, you can-- just anything that is available in the host environment-- which, in the browser, is JavaScript-- you can just use that as an import, and then that will just kind of work, as long as you stick to the types that WebAssembly understands or that WebAssembly knows how to convert to JavaScript and vice versa. That's a little bit more interesting. Jake, I've asked you this quiz before, but I will ask it to you again-- what do you think is different about this example than all our other ones? JAKE: I don't think array.push will work, because this is not TypeScript, this is not JavaScript, this is AssemblyScript. So arrays work, but maybe not array methods work, things like the prototype chain. That sort of stuff is not going to be there. SURMA: Right-- no, that is not right. Because this is actually what people mean when they say a language is shipping its own runtime. AssemblyScript actually has an implementation for these growable arrays. So all these methods do exist, and they will cause code to be added to your WebAssembly binary. And this is what I mean-- it starts to ship more and more runtime the more you start using it. But this works. They actually have memory management. And I think this, for me, is the big difference with this. We are now in memory management territory, because we can push into arrays as much as we want, which mean our memory usage can grow. Which also means, at some point, we can run out of memory. And so the second you use this kind of code, your instantiateStreaming will start to fail again, because it will say, hey, I'm expecting you to provide me an abort function. Because we need something that gets called when things go wrong-- or AssemblyScript needs something that it can call when things go wrong. And so it expects for this u32 array to have an abort method in case the push doesn't succeed, because it might try to grow the memory, and then it's not available. The abort function actually takes parameters, so you can figure out which actual function from which actual file calls this abort. So the way I'm handling the error here is probably not very elegant, but I just wanted to explain why this is suddenly there. Overall, I would recommend to make use of the AssemblyScript loader, which takes care of all of this for you. Not only does it provide an abort function, but it also generates the appropriate error messages to tell you, in this file, this function call, on this line, calls the abort, and here is the actual error description, fix your code. So the AssemblyScript loader takes care of all these little details under the hood for you. It gives you a proper abort function. It gives you in this file, on this line, something went wrong, here's the error message, fix your code. So most of the time, I would recommend using the AssemblyScript loader, because it is actually not that big and just takes care of the basic fundamentals of good error messages for you. JAKE: Right. What's not that big? Well, what are we talking here? Because the glue code for Emscripten and Rust can be quite big. SURMA: Right, so the good news about this code is that the loader is not module specific. With Emscripten, you get glue code generated for each WebAssembly file. And so even though they share some code, you usually will end up double loading it. The AssemblyScript's loader, you only need once for-- it doesn't matter how many AssemblyScript modules you have. And I think even then, if you use everything it provides-- I think we're talking about 1KG zipped, so it's definitely not something that should hurt you that much to load into your bundle, which I think is really, really nice. All right, let's make it a bit more interesting. Because as I said, the interface that WebAssembly supports is pretty strictly numerical. But as developers, we know that we work with more than just numbers. Most notably, strings are usually a primitive that we often use. And so I turned our main function now into a function that takes a name as a string. And our alert function now also takes a string, which we know in JavaScript land is true, and so far that conversion just happened to work. But now we want to pass an actual string value. One thing that we will need to do is now to export the runtime, because strings are runtime values. They are not something that can just be passed along as a parameter but that needs to be a pointer to a string. And so by exporting the runtime, we can create those strings through the API that the module exposes. And this is something, again, that the AssemblyScript loader can take care of for you. So it exposes two functions, among many others-- a newString function that creates a new string in AssemblyScript land that AssemblyScript can then understand, and a getString function that turns a number, a message pointer, back into a string in JavaScript land. This is not very nice. I think I kind of like the approach that they give you, a non-automatic interface, so that you are in control when the conversion happens, and you exactly know-- no cost here is hidden from you. You can see that there's extra function calls happening to make this interface work. But I definitely feel that you have to know your interface very well, and it is a lot of extra work and makes it very noisy. We can't just pass through alert anymore, we have to do a wrapper alert function. There is something that does it better, which is called as-bind, which is a library from one of the core maintainers of AssemblyScript that tries to do all of this automatically for you. And that can be really, really nice. So if you want to use that, I would recommend looking at a README. And even this library, I actually don't know how big it is, but it is still way below the glue code that we usually see in some of the more established languages. All right, one last thing I want to talk about, which I mentioned earlier, is the runtime itself. By default, the runtime that is used is called "incremental." So I have it explicitly as a flag here, but you don't need to specify it, because that's the default. That is a full, automatic garbage collector that every now and then will just run and collect all the memory and free it up of things you don't use anymore. That will add about 3 to 4 kilobytes of gzipped WebAssembly to your WebAssembly file, depending on how many runtime functions you use. JAKE: And that's not too bad, either. We start to get concerned about that kind of file size when it's blocking your initial render or blocking fetching some data or something like that. But we tend to use WebAssembly in a way that it's lazy loaded. You get an initial render down, and then the WebAssembly comes to do something else. So in terms of that, a few k is nothing. SURMA: Exactly. I think it's an absolutely acceptable payload, but it is a garbage collector that will just run every now and then, I think, to just inject garbage collect calls at the end of functions or something? I don't know. But as I said, sometimes when you have a really high performance use case, like a game that wants to ship 60 frames a second, that might be unacceptable. And for that, they provide a second runtime, which is called "minimal." It is still a full garbage collector with memory management and everything, but it doesn't garbage collect until you explicitly call the function to tell it to garbage collect. And that can be really nice. So sometimes it might be worth your while to build up some unused memory that is still allocated. And then once you know the level is over, you're showing your high score at the end, where the frame rate isn't as important anymore, or you know there's not much input happening, that's where you run the garbage collector. And I think that's actually really interesting. There even is a "stub" runtime, which we used in the past for our image rotation example. Because we exactly knew what kind of memory we need, how much we need. We didn't want to free up any memory, and that basically reduces the 3k almost completely from the file. I think it's about 500, 600 bytes gzipped that are left. Because you can still allocate memory, but you can't free it anymore. But this is completely adequate for these modules that you instantiate, you run them, and you throw them away, which is exactly what we did with the image rotation. We just create a new instance, put in the image, waited for it to rotate the image, got it back out, and then the module gets destroyed. And if we want to rotate again, we create a new one. And in that case, you could actually use AssemblyScript, even for render-critical code. I probably wouldn't recommend that, because you still need to load it, instantiate it. But yeah, you can really squash the file size down. You could even-- this flag even accepts a file, so if you wanted to, you could write your own runtime. But we're not going to get into that, because that obviously needs a bit more detailed knowledge about WebAssembly and memory management. And that's not easy. JAKE: But the design here is brilliant, right? And it's something that a lot of JavaScript projects, JavaScript libraries, JavaScript frameworks could really learn from, this layered approach. Because it's great to know that, well, OK, so we've given you the easiest thing, but here's a flag you can swap it out for something simpler. You can even write your own. You can control the whole stack if you want. I really like this design. SURMA: I also think it's a very nice approach, and it's very extensible in general. And they have a Discord linked on their main website, which is linked here, with a super helpful community. So if you are trying to get started but you're getting stuck, there's always someone in that Discord channel willing to help. But this was basically my AssemblyScript speedrun introduction. JAKE: Yes, and I would say, if you're unfamiliar with lower level programming languages, like me-- so I don't have a lot of background in C or C++-- I found AssemblyScript really easy to get started with. Because it was in that familiar JavaScript-esque, TypeScript-esque environment, but it was just introducing the more manual memory management stuff and the lower level types. I find it really easy to get going with and write some really optimized WebAssembly code. SURMA: Well, then there's no better way to end than with that. [MUSIC PLAYING] JAKE: If the light falls over, it will change the shot quite dramatically. It'll knock all kinds of [BLEEP] over. So all right, OK. It'll be funny if it falls over, won't it? SURMA: Breaks your nose, blood everywhere. JAKE: Yeah. SURMA: Humor. JAKE: [LAUGHS]
Info
Channel: Google Chrome Developers
Views: 17,429
Rating: undefined out of 5
Keywords: GDS: Yes, AssemblyScript, how to use AssemblyScript, what is assembly script, difference between typescript and AssemblyScript, webassembly, assembly languages, webassembly language, why use webassembly, webassembly vs javascript, Wasm, low level assembly languages, new videos from Chrome, Chrome, Chrome Developers, Chrome devs, developers, Google, tech, tech videos, web, videos for developers, css, javascript, performance, new in tech, new in web, new in chrome, HTTP203, Jake and Surma
Id: u0Jgz6QVJqg
Channel Id: undefined
Length: 28min 45sec (1725 seconds)
Published: Tue Apr 06 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.