Glimmer: Blazing Fast Rendering for Ember.js, Part 2
June 28, 2017
Co-authors: Chad Hietala and Sarah Clatterbuck
Additionally, since our first blog post, we're happy to announce that Glimmer is now available as a standalone project that serves as a gateway into the Ember ecosystem.
The runtime is a virtual machine (VM) architecture, which is responsible taking the wire format and producing a program that can be run to do the initial render and any subsequent renders. At a high level, the runtime looks like the following:
While a VM architecture is a novel way of modeling rendering, the initial execution of the VM sets up Reference and Revision Tag subsystems, which allow us to model a pull-based functional reactive program (FRP) used for updating the UI. What this means in practice is that there is no notion of observers or subscriptions to keep values updated in the UI, but rather that we allow the backing values in a template to freely mutate. For now, the simplest way to conceptualize this system is something like the following:
Runtime compilation and execution
In Ember, when the application routes to a specific URL, there is a template that is associated with that specific URL. Within Ember, we use the container system to look up the correct template object and invoke it. At this point the Glimmer VM is initialized and is now ready to compile the wire format into an executable program.
The first thing that happens is that the wire format data is passed through a scanner that iterates over the top level. As the scanner iterates over the wire format data, it maps operations in the wire format to their corresponding compiler functions.
There are compiler functions for every statement and expression that can be expressed in a template. Each compiler function will receive the statement or expression from the wire format along with an OpcodeBuilder (“builder” in the example above). The OpcodeBuilder is what actually creates the executable program and places values into a heap. In the example above, “builder.openPrimitiveElement” would simply do something like the following:
At the end of runtime compilation, we have a byte array and strings, numbers, objects, and References in a heap. As we saw above, an operation is a 4-byte segment of the array, where the arguments, positions 1-3, are just memory addresses into the heap.
Once the template has been compiled, we can then start executing the program.
The Glimmer VM is a stack machine that uses the MIPS calling convention to model its computation. Execution of the VM is simply a linear loop over the compiled program, mapping every fourth item into an operation to perform some work:
In the example above, the VM would look up the 26th item in its instruction set and run that instruction. The operation will also pull the first item out of the heap to perform the operation. In the example above, this ends up looking like the following:
The compiler structures the data so that the VM invokes a DOM construction helper that is responsible for creating the element. The above is pretty much identical with the typical DOM operations:
In the Glimmer VM, there are two categories of operations: append operations and updating operations. Since the Glimmer VM breaks down the operations to do the initial render into very terse and targeted operations, we can infer exactly how those operations will behave when they are updated. This allows us to build up the exact program to update the UI as we are executing an append program. The updating program has access to References that track and update the dynamic content in the template. Since the updating program only needs to know about the dynamic portions of the UI, it tends to be much smaller.
The Reference and Revision Tag systems
As mentioned earlier, Glimmer VM uses References and Revision Tags to update the dynamic content in a template. A “Reference” is a stable data structure used to track the actual content that is put into a template. The most basic Reference has the following interface:
Unlike previous solutions in Ember, there is no notion of observers and subscriptions. Instead, we use a discrete signal to pull the value from the Reference to capture the current value to update the UI. This can be illustrated as follows:
References are simple structures that let us model any type of computation or functional combinators. For instance, let’s take a break from our simple “Hello World” example and look at an example that concatenates and then uppercases:
As you can see, the Uppercase Reference receives the Concat Reference as an argument in its constructor. When we call “value” on it, this will cause a recursive call to the Concat Reference. Concat maps over the references, pulling their value out and joining them to create a string when we call “value.” This result is then passed back to Uppercase and we uppercase the result and return.
Since we can model arbitrarily expensive computation, we must have a system that knows about the freshness of the computation. Otherwise the system will become volatile, performing computation when the result of that computation is idempotent. We will see later how Revision Tags are used to track the freshness of the computation.
Validating and updating
The idea is pretty simple: “value” returns a “Revision,” and you pass that back into “validate” to see if it is still valid. In Glimmer, all References have a Revision Tag on them so we can quickly check to see if the computation of the reference is stale or not.
The Dirtyable Tag is responsible for incrementing the global revision counter, and is placed on an object that is being used to back a template. In Ember, we use “set” as a way to not only update the value dirting the reference, but also to schedule a re-validation of the UI. Below is an example of what that looks like.
Since every mustache expression in our templates is backed by an object with the Dirtyable Tag on it, we know exactly when the UI needs to update. As we mentioned in the VM execution section, the initial render builds an updating program. Prior to updating the UI, we schedule a re-render, which effectively executes the opcodes in the updating program. During this process each opcode validates the tags that it knows about. If the tag is invalid, we know we need to pull compute the new value by pulling it from the Reference and update the UI with it. At the end of this process we are left with the updated UI.
What’s next for Glimmer VM?
As of today, there are quite a few things that we are thinking about with the Glimmer VM, with our primary focus being on how we can reduce the size of not only the templates of an application, but also of the VM itself. Since we are compiling the wire format into an array of bytes and a constants heap, one could imagine that our wire format could change to be more reflective of what actually gets executed by the VM.
Since we have a VM with an evaluation loop, it’s not hard to imagine where we can start, pause, and resume execution of the VM to allow for better alignment of the browser’s rendering pipeline. It also means that there is the potential for doing streaming server-side rendering response for the initial render of the UI.
Finally, we’d like to call out the great work that’s been done by the Ember community to help launch Glimmer.js as a standalone component rendering library. We’re excited that more developers will have the opportunity to experience the speed and productivity of Ember's components first-hand through Glimmer.
In the near future, we plan to use Glimmer.js components as part of a lightweight version of our Pemberly framework for mobile-only and other lightweight development use cases. Separately, Ember users will be able to use standalone Glimmer components in existing Ember apps, just by installing an addon. You can read about the plans for Glimmer.js on the official Ember blog.