Updated: Aug 2021
I’ve created this page to informally explain some behind-the-scenes points about the development of Microvium — things that probably don’t belong in the repository or formal documentation, but which nevertheless may be interesting to some people.
I’ll treat my normal blog posts as essentially an immutable history of things I’ve been doing (Microvium or otherwise), while I’ll try to instead keep this page updated with the latest information1.
The project started in February 2020 and reached its first usable release in June 2020. Development has continued since then to refine it and add more features.
Microvium is more than just another embeddable interpreter. It introduces a novel paradigm that I haven’t seen used this way in any other comparable engine.
The “big idea” with Microvium is that you don’t deploy the source code or a compiled form of the source code, you deploy a snapshot of the running Microvium virtual machine. At build time, the engine runs all of the root level module code (transitively including root-level code from imports) and then the resulting state of the virtual machine is saved to a snapshot file that you “restore” on the target device.
This seemingly-small principle has big consequences in many different ways.
- No Configuration Files
It makes Microvium much easier to use because there are no configuration files to orchestrate compilation (e.g. project files, make files, manifest files, etc.). See Snapshotting vs Bundling for more detail.
- Build-time Execution
With Microvium, all your root-level module code runs at build time with first-class access to build-time capabilities, enabling a whole world of possibilities:
- Reading and executing other code modules (importing other JS files), as mentioned above.
- Importing configuration data from the file system or database.
- Pre-calculating expensive data such as lookup tables
- Executing all your initialization code at build time so that startup is instant at runtime.
- Producing supplimentary output files (as simple as using
fs.writeFile). In particular, this includes the ability to automatically code-generate C code to deploy alongside the VM image, such as glue code for your custom host API.
- And all of this functionality can be bundled into third-party libraries rather than being written by the user by hand or baked into the engine.
- Accurate Global Optimization
Beyond this, an experimental and closed-source project called Microvium Boost that performs whole-program static analysis on the snapshot itself and is able to accurately determine what things like what variables, properties, and parameters are used or not used in the app, and what state in the snapshot can be kept in ROM vs what needs to be stored in RAM. It does this by a completely novel technique where it maintains a symbolic representation of the virtual machine and runs the runtime code symbolically. See Microvium Boost – It’s Like Magic.
For advanced readers: the runtime machine is non-determinisic (from the perspective of a build-time analysis algorithm) because the input IO is not known at build time. However, the runtime machine can still be executed deterministically at build-time similarly to how an NFA can be converted to a DFA (see Powerset Construction on Wikipedia), where the non-deterministic state of the machine can be represented as the deterministic powerset of possible runtime states. The trick is in determining an efficient representation of the powerset since real machines can be a in a practically-infininte number of possible states very quickly after only a few CPU instructions. This component of Microvium is closed-source and proprietary for the moment but I’m happy to discuss more details with anyone if they’re interested.
- Easy Distributed Applications
Microvium snapshotting gives apps the ability to execute build-time code and then carry state to the runtime environment via the snapshot. This opens up the door for a new style of programming distributed systems which is much simpler and more robust than many approaches today. For example, a novel build-time host API could be designed which exposes methods to actually set up multiple runtime environments and resources (e.g. cloud infrastructure), and then the snapshot can be deployed to those multiple environments. A particular case might be a script that uses a build-time API to define an instance of a cloud-side microservice and database, and then deploy a snapshot of itself to both the IoT device and the microservice. See the unlisted page Distributed IoT Programs Using Microvium for an in depth view.
Some engines make a tradeoff with the standard library, such as with the function Array.filter. Generally, the choice with engine design is either to favor completeness (i.e. include
Array.filter in the engine), or compactness (i.e. leave
Array.filter out of the engine). The intended future roadmap for Microvium is to get the best of both worlds by including
Array.filter in the user-space bytecode image if it is used but omitting it if it is not used, so the standard library will only cost MCU resources on a pay-as-you-go basis (and likewise for any third-party libraries). This will be facilitated by a component called Microvium Boost which enables the accurate elimination (“tree shaking”) of unused parts of the scripted application.
Expect these numbers to change a bit over time.
- The engine currently uses about 12 kB of flash space2. More features are being added, so consider that this number may go up.
- A bytecode image has a fixed overhead of 32 B of ROM, including things like the built-in CRC, and then care has been taken to keep the bytecode small.
- Each instance of a instantiated VM has a fixed overhead of about 22 B of RAM while idle and an additional 14 B RAM for the allocation of the virtual registers when running (when the host calls into the VM), plus the size of the virtual call stack (customizable but defaults to 256 B).
- Beyond that, the usage depends on how big your script is and what it does.
- Microvium uses a 16-bit slot size, meaning that every variable has an associated 16-bit slot in memory. This 16-bit slot can directly hold a 14-bit integer, a boolean, or various other special values such as
undefined(see here for details). Contrast this to some other engines which may use a 64-bit or 128-bit slot size and so some variables are 4x or 8x more expensive.
See also memory-usage.md
The reasons are mostly personal. I think Microvium would be almost exactly as useful if it were closed-source, but by making it open-source, I don’t need to develop the project on my own in silence and isolation like I did with MetalScript. I much prefer being able to openly share ideas and collaborate.
Not only have I open-sourced it, but I’ve also used a very permissive license (MIT) for both the compiler and bytecode interpreter, making it easy for anyone to incorporate Microvium into other projects, without needing to worry much about legal issues, security issues, or what happens if Microvium stops being maintained.
(I might change this at some point to a dual-licensing model, where you can use it for free for non-commercial use but contribute financially to its development if you use it for commercial use)
I’m intentionally trying to avoid committing formally or informally to any particular development path or schedule. It just adds unnecessary stress to my life. Just on a personal level, I’m starting to realize that always having my head in the future, on what-could-be-but-isn’t-yet, is detracting from the great things that already exist right now and putting me in a kind of psychological “debt” — a deep and unsatisfying sense that the current reality is always less than I want it to be.
Having said that, no doubt there will be many cool things coming to Microvium in the future — the list of possibilities of directions I could go with this seems endless. Subscribe to my blog to get updates on new developments.