The Glimmer VM: Boots Fast and Stays Fast
yehudakatz.com
yehudakatz.com
I don't follow the last part of the article though. Specifically this part:
> We accomplish this by (under the hood) allowing every bit of data that goes into a template to separate the process of computing its current value from the process of determining whether the value might have changed.
When you say "bit of data", what exactly do you mean? I assume you mean some field that is bound to a template. Like a property on glimmer component. Using KVO this would be a compute of some kind, that triggers an event when its value changes.
Perhaps you're doing something totally different, but I don't see how? Breaking it down, we have an object like:
{
"foo": "bar"
}
I think you're saying that you don't observe this object (using some kind of obj.set('foo', 'baz') convention) but rather determine that the value changed by some other means. If that is so, what is the other means?What this means is that the view layer only works with evented KVO objects. What Glimmer has achieved is the ability to work with other types of data management systems. Knowing that is key to understanding (for me, at least).
So given that, what must be going on is that the view layer gets the value of a property with some interface. That interface implementation is provided by the view model so it differs. For plain objects it might just be `return obj.foo`. This will make getting a value really fast. No notification systems are required.
For updating the view layer there must be some generic way of telling it "hey, the obj.foo property might have changed (or maybe not, shrug), figure it out for yourself". Then using the update VM it is able to figure if it really did change, and only update the subtrees if it did.
Or something, this my interpretation. Sounds smart!
At a high level, the Glimmer VM is built on top of two primitives:
1. References (as you describe, an interface implementation to provide the underlying value)
2. Tags, an interface that communicates "is it possible this value has changed?"
Every reference has an associated tag, so once you have a reference, at any time you can ask: Is it possible this thing has changed? If so, what is it's value?
In KVO systems, every time a dynamic value is used in the template, that view usually adds an observer on to the root object. This adds a fair bit of overhead to both rendering and teardown. And if multiple properties change, you have to figure out the optimal re-rendering strategy. (E.g., if a value inside an `if` changes, you probably don't want to re-render it if the conditional also changed from truthy to falsy!)
Today in Ember, the view layer no longer sets up observers. Instead, during rendering, we create a tag for each property. When you mutate a property (this.set('firstName', 'Matthew') for example), two things happen:
1. That tag is marked as dirty.
2. A revalidation of the entire tree is scheduled.
The revalidation process starts from the top of the render tree and walks down, asking every reference/tag "is it possible you've changed?" Because this is just an integer comparison, it's very very fast on modern JavaScript VMs, even if you have lots of data on screen.
The tag is like a Bloom filter, though. It means a change MAY have happened, not that one necessarily did. If the tag is dirty, we do a last chance identity check for primitive values. Only if it has actually changed do we update the DOM.
One nice thing that falls out of this is that the application developer can change as much component state as they'd like at once, and we can avoid doing any expensive computation to figure out the optimal place to start re-rendering (the `if` case I mentioned above). By revalidating the tree from the top down and keeping constant time factors low, you get that optimization "for free."
The other nice thing is that you can express all sorts of cool semantics on top. For example, if you have immutable data you can attach a tag that always says "I'm never modified." If you don't want to do any bookkeeping at all, you can attach a tag that says "Always recheck me to see if I've changed." Best of all, as Yehuda mentioned, you can mix and match these semantics in your components. It also allows the data to drive the change semantics, not the component, which you often don't want to have care about how model data might change.
If this is interesting to you, there is some WIP documentation in the Glimmer VM repository that talks more about the philosophy behind references and tags:
https://github.com/glimmerjs/glimmer-vm/blob/master/guides/0...
https://github.com/glimmerjs/glimmer-vm/blob/master/guides/0...
> Because this is just an integer comparison, it's very very fast on modern JavaScript VMs, even if you have lots of data on screen.
I don't follow, what is an integer comparison? Don't you have to compare the reference's value to the previous value? Meaning you need to get that reference's value. That reference could be somewhat expensive. Imagine if it is a property getter that calls into a few more functions.
This seems like the disadvantage of not using KVO, you don't know a property's value without asking for it. with KVO the value is cached and only changes when the underlying dependency tree changes. Of course vdom works without this, so it can't be that bad.
But I'm guessing you've found a clever way to avoid having to call out to get the reference's value to compare, so what is it? :-)
And I'll definitely read me in those docs, thanks!
EDIT: Probably should have read the docs first. A global counter id! Very clever... I wonder if you could get away with just a lastModified though. The view layer could keep track of the last time it updated and then just compare lastModified, if something has changed since the last time it updated then it must have changed too.
Going to think some more about this, thanks for the puzzle!
https://github.com/glimmerjs/glimmer-vm
It is JavaScript code or native code?
https://www.youtube.com/playlist?list=PLpAr6J-75N24C-XQgIUDM...
Glimmer is a flexible, low-level rendering pipeline for building a "live" DOM from Handlebars templates that can subsequently be updated cheaply when data changes.
This sounds like something written on top of a JavaScript VM, not a VM itself. Is it implemented with VM-like techniques? What does the instruction set look like?
You can write VMs in languages that are implemented with a VM.
> Is it implemented with VM-like techniques?
More than that, it is literally a virtual machine.
> What does the instruction set look like?
IIRC, it's a stack-based VM. Here's the opcodes, I believe: https://github.com/glimmerjs/glimmer-vm/blob/master/packages...
(I have mostly a high-level understanding on this, after talking to lots of people and watching presentations; I don't hack on Glimmer myself.)
Looking this over, I can see why my initial reaction of "why aren't you using WebAssembly?" was facile; this really is a VM optimized for templating languages.
I'm reminded of the Bad Old Days when I was a ClearCase admin; we ended up building what my boss at the time called a Revision Control VM on top of it, so that our developers never really learned ClearCase per se; instead, they learned our system that happened to use CC under the hood. Since then, I've treated the term "VM" with a looser interpretation than usual; clearly, I should have applied that here (though it is, strictly speaking, a VM in the classical sense as well).
I've implemented template languages in multiple languages. I've also thought of compiling different languages to the same VM with string instructions (IIRC the old Cheetah template language compiles to Python bytecode).
I sort of see where this is going but the docs weren't particularly clear to me. If anything it sounds like there are a lot of other components besides the VM, which weren't really described in the blog post, and I couldn't find any links.
As the article mentions, this technique is distinct from virtual DOM. The article is light on what specifically it does though. From the article:
> I'll describe the details of this approach in another post, but the short version is that we compile templates into "append-time" opcodes for a bytecode VM. The process of running the "append-time program" produces the "updating program", which is then run every time the inputs change.
anyway.
> If anything it sounds like there are a lot of other components besides the VM, which weren't really described in the blog post, and I couldn't find any links.
There's a little bit; that is, this post is specifically about Glimmer's VM; Glimmer itself has a few more things, just like React is more than just a virtual DOM implementation. In Glimmer's case, there's glimmer-component, which lets you write web components, and glimmer-application, which lets your register your components into a cohesive whole, etc.
TL;DR: Glimmer as a project is similar to react. It has some significant and novel implementation details that keep it speedy.
It is and it isn't; that is, all of this is extracted from Ember, so it has had a lot more maturity and testing than you might think at first. Not _super_ so as it's still relatively new, but deployed in lots of big places.
And second, generating imperitive code makes it harder to perform runtime optimizations. For example during initial render Glimmer could note if a property lookup yields an Immutable object and use that knowledge to disregard change tracking for properties off that object. There are some fun things we can do here.
It turns out JavaScript engines are very good at iterating flat arrays and running small, highly optimized functions and that's what Glimmer is doing at its core. When your compiler output generates functions, you're creating lots of functions specialized for each template which means a larger surface for the v8 JIT to worry about optimizing. By instead shipping lots of small, hot functions to be re-used (instructions fired via opcodes), we can get a better optimization result from v8 and other engines.
1) Size - turning each op into a couple of bytes means that the size of your template is significantly smaller. If each instruction is 4 bytes, I could get ~20 instructions in the space of just one function with a setAttribute call:
function t(e) {e.setAttribute("id", "bar")}
Size is much more important on the web than it is in other places especially as the next billion people start using the mobile web.2) Parsing speed - given the size of the JS that templates produce, you start running into JS parsing performance. Just by volume you're going to eat an insane amount of time not just downloading the JS but then trying to turn it into something executable. A correctly implemented bytecode VM could easily beat the cost of parsing.
3) Scheduling - If you just produce raw JS code, you don't have much room to dictate how it executes. Since glimmer's goal is to never miss a frame, they're going to have to take control of the work that gets executed to make sure that they always pause at a frame boundary. That's a much more straightforward thing to do in a VM, where pausing work is just a matter of yielding the interpreter loop. This gives you complete control over how you schedule the work from the ground up. Have some huge dom tree to render? Split it across 10 frames without doing a bunch of control inversion.
In terms of cost, I haven't looked at their implementation, but I assume these guys did their homework. You can implement interpreters that execute instructions in a few nanoseconds without too much effort. If you really put in the effort, you can do it subnano, but that's outside of the scope of handwritten JS. Even if the overhead was 20x a normal call, the cost of the operations this interpreter is running makes that a rounding error. The DOM is slow and the other benefits almost assuredly outweigh whatever tiny cost they're paying at the per instruction level.
There are lots of other potential benefits as well: opportunities for specialized optimizations over the bytecode (you could basically do your own domain specific jit), ease of implementing the base VM for different targets, and so on. There are relatively few times when writing your own interpreter probably makes sense, but it seems like this architecture would give them a ton of headroom to do some great stuff down the line.
There's really not much noise in that expression, just the "(,,)", so maybe 4 chars that an opcode could save.
As for 3), is that really possible? If you profile most modern frameworks, they're already fast enough that most of the time is spent in rendering, not in javascript DOM manipulation. So even if you cut short your js before 16ms (60fps), you have no idea how long the browser is going to take to render your changes. Plus, the browser will be doing extra work, since it needs to render all the frames in which you've only done part of your updates.
> they're already fast enough that most of the time is spent in rendering, not in javascript DOM manipulation
That hasn't been my experience, but it's been awhile since I've benchmarked any of the frameworks in common use. Change tracking, diffing, and then the dom calls have all been the bulk of the work in large updates. Assuming you're doing those in a dom fragment I'm not sure how "rendering time" (I'm taking that to mean compositing and painting?) could be the bottleneck in that scenario.
Paint and composite are usually fast, but calculate styles, layout and hit test may not be. It totally depends on the complexity of the DOM and CSS, of course, but as an extreme example, the js-framework-benchmark tasks are often 90+% time in render. That's why the results converge on 1.00: 0.95 of that is time spent in the browser rendering the DOM, and the time spent in javascript between a framework at 1.00 and one at 1.05 may be 2x difference (0.05 vs 0.10).
Compared to the string encoding above we went from 14 chars (1-4 bytes each, we'll just say 2) at 28 bytes to 4 bytes for our 32 bit int.
The `a(e,"id","bar")` format is what other frameworks produce. It sounds like in Glimmer it would be `[1,"id","bar"]`. So that's only a single char savings.
The client then compiles that representation into flat opcodes, in part by specializing the template based on runtime information (like the exact identity of the components in question).
The runtime opcodes are binary (128-bits apiece at the moment) and optimized for reasonably fast iteration. The wire format is, as chadhietala1 said, not as flat or compact as it could be, but still much more compact than our earlier representations (or the representations of competing rendering engines).
We plan to improve the wire format representation in the near future.
Considering that Glimmer goes quite far in optimizing stuff, at which point would it perhaps make more sense to just emulate HTML elements on a pixel-level?
Or am I vastly underestimating how complex the standard UI elements really are? Or overestimating how much effort went into Glimmer? And if so, as perhaps a more constructive question: are there any areas of 'web' where someone feels a shortcut should be taken?
(On the latter, I'm inclined to feel that WYSIWYG textareas and CSS above class-scope are ripe for 'shortcuts', but perhaps that's opening some cans of whole other worms...)
Even if this could be done with reasonable performance (and people have tried this), you shouldn't do this in any browser environment. You can't possibly emulate the behavior of every browser in a satisfactory way; you'll end up behaving gratuitously differently, with subtle breakage of platform conventions, browser conventions, user expectations (including potential extensions), hardware requirements, scaling, rendering, and accessibility.
Both, especially the first one.
Think of VNC or remote desktop. This is basically an interactive moving image representation of server-side state and logic, which takes inputs from the user in the form of clicks, mouse movements, keyboard input etc. and pipes it to a server which is doing all the rendering and feeding images back to the client.
Now, if you are simply rendering images in the browser that are as good as HTML elements, the only difference between that and VNC really is the presence of client side state, client side event management and response, and somewhat increased interactivity of the client side with the local machine. Otherwise, you are simply collecting user inputs and producing outputs as a function of client/server state interactions and behaviors.
Personally, I see an interesting trend. For example, React seems to be about custom components that have client side local state and event handling, whereas things like Redux seem to be about gracefully handling the interactions between client/server state and behaviors. This same pattern appears in some way in Angular, Vue, etc. - two-way databinding, injection of services representing server-side objects, etc.
Also, consider things like WebAssembly, which will essentially provide an optimized VM in the browser closely linked to Javascript. What do we get when we cross improved client/server side state and behavior interactions plus optimized run-time environment in the client?
I think it's basically like the UI of the application runs in the client (whereas VNC was just an image of the application running), the client handles some of the processing logic and the server handles the rest, often manipulating the view of the user and coordinating that with the user's input.
All this, and it only takes a server, server application, server protocol, client application, client VM, client app/scripting language and client styling.
Looking into the future, this architecture seems to be presented as something that is the future - for example, look at Electron apps or WPF - they actually have a lot in common in some ways. Electron - node, Chrome, HTML, CSS, JS. WPF - UI host/application main loop, XAML, styling, C#...
I also think isomorphic Javascript concepts (write in one language, this code runs in the client, this code runs on the server) are going to make a massive leap forward when WebAssembly hits (shortly!!!) [1].
Exciting times in my opinion.
[1] https://lists.w3.org/Archives/Public/public-webassembly/2017...
- (Part 1) https://www.codeproject.com/Articles/43176/How-to-create-you... - (Part 2) https://www.codeproject.com/Articles/61924/How-to-create-you...
https://medium.com/@bluepnume/introducing-support-for-cross-...
I'd love it if it were a little more flexible to get started with though. Rather than fire up ember-cli, I'd love to just be able to:
1. Load glimmer.js from a CDN
2. Extend glimmer.Component
3. Render my component into an element
I know ember-cli solves this problem for a lot of users, but I'd love for the number of steps to get a "hello world" working to be as simple as the above. I think that's one of the reasons React has been so successful: it's stupid-simple to get started with, and it draws you in from there.
The point I'm trying to make it, there's already a huge amount of churn around the vast amount build tooling needed to write any javascript these days. Part of making a framework with a low barrier to entry is having the build tooling stay out of your way until you're ready to deploy.
I get that's not necessarily Glimmer/Ember's philosophy, I just think it's a shame that using the framework from day 1 requires buying into all of the technology choices around ember-cli.
(Not to mention that the app size zealots will eat you alive)
(No Glimmer there yet, only full Ember, but you can get a feel for what it can do, how it is like, test a things out...)
The term suggests some kind of Turing complete language with sandboxing and memory management.
Without these features "domain-specific language" would be more precise description.
All development is 'just' re-solving an existing problem with better performance, or 'just' extending an existing algorithm to be more resilient, or 'just' implementing a legacy interface on a new platform.
There are lots of very obvious reasons that the web has the constraints that it does and describing them as 'unnecessary' adds literally nothing useful to that conversation.
I think it's wise to keep ourselves humble -- few of us are blazing new trails, in anything that we do. And I agree that many so-called innovations are nothing of the sort. But if we reduce everything that we do to revision and extension, then the word "innovation" entirely loses meaning. Why would we want to do that?
Only this is "re-solving an existing problem with worse performance that we had decades ago (on native), but slightly faster than the previous speeds we achieved having it run with its feet tied".
(Where by "feet tied" we refer to the performance penalty imposed by having everything run on the web stack).
Decades ago, we had the Win32 API, which didn't even try to solve this—apps had to roll their own.
That's not an actual problem people have. Nobody says "what I want is minimal MVC-style updates to a UI". What they want is a fast UI.
So an actual problem is e.g. "having a slow UI" -- and native GUIs solved it "decades ago" by being able to do stuff faster and with less memory and power compared to the web stack.
That said, there were several frameworks that allowed that, especially since MVC didn't magically appear with the web -- the concept originated with Smalltalk. And even "dumb" native GUI frameworks were much closer to the metal than the DOM, and knew how to repaint e.g. only the area of a widget that changed.
GDI wasn't "closer to the metal" than the CSS painting model was. It was reviled for not being close to the metal, in fact (which is why WinG and later DirectX were so important). On Windows NT you had to take a context switch to kernel mode to issue painting commands—how can that be "close to the metal"?
> and knew how to repaint e.g. only the area of a widget that changed
Browsers have been doing this for over a decade.
Actually, I think they should largely stop doing this, since in a double buffered scenario (which has been the norm since the Vista era) partial repaints become complicated, and GPUs are so good at blitting a window-sized area that it's really a non-issue. What are issues are state changes and overdraw, which native frameworks are exceptionally bad at minimizing (this is largely why Skia-GL is underwhelming on Android) and the declarative CSS model is good at supporting. Native frameworks such as Win32 and GTK are held back by having to support this obsolete model. None of the Win32 designers could have imagined that Z-buffers and early fragment tests would become universal.
A meta-note: Pretty much without fail whenever anybody has mentioned some specific thing that Web browsers supposedly fail to do that native can do (the incorrect idea that browsers don't do partial repaints being just the latest instance of it), browsers have been already doing that thing for years. This indicates to me that most people who complain about "native" vs. "Web" haven't really looked into how the Web works. I'm all for acknowledging the Web's shortcomings, but let's concentrate on real issues. (In my view, legacy browser design, which is largely the direct result of trying to be "native", is the biggest problem, not the Web.)
It was native on the Alto, as the primitives were implemented in microcode, which could be a FPGA nowadays.
Also Squeak and Pharo are implemented in Smalltalk.
Clearly for some subset of people, "something" is missing for those native solutions (I can guess - portability, convenient 'moddability', etc) which they believe the standardization-reliant web can better address for them.
As said above, it's not a distinction, thus it's meaningless.
Let me know when Windows gets 100ms install+load and I'll be interested in the rendering model. Until then... measuring install time in seconds, or minutes? You can hardly claim to be performance oriented.
The web is for disposable toys, uninteresting ones at that. All of Windows 3.1 could fit in less space than the Apple website. You're telling me a silent install of Windows 3.1 would take more time to install than...Some app to find local hookups by GPS?
Java was a pioneer with shipping untrusted code to users in a sandbox, but it turns out its sandbox wasn't secure, and there were other UI issues as well. (Similarly with Flash, though that was more of a mobile performance issue.)
To be fair, the early web had a lot of security problems. What we have now is what's left after a lot of hardening. And we still have a lot of security issues.
But anyway, if you use JavaScript this isn't really your problem. Browsers take care of sandboxing and deployment for you. Nothing wrong with building on the hard work of others.
I don't think "it's been done before" has ever in the history of progress been a good reason for something to not be reexamined and made better or easier to use.
I'd like to see an example of the same static gzipped HTML content delivered to the browser and a template rendered with this.
I have doubts this is any more efficient than the browser's built-in gzip decoder on a time basis nor do I think it beats gzip in terms of space efficiency.
It's hard to take seriously those who claim to be optimizing things when they don't have any measurements on what they are trying to improve vs. the new method.
I don't get the gzip comparison because this is about updating views in an SPA framework (Ember)