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 │ │ │ └──────────────────────┘
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
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);
use web_sys::console; console::log_2(&"Logging arbitrary values looks like".into(), &JsValue::from_serde(&some_variable_here).unwrap());
Slow Data Transfer
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 though is repeatably called by
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.
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.