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
andTextDecoderStream
atob
andbtoa
- 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!