For a while Ive been interested in not only Rust but WASM but having limited familiarity with many of the languages that can be compiled to WASM I never really had a chance to try it out until recently. Over the last few months though I got the opportunity to learn Rust.
Rust in my opinion is very much like typescript on steroids. While Typescript may enforce types in your code if you happen to pass a string to a type expecting a number things will still compile and may even work as expected still. With Rust this is not the case. If you provide an unexpected type either things will not compile in the first place or your software will crash.
A personal project of mine (Cryptotithe) which is an open source tax software for cryptocurrencies was something I always though would benefit from WASM since it has some computation heavy parts. While I would not say it is extremely resource or computation heavy calculating gains does need a little bit of basic math. There is also a need to do some searches with in arrays depending on the users selection of alternative types of accounting such as LIFO, HCFO(Highest Cost First Out), etc.. which can increase the amount of calculations being done.
So, a few weeks back I decided to try converting the heaviest parts into rust and then using wasm-bindgen convert it wasm for use in my typescript project. While creating a basic Rust project was easy moving building the WASM package and linking things proved to be the first challenge.
My project has a few different functions but overall has a straightforward path of functions that more or less all rely on one another which is broken down below. The end goal being to convert all of them to Rust.
┌─────────────────┐
│ │
┌────┤ calculate_gains │
│ │ │
┌──────────────────────────┐ │ └────────┬────────┘
│ │ │ │
│ add_to_currency_holdings │◄──┤ │
│ │ │ │
└──────────────────────────┘ │ ┌───────▼───────┐
│ │ │
└─────┤ process_trade │
│ │
└───────┬───────┘
│
│
│
┌───────────────────────────────┐ ┌─────────▼─────────┐
│ │ │ │
│ check_currency_holding_amount │◄─────┤ holding_selection │
│ │ │ │
└───────────────────────────────┘ └─────────┬─────────┘
│
│
│
┌──────────▼───────────┐
│ │
│ get_currency_holding │
│ │
└──────────────────────┘
Gotchas
While wasm-bindgen has support for automatically generating typescript types, in general there are some common gotchas.
One of the biggest gotchas is that u32
are converted to regular typescript numbers but u32
are actually smaller.
// this is not valid
let num: u32 = 1621867244484;
This might not seem like a big deal but if your dealing with numbers on the higher end of this specturm it quickly becomes an issue. This means a u64
has to be used, but sadly this means the typescript interface wasm-bindgen generates will have this as a BigInt
instead of a number
. This simply pushes the complexity to the javascript side though.
After trying a few different ways I could not find a great solution that didn't involve a lot of extra boilerplate code. In the end I personally found it easier to simply give up on having correct typescript types and instead accepted that Any
were going to be there.
While not specifically a wasm-bindgen issue debugging wasm can be quite a challenge. Perhaps this is due to the way I was converting types or maybe there are tools Im not aware of that make this easier. The majority of time there was an issue I basically got a standard unreachable code error which would link to some wasm that was not at all helpful.
Solving issues like this basically became a guessing game to see where exactly it stopped working and then try to backtrack to understand the why.
One helpful way of debugging is by logging right in your wasm code which wasm-bindgen natively supports
use web_sys::console;
console::log_2(&"Logging arbitrary values looks like".into(), &some_variable_here);
The best part about using console log in rust is you can also log javascript objects passed directly into rust relatively easily by simply first converting them to a JSValue as seen below:
use web_sys::console;
console::log_2(&"Logging arbitrary values looks like".into(), &JsValue::from_serde(&some_variable_here).unwrap());
Slow Data Transfer
While not a gotcha, one thing to be aware of is that transferring complex types between Javascript and WASM can be slow. This means its often not worth it to simply pass an object to WASM for one or two small computations. If you can simply pass a number instead it may be significantly faster, but in scenarios where thats not an option, WASM may actually be slower. This means when planning to convert some area of your code to WASM you should first investigate what data would be passed around and how much you may need to rewrite in order to reap the benefits.
I originally started working by simply converting the bottom most function in my project, get_currency_holding
and exposing it as a proof of concept. As a proof of concept this was great, but it was significantly slower.
The slowness made sense since holding_selection
, the function that calls get_currency_holding
does so repeatably possibly multiple times per trade. This made it clear to me that I needed to rewrite this function as well which began a snowball effect. First holding_selection
but that requires calling check_currency_holding_amount
; But still to slow since holding_selection
is simply called repeatably per trade by process_trade
. process_trade
requires add_to_currency_holdings
. process_trade
though is repeatably called by calculate_gains
.
Its only at this final function calculate_gains
where the speed benefits became clear and the whole conversion ended up being worth it since this function is called one and only has a one time transfer cost typically.
Results
Overall I would consider the work a success as it took the time to execute on a personal data file of mine from ~130ms to less then 10ms. A 10x improvement. I have yet to push this new WASM powered version live quite yet as I need to do some clean things up a bit but you can take a look at the rust version here, CryptoTithe-RS
Top comments (2)
What data types have you experienced slow performance from? what about using the REST approach, offloading some computations to the server and processing the end results at the front end?
Regarding slow performance, complex types like objects or strings can be slow. Unless you have a use case that will greatly benefit from WASM the cost to transfer it may not be worth it.
Offloading to a server may work depending on the use case, but you also then have to take into account round trip time. For my specific project there is no server side, its 100% client side code so using WASM made the most sense.