blog post banner image

Powered by WebAssembly: Crafting a Shared Bucketing Library for Cross-Platform Consistency

Jonathan Norris

Jonathan Norris

8/28/2023

Building a WebAssembly Shared Bucketing and Segmentation Library

When architecting the original vision for DevCycle, we decided on a couple of core design goals from our SDKs:

1. SDKs should be easy to understand and have consistent functionality across platforms.

2. Business-critical code should be shared across platforms to ensure consistency and reduce bugs.

3. A cross-platform end-to-end test-harness is required to ensure SDKs are working as expected.

4. SDKs should limit the number of start options that change the core behaviour of the SDK.

Achieving Cross-Platform Consistency

One of the most challenging goals was how to share business-critical code across platforms to ensure consistency. Sharing code across platforms has always been a holy grail in software development that, in theory, provides teams with benefits in reusability, consistency, and collaboration. However, anyone who has tried to write cross-platform code knows that it usually requires compromises in the codebase's complexity, performance, and debugability.

The code we needed to share was our core bucketing and segmentation library. It combines configuration data containing Feature / Variable / Variation / Audience / Targeting Rule definitions with user data to bucket users into features and variations and determines feature flag variable values. This library is at the core of most of our APIs and SDKs that serve feature flag values.

After some quick research into other options; building a shared C-lib from Rust / C++ (too much low-level complexity) or running Javascript in some runtime (poor performance and support), we landed on researching a WebAssembly-based approach. WebAssembly (WASM) theoretically fills the need for the holy grail of cross-platform code execution:

  • Fast: WASM is compiled to a load-time-efficient binary format, loaded very quickly, and executed at near-native speeds.
  • Portable: WASM is a portable binary format designed to run on any platform with a supporting runtime
  • Secure: WASM runs in a memory-safe, sandboxed execution environment.

The original code base we were attempting to make cross platform was a Typescript library used in our Cloudflare Workers APIs and NodeJS SDK for our core bucketing and segmentation logic. The obvious first choice was to try and port the Typescript code to AssemblyScript. AssemblyScript is a Typescript-like language that uses the Typescript syntax with lower-level types and compiles to WebAssembly. This initially felt like a massive win; we ported our Typescript library into a running AssemblyScript WASM binary in a few days. We could quickly run against the existing jest unit tests, and easily run head-to-head tests against the Typescript library, and the performance looked solid. We even set up a DevCycle feature flag to compare the output of the Typescript library against the new WASM library in production.

Limitations & Solutions

However, over time, the limitations of AssemblyScript really required us to think about writing code in a much lower-level way, which ended up being very different than the Typescript code we were coming from. Some of the core missing features and challenges we faced were:

  • Closures: Captures of local variables are not yet supported.
  • Promises: no promise support or async / await
  • Iterators: only basic for (let i = 0; i < keys.length; i++) { } support, no for..of, and limited closure support makes .map() / .forEach() / .reduce() functions difficult to use at times.
  • Exceptions: It doesn't have built-in support for try/catch blocks, exceptions, or generics, which makes it harder to handle errors gracefully.
  • JSON: no native JSON support. Packages like assemblyscript-json and as-json work but have limited communities and support. We had to fork assemblyscript-json to fix memory leaks and string compatibility issues.
  • RegEx: no native regex support, assemblyscript-regex package works with some limitations.
  • String Compatibility: This could be its own blog post, but we ran into various issues converting between UTF-16 / WTF-16 / UTF-8.
  • Garbage Collection: all garbage collection (GC) is handled internally in AssemblyScript and is simple and configurable. However, we saw major performance issues from gc and had to tune the GC to our specific workload.

Reflecting on Our WASM Journey

Overall, our experience with our AssemblyScript bucketing and segmentation library has been rewarding and frustrating at the same time. We quickly got something working that integrated well with our existing codebase. However, we discovered and helped fix core bugs in the AssemblyScript compiler and almost every open-source library we used. All while fighting through string compatibility, JSON parsing, and garbage collection issues and learning to read the WebAssembly ".wat" compiled output to optimize the library's performance.

We are proud of what we have achieved with our WASM bucketing and segmentation library, which we are now using across our Cloudflare Workers and NodeJS / Python / Ruby / .NET / Java local-bucketing Server SDKs. All of these SDKs are consistent, stable, and in some cases faster than native execution would have been. However, for one exception, like our local bucketing Go Server SDK, we needed to build a native implementation of our bucketing and segmentation code to meet the high-concurrency, nano-second response time targets required by highly tuned Go applications.

What's Next?

Long term, we will likely take the plunge to build a Rust version of this library where better support for existing and new WebAssembly features like WASI, Garbage Collection, Threading, and Component Model that would have solved most issues we faced and have us excited about the future of WASM, and is a future we want to continue to invest in.

Jonathan Norris

Written By

Jonathan Norris