Crows, a Rust and WASM based load testing tool

Comment on: HN Reddit

Today I released Crows, a distributed load testing tool written in Rust running scenarios compiled to WebAssembly. If you are new to load testing - it's a type of performance testing that helps you determine how your system behaves under load. Sometimes also the term "stress testing" is used, which usually involves testing the system with a load big enough to make the system fail and observing how the failure manifests itself. In a simple case a tool like wrk may be used to send several requests to a single URL, but on many occasions, you may want to write more advanced scenarios simulating user behavior. Crows falls under the latter category. One of the interesting things about Crows is that it aims to be a distributed tool that allows to dynamically add and remove workers to the system, but also upload scenarios dynamically. If you want to know more about the tool itself take a look at the README, but in this post, I wanted to focus a bit more on another interesting feature: WebAssembly support.

While I'm not saying using WebAssembly magically makes software better, I think WebAssembly is one of the most interesting things that happened in the programming world in recent years. Crows it's still in the "proof of concept" phase, but there are many tools that use WebAssembly successfully: (SpacetimeDB)[https://spacetimedb.com/] (disclaimer, it's where I work), Lunatic runtime, Fluvio or other experimental or "in the works" software like Flawless. If you are new to WebAssembly or to WebAssembly outside of the browser, you might be wondering: what's the appeal?

I feel like writing applications using WebAssembly to run code is a bit like having at least part of the power that comes with writing your language without actually having to design and implement the language. Instead, you connect a runtime to a language or languages of your choice. You can use Rust and execute I/O operations asynchronously and yet write the code as if it was a regular synchronous program. You can control how I/O functions are performed. You can support running sandboxed programs compiled from multiple languages. Ideally, in the future, you could easily re-use libraries written in one language when coding in another language. And a lot more. I don't want to dive into all of these features, but I'd like to talk about two things that are very appealing to me personally: many language features working out of the box and sandboxing.

One of the libraries that was an inspiration for Crows is a well-known load testing tool - K6. It's a great tool and interestingly it kinda uses a similar model to Crows: the host (written in Go) provides the functions needed to interpret the scenarios, make HTTP requests, etc. while scenarios are written in another language (JS). Doing it this way makes a lot of sense. JavaScript is a fairly fast-interpreted language (which means no compilation is required) that works well in the context of handling and sending HTTP requests, but it's usually slower than Go and in most implementations, it runs only on one thread. The latter part makes it trickier to write tools that use all of your cores. It's not impossible, but usually, it involves some form of creating multiple processes like forking and then we get into the inter-process communication. Not my favorite style of programming to be honest. One of the issues of the model K6 uses, though, is the need to implement a lot of plumbing in between JS scenarios and the Go host. Let's take timers as an example. Javascript allows you to use functions like setTimeout and K6 also has an equivalent, but the implementation involves a few hundred lines of code. It's not an issue for the users of the library, of course, but it's a burden on the maintainers. For comparison when writing Crows, I didn't have to do anything special to make something like std::thread::sleep() in Rust work. Of course, it's not entirely free, someone had to implement it, ie. Rust needs to correctly compile this code to WASM format and a runtime, like Wasmtime, has to correctly interpret it. But then it's a part of the standard and work we can use as a foundation. In other words, it gives a lot of power to programmers without the need to reimplement most of the glue code over and over again.

Another thing that is very interesting for me is sandboxing. Sandboxing, ie. running a program in a sandboxed environment thus also controlling the resources the program has access to and things it can do, is not a trivial problem to solve. It differs across different languages and in some cases, it's either very hard or impossible to achieve. WebAssembly changes that. While it's not something crucial for implementing tools like Crows, it comes in very handy in many situations. For example, imagine allowing users to write server side plugins to your hosted service. Doesn't it sound powerful?

People usually suck at predicting the future, so I hope I'm not wrong here, but I think WebAssemby usage outside of the browser will grow in the next few years. It has its disadvantages and may be rough on the edges, but I think it opens a whole new world of possibilities that we didn't have before. I'm hoping I picked your interest and be sure to check out Crows!