Designing and Implementing Glimmer Like a Programming Language

The first Ember view layer was designed kind of like you'd expect: it treated the "template language" as a very specialized external DSL with a decent amount of special syntax.

In addition, the implementation was very ad-hoc, which led to a number of persistent, whack-a-mole bugs (including cases where "string literals" were interpreted as expressions and bare words were interpreted as string literals!)

When we started to work on the next generation of our engine (Glimmer), I made an intentional decision to design it as a programming language and to implement it using standard programming language techniques.

As a simple example, here's the syntax for loops in Ember 1.0:

<ul>  
{{#each item in items}}
  <li>{{item}}</li>
{{/each}}
</ul>  

Each form in our templates that introduced new names had its own syntax, implemented in an ad hoc way without a coherent, rationalized set of semantics.

This is the syntax for loops in Ember 2.0 (after Glimmer):

<ul>  
{{#each items as |item|}}
  <li>{{item}}</li>
{{/each}}
</ul>  

It might look pretty similar, but that as |item| syntax is the standard (and only) way to introduce names into a Glimmer template.

This paved the way for user-specified components to also expose names to passed blocks, something that we could never have done before because of how ad-hoc the implementation and syntax was.

The addon ember-power-select makes good use of this functionality:

{{#power-select
  options=countries
  selected=destination
  as |country|}}
  <img src="{{country.flagUrl}}" class="icon-flag"> <strong>{{country.name}}</strong>
{{/power-select}}

But designing the semantics in a more traditional way had another benefit: we could take advantage of standard programming language implementation techniques to improve performance.

Glimmer 2, the second iteration of the Glimmer engine, migrated the implementation to a VM architecture, which allows us to perform a number of simple optimizations like pre-computing values during first-tier compilation, avoiding work when values are constant or static, and using deoptimizations for slow paths that are very uncommon in practice.

Last year, we did work to transition the VM architecture to binary internals (currently 128-bit opcodes, but with further changes expected), which significantly reduced machine overhead.

Earlier this year (hey, I needed something to do on paternity leave), we began transitioning the opcode machinery to evaluate expressions and rationalize all of our invocation styles to pass arguments on the stack.

In general, these changes have all been driven by performance goals: reducing machine overhead, making internals more consistent, enabling desired optimizations.

And the more we've moved our internals to standard machinery, the more we've found that our understanding of our own system has improved, giving us more ability to do interesting things with it.

I'll close with one recent example: you may have seen that React announced that they redesigned their view layer around "React Fiber", which allows them to break up rendering into pieces to avoid browser jank and improve concurrency and scheduling.

The basic idea at a high level is that instead of doing all the work at once, which freezes the browser, you want to break up the work of rendering into pieces, letting the browser proceed as you go. You should check out Andrew Clark's great summary of React Fiber's architecture for more.

The punchline is that because Glimmer is implemented as a bytecode VM, we were able to implement a similar feature very quickly earlier this year. The basic story is that instead of running all of the opcodes from start to finish synchronously, our execution is now a generator. This allows the caller to run the execution until some deadline, yield back to the browser, and resume execution after the browser had a chance to do it's thing.

A recent commit to our job queue ("backburner") allows it to handle "smeared" tasks that can run across multiple turns. Together, this means that Ember can schedule a large Glimmer rendering jobs. And because Glimmer jobs can be paused at each opcode, the rendering process will naturally avoid browser jank.

There's a lot of other cool stuff in React Fiber, but the bottom line is that by designing Glimmer to have standard programming language semantics like lexical scope, and by implementing it like a VM, we can use standard techniques to achieve goals that are relevant for web applications.