Microvium is very small
TL;DR: The Microvium JavaScript engine for microcontrollers takes less than 16 kB of ROM and 64 bytes of RAM per VM while idle, making it possibly the smallest JavaScript engine to date with more language features than engines 4x its size.
I’ve designed Microvium from the ground up with the intention for it to be tiny, and it’s been an absolute success in that sense. Microvium may be the smallest JavaScript engine out there, but it still packs a punch in terms of features.
*Edit: this post was originally written when Microvium was around 8.2 kB (rounded up to 8.5 kB). Since then, new features have been added. As of June 2022, Microvium is now 9.04 kB (rounded up to 10 kB). In the TLDR headline, I’ve rounded this up again to “less than 16 kB” since that figure will remain true for a very long time (possibly forever). Similarly, the idle RAM usage for a VM can be as low as 22 B1, but I’ve rounded this up to “less than 64 bytes” to accommodate different situations and future expansion.
Does size matter?
Size often matters in small MCU devices. A large proportion of microcontroller models available on the market still have less than 64 kB of flash and less than 2 kB of RAM. These are still used because they’re smaller, cheaper, and have lower power than their larger counterparts. All the microcontrollers I’ve worked with in my career as a firmware engineer have had ≤ 16 kB RAM.
Some might say that you shouldn’t even want JavaScript on such small devices, and certainly in some cases that would be true. But as I pointed out in my last post, juggling multiple operations in firmware can be both easier and more memory efficient if the high-level logic is described in terms of a language like JavaScript, even if that’s the only thing you’re using it for.
Even on larger devices, do you really want to dedicate a large chunk of it to a JavaScript engine? A smaller engine is a smaller commitment to make — a lower barrier to entry.
How does it compare?
If I Google “smallest JavaScript engine for microcontrollers”, the first one on the list is Elk. Elk is indeed pretty tiny. For me, it compiles to just 11.5 kB of flash2. Microvium compiled with the same settings compiles to about 10 kB — in the same ballpark.
What about RAM?
The amount of RAM Elk uses is not pre-defined — you give it a buffer of RAM of any size you want, but it needs to be at least 96 bytes for the VM kernel state. Microvium takes 54 bytes for the kernel state.
But where there’s a massive difference in memory requirement is that Elk requires all of its memory allocated upfront, and keeps it for the lifetime of the VM. If your script’s peak memory in Elk is 1 kB then you need to give it a 1 kB buffer at startup, so its idle memory usage is 1 kB. Microvium on the other hand uses malloc and free to allocate when needed and free when not needed. Its idle memory usage can be as low as 62 bytes3. In typical firmware, idle memory is much more important than peak memory, as I explained in my last post.
What about the feature set? This is another area where Microvium and Elk diverge significantly. The following table shows the differences:
Microvium | Elk | |
---|---|---|
var , const (Elk supports let only) | ✓ | |
do , switch , for | ✓ | |
Computed member access a[b] | ✓ | |
Arrow functions, closures | ✓ | |
try … catch | ✓ | |
Modules | ✓ | |
Snapshotting | ✓ | |
Uses intermediate bytecode (better performance) | ✓ | |
Parser at runtime | ✓ | |
ROM | 10 kB | 11.5 kB |
Idle RAM | 36 B | Lots |
Peak kernel RAM | 56 B | 96 B |
Slot size (size of simple variables) | 2 B | 8 B |
The only thing that Elk can do that Microvium can’t do is execute strings of JavaScript text at runtime. So if your use case involves having human users directly provide scripts to the device, without any intermediate tools that could pre-process the script, then you can’t use Microvium and you might want to use Elk, mJS, or a larger engine like XS. On the other hand, if your use case has at any point a place where you can preprocess scripts before downloading them to the device then you can use Microvium.
Comparing with mJS
But Cesanta, the maker of Elk, also made a larger JS engine with more features: mJS, which is probably the closest match to Microvium in terms of feature set. mJS lets you write for-loops and switch statements for example.
Since they’re closely matched for intent and features, I did a more detailed comparison of mJS and Microvium here. But here’s a summary:
Microvium | mJS | Elk | |
var , const (mJS supports let only) | ✓ | ||
Template strings | ✓ | ||
Arrow functions and closures | ✓ | ||
try … catch | ✓ | ||
ES Modules (but mJS does support a non-standard load function) | ✓ | ||
do , switch , for | ✓ | ✓ | |
Computed member access a[b] | ✓ | ✓ | |
Uses intermediate bytecode (better performance) | ✓ | ✓ | |
Some builtin-functions | ✓ | ||
Parser at runtime | ✓ | ✓ | |
ROM | 10 kB | 45.6 kB | 11.5 kB |
Slot size | 2 B | 8 B | 8 B |
I’ve lumped “some builtin-functions” into one box because it’s not a language feature as such. mJS has a number of builtin functions that Microvium doesn’t have – most notably print
, ffi
, s2o
, JSON.stringify
, JSON.parse
and Object.create
. You can implement these yourself in Microvium quite easily without modifying the engine (or find implementations online), and it gives you the option of choosing what you want rather than having all that space forced on you4.
In terms of features, mJS is a more “realistic” JavaScript engine, compared to Elk’s minimalistic approach. I wouldn’t want to write any substantial real-world JavaScript without a for-loop for example. Like Microvium, mJS also precompiles the scripts to bytecode and then executes the bytecode, which results in much better performance than trying to parse on the fly. Engines like Elk that parse as they execute also have the unexpected characteristic that comments and whitespace slow them down at runtime.
But the added features in mJS means it costs a lot more in terms of ROM space — about 4x more than Elk and 5x more than Microvium.
Microvium still has more core language features than mJS, making it arguably a more pleasant language to work in. These features are actually quite useful in certain scenarios:
- Proper ES module support is important for code organization and means that your Microvium modules can also be imported into a node.js or browser environment. You can have the same algorithms shared by your edge devices (microcontrollers), backend servers, and web interfaces, to give your users a unified experience.
- Closures are fundamental to callback-style asynchronous code, as I explained in my previous post.
Conclusion
I’m obviously somewhat biased since Microvium is my own creation, but the overall picture I get is this:
- Microvium is the smallest JavaScript engine that I’m aware of5
- In this tiny size, Microvium actually supports more core language features than engines more than 5x its size. Some of these features are really useful for writing real-world JS apps.
- Having said that, Microvium has fewer built-in functions — it’s more of a pay-as-go philosophy where your upfront commitment is much less and you bring in support for what you need when you need it.
- The big trade-off is that Microvium doesn’t have a parser at runtime. In the rare case that you really need a parser at runtime, Microvium simply won’t work for you.
Something that made me smile is this note by one of the authors of mJS in a blog posts:
That makes mJS fit into less than 50k of flash space (!) and less than 1k of RAM (!!). That is hard to beat.
https://mongoose-os.com/blog/mjs-a-new-approach-to-embedded-scripting/
I have great respect for the authors of mJS and what they’ve done, which makes me all the more proud that Microvium is able to knock this out of the ballpark, beating what the seasoned professionals have called “hard to beat”. Of course, this comes with some tradeoffs (no parser and no builtin functions), but I’ve achieved my objective of making a JavaScript engine that has a super-low upfront commitment and will squeeze into the tiniest of free spaces, all while still including most of the language features I consider to be important for real-world JavaScript apps.
When compiled for a 16-bit device and excluding any built-in objects. ↩
All of the sizes quoted in this post are when targetting the 32-bit ARM Cortex M0 using GCC with optimization for size. I’m measuring these sizes in June 2022, and of course they may change over time. ↩
This idle memory figure includes the overhead of a single heap bucket with the default array prototype. The earlier quoted “kernel state” does not include this but includes the register bank which is not allocated during idle time. ↩
The
ffi
in mJS is something that would need to be a built-in in most engines but Microvium’s unique snapshotting approach makes it possible to implement theffi
as a library just like any of the other functions ↩Please let me know if you know of a smaller JS engine than Microvium. ↩
5 Replies to “Microvium is very small”
This is awesome! About 15 years ago I tried to get a scripting language built into WD HDDs, mainly to allow the same code to be used for remote and built-in tests, with a suitable abstraction for the hardware layer. The idea was to debug using a desktop connection, then move the script to the drive.
Management had a hard time taking the project seriously when I suggested porting Pawn – you can imagine what they heard when I said that with my English accent!
How easy is it to make calls from Javascript to functions written in C or asm?
Yes, this sounds very similar to my situation. At every past job where I’ve worked as a firmware engineer, we’ve eventually reached a point where it would make things a lot cleaner if we had scriptable behavior that was shared between the embedded device and say a backend server or test environment. A concrete example is that you can pay for parking at a parking meter (which has a microcontroller) or pay for the same parking space on your phone, so you want the logic, user workflow, and tariff rules to be the same either way. If Microvium had existed when I was doing that job, I would definitely have used it. I kept running into similar problems, so I decided to create Microvium.
I think it’s pretty easy to make calls from JS into C. Have a look at the C code in the getting-started guide for an example where the JS calls
print
that is implemented in C. Let me know if anything is confusing so I can revise the guide if necessary, since one of the objectives of Microvium is to be easy to use.Nice post thanks !!!
tell me more…I’m a freak of arduino/raspberry how can I use it on SBC running on arm 64?
My suggestion is to try the getting-started guide linked from the readme
https://github.com/coder-mike/microvium
Let me know how it goes!