Home //

Posts

AssemblyScript from an idiot

I'm a javascript developer at my core. I like my stuff interpreted, loosely typed, and runnable in web browsers. C/C++ things like managing memory, pointers, etc, that real computer science stuff is usually too much for my squishy OOP brain. *. Now most days, this is fine, but if you've looked at snooze you'll see that I do a lot of array math there. Lately I've been getting a point where javascript just won't perform fast enough to keep the image rendering smooth. From here I had a few options: give up, rewrite everything not in javascript, or learn some language that transpiled to webassembly (or write raw webassembly).

Wait wait wait what's webassembly!?
Okay us as mentioned earlier, I'm not good at computer science, but I do know about assembly is like raw computer commands. Shit like "go to this address, read the value, compare it to this other value". It was a hard class but super rewarding and you felt like an absolute wizard when you did stuff. So when people smarter than me also decided that the browser wasn't fast enough, they invented WebAssembly, assembly code that gets ran in the browser! WebAssembly aka WebAss aka wasm is a bunch of things, the two primary being "hard to read" and "fast".

For quite awhile the "hard to read" repulsed me more than the "fast" attracted, but then AssemblyScript saved the day! § Which is super cool! I've been struggling through a bunch of problems and wanted to get more information about AS out there.

1: Starting Point

Original overlay function:

Timings:


For context: snooze takes the data from 2 images, performs a merge-type operation, then renders it onto a central screen. This op gets ran 30~ times per second so performance is kind of important. What you're looking at this is the handler function that renderScreenContents using the overlay function. overlay written in pure js takes 15.4% of the runtime. Let's trying moving that to wasm.

2: Porting it over to AssemblyScript

My first step was to copy the function over to my configured directory with the asconfig.json, then run asc. And bam! error

AS seems rough in some places (and I have no idea how to set up a dev env) and this was one of the gotchas: no arrow functions, just real functions.

Here's another gotchas, because I want to pass data into a function later on, I need this export. I'll talk about it more later, but tl;dr it provides a way for AS to persist an array in a way that wasm can use. I'm using a Uint8ClampedArray because that's what the getImage/putImage api expects. Time to run asc again. We have two pieces of output now, a wasm file and a type file.

Notably, you can see all the AS types, the array id, the migrated overlay function, more in progress code, and a bunch of things that are added when you build with the "exportRuntime". We've got the wasm, let's look at how we run it from js.

3: Back to js-land


I actually added some comments here because uhhhh documentation is good. There's a couple major steps here:
1: Load the wasm with instantiateStreaming. Since you can't inline wasm in js (or you probably shouldn't), you'll need to load the file from another source. And loading from another source means async behavior! # Once the wasm module is loaded we grab some of the exports, overlay, __getUint8ClampedArray, and __newArray. Next we return a named function that creates a newArray using the leftArray, keyed to the id of the Uint8ClampedArray, does the same for the rightArray, and finally returns the result of wasmOverlay. Now it doesn't directly return that, it "extracts" the data from the wasm runtime with the __getUint8ClampedArray function. Without that the return of the wasm function is just a pointer (in the form of a number) to where the array lives in memory. Let's see how it runs!

4: oh god oh christ what have i done this was a mistake


We've managed to take a function that ran at 15.4% of the runtime, apply wasm, and get it to take 33.4% of the runtime. Bad times. Before we start panicking, let's see exactly what's eating up that time.

So the cool parts are that __newArray and getTypedArray, the functions that pass in and pull out data from the wasm runtime are super quick. It's just my code that is slow. At this time I got real depressed and in the interest of not depressing you, I'll skip to the solution: being reckless. unchecked is an AS decorator-y thing that basically says "hey i know normally you'd add a block to make sure that the runtime doesn't bomb if the data is not in the correct shape, but I'm an adult and you can trust me." So pretty much the "! yeah just trust me the data will be here" in typescript. I applied the unchecked to all the places I saved data to the array and:

it got better!

Adding those uncheckeds cut it down to 29.5% of the runtime, let's add more!


5: almost done


16.6% of the runtime is pretty cool for js calling wasm, but it's still worse than where we started. I'm pretty stubborn so I looked for some more optimizations and flipped the while and if statements

bringing it down to

11.3%! That's good enough for me! Time to merge!

6: final thoughts

Okay I'm not crazy about what I did at the end there. Maybe if I just did the while/if bit in js it would have been faster than where I'm at now. I'm not going to do that for two reasons: I'm afraid I just wasted a bunch of time and because I don't plan on stopping here. Now that I've kind of got an idea what's going on I think I might be able to optimize the wasm more and, more interestingly, move more of the entire project runtime into wasm. The interop still took 2.3% of the runtime, so if I'm able to reduce how often that needs to happen, the code will run even faster!


Footnotes

* However PEACE MAKER! & Fish Scale - Drive Pointers is an absolute banger

tl;dr i paint an image onto a <canvas>, get the data into an r,g,b,a array, and perform transformations on it

getImageData is the real culprit here and is really slow, but hopefully not for long

§ Okay actually it took about a year for AS to get simple enough for an idiot developer like me to grok.

Ignore the ms timings, I'm running the code from roughly 5 seconds at a time.

# Fun fact: async is a weird abbreviation because the root wards are a-syn-chronus, or "not with time". Following that translation logic, the english version would be "notwithti"

used a named function causes it easier to debug cause of it having a name and shit