txiki.js 24.6.0 released!

Long time, no post. Time to break the dry spell!

txiki.js is a small and powerful JavaScript runtime. It targets ECMAScript 2023 and implements many web platform features.

It’s built on the shoulders of giants: it uses QuickJS-ng as its JavaScript engine, libuv as the platform layer, wasm3 as the WebAssembly engine and curl as the HTTP / WebSocket client.

This is the first release after 6 months, and a lot has happened since the last one. 103 commits to be exact. Let’s break it down.

New web APIs

Slowly but surely, txiki.js keeps getting new web platform features. The plan is to get as close as possible to the Minimum Common Web Platform API. Progress is being tracked here.

This release adds / improves the following web APIs:

  • navigator.userAgent
  • TextEncoderStream and TextDecoderStream
  • atob and btoa
  • Improved URL polyfill
  • Compression Streams
  • FormData
  • FileReader
  • Complete console API
  • Fixed bugs in performance
  • Improved XHR to send FormData, URLSearchParams, Blob, ArrayBuffer and Typed Arrays

Memory allocation improvements

This one was quite interesting. While working on QuickJS-ng, I learned that applications such as a JS interpreter may greatly benefit from a fast memory allocator, since they perform many small allocations all the time. While intuitive (once you know it, of course!) it hadn’t occurred to me to attempt to change the allocator used in txiki.js.

Enter mimalloc, a egeneral purpose, and high performance memory allocator by Microsoft. Integrating it was interesting because I discovered a number of memory corruption bugs that had gone unnoticed! libuv had an interesting bug when the default allocator was changed. The fix was simple, yet interesting. The performance improvements can be signifficant, here are some benchmarks.

Performance improvements aside, replacing the allocator was a useful experience, I learned about mimalloc and fixed a bunch of memory errors here and there.

Taking a closer look at that also made me realize the logic for engine shutdown was not great. In txiki.js we wrap many libuv handles and expose them as JS objects. While doing this, once must keep the lifetimes of both aligned, but things can get tricky when a JS object with a native libuv handle attached to it gets garbage collected. In that case we might need to call uv_close on the libuv handle, but that function is not synchronous, it’s only safe to free the handle after the close callback is called.

The extra challenge here is that when the garbage collection happens as part of the JS runtime destruction, the QuickJS JSRuntime might be freed before the libuv close callback gets called. Oops. After some thinking, I solved it by not using the allocator provided by QuickJS, which is linked to the JS engine. Instead, we can allocate with our own (mimalloc) allocator directly, so the libuv handle can be freed independently of the runtime lifetime.

Coincidentally, that simplified the engine teardown process, so it was certainly worth it!

REPL improvements

The top-level await support in QuickJS-ng has been refactored to be more compliant with the spec and fix a bunch of corner cases, and that brought better await support in the REPL, since it’s now native, and not relying on some wrapper function before eval.

Finally added history support! Not sure why I didn’t think of it before, but since SQLite support is builtin now, I chose to use that as the storage for REPL history, fast and painless.

So much more

There are many other quality of life improvements, check the full changelog if you’re curious!

Leave a Reply

Your email address will not be published. Required fields are marked *