State Manager Upgrade

As most experienced developers eventually figure out, modeling complex behavior is typically best done using a state machine. Internally, Ember models the behavior and state of several different objects as state machines:

  • The router (via router.js/route_recognizer.js)
  • DS.Model lifecycle
  • Ember.View render lifecycle

Unfortunately, all three of these use different implementations of state machines.

The router is a little bit of a unique case in the sense that it needs to track both current state (the active route) plus the current context (which model is backing that route).

However, DS.Model and Ember.View can likely be refactored to share a common statechart implementation.

The state manager that Ember Data uses is Ember’s Ember.StateManager class. Work was done to refactor Ember.View to use this same class, but it caused massive performance regressions and had to be reverted. This also explains the performance issues Ember Data users are running into when trying to load large numbers of records at once.

On the plane home from JSConf, I started spiking out a new library for building fast statecharts. However, upon landing, I discovered that @stefan.penner had already worked on a similar project with @kris.selden, with a focus on performance. I wanted to start a thread on Discourse to help us identify how we can unify our work, and make sure that we discuss all of the design constraints for a new library, so that whoever ends up doing the implementation work maximizes the value of the library to the greater Ember ecosystem.

I was tentatively calling the library I was working on governor.js (because it manages state—get it?) but am happy to reconsider if anyone has any objections.

Important Design Decisions

Microlibrary

In keeping with our recent trend of making the core components of Ember as useful as possible to our friends outside the Ember community, this library should not have a dependency on ember-runtime . Backbone.js users that want to use a state machine should be able to.

Performance

Given that this library is intended for use by both the Ember view system and for the creation of Ember Data models, both potentially hot paths in end user applications, performance is of the utmost importance. In particular, creating new instances of state charts should be made as cheap as possible.

The main avenues of accomplishing this are:

  • All state managers should share a single instance of the tree of state objects.
  • Reduce the dependence on creating Ember.Object instances.
  • Dispatching events to the state machine should be cached methods so that V8 can inline the function in cases where it becomes a hot path.

Stefan and Kris can probably share some more insight into their work on improving the performance on the view layer.

Ideally, we can add performance benchmarks to the library and be mindful of merging in PRs that cause regressions.

Improved API

The current API leads to code that is rather unwieldy and difficult for new developers to reason about. For an example, just take a look at the current state chart for DS.Model: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js

I think an important feature of the Ember router API that we need to learn from is that it separates hierarchy from behavior. Currently, the two are conflated, leading to deeply nested, noisy structures that are reminiscent of the router v1 API.

I’d like to propose that the new API separate these concerns. Each state can be a separate object in a flat hierarchy, and then that hierarchy is “stitched together” in a separate function. For example, here’s my first stab at an API for this, using the current DS.Model statechart as a strawman:

var ModelStates = Governor.Statechart.map(function() {
  this.state('root', function() {
    this.states('empty', 'loading');

    this.state('loaded', function() {
      this.states('reloading', 'saved');

      this.state('materializing', function() {
        this.state('firstTime');
      });

      this.state('created', function() {
        this.state('uncommitted');
        this.state('inFlight');
        this.state('invalid')
      });
    });

  });
});

While this is an improvement, it can be made even nicer for CoffeeScript users:

ModelStatechart = Governor.Statechart.map """
  root
    empty
    loading
    loaded
      materializing
        firstTime
      reloading
      saved
      created
        uncommitted
        inFlight
        invalid
      updated
        uncommitted
        inFlight
        invalid
    deleted
      uncommitted
      inFlight
      saved
    error
"""

Once we have ES6, this API could be used nicely with the new string templates feature, which supports multiline strings:

var ModelStates = Governor.Statechart.map(`
  root
    empty
    loading
    loaded
      materializing
        firstTime
      reloading
      saved
      created
        uncommitted
        inFlight
        invalid
      updated
        uncommitted
        inFlight
        invalid
    deleted
      uncommitted
      inFlight
      saved
    error
`);

Constrained Focus

It is of course easy to try to predict how end users will want to use a library, and add features that we anticipate that will be helpful. For the purposes of this discussion, and for getting a version 1 out the door, I would appreciate it if we could constrain API strawmen to discussing specifically the Ember.View/DS.Model use cases. Once we have unified the implementation for both I am happy to discuss additional features that add syntactic sugar for using the API.

15 Likes

I like separating hierarchy from behavior – it’s a good lesson to take from the new router, and I’ve definitely struggled in the past trying to comprehend the DS.Model states.

Questions:

  1. Wil this affect the actual state hierarchy / behaviors of DS.Model? Will we have to change any external code?
  2. Have you considered representing the state hierarchy as a nested object literal? Seems more “natural” JS than a multiline string with indentation. CoffeeScript users get nearly the same benefit – the indentation and hierarchy is enforced by the language.
 
 Governor.Statechart.map
   root:
     empty: true
     loading: true
     loaded:
       materializing
         firstTime: true
    ...

The only concern I can think of is that leaf states need to be represented some how – here it’s using “true”.

@tom how do you envision matching the behaviour and hierarchy back together? The Ember router does a lookup based on the name of the route to a corresponding class. This is great for Ember but not so much externally.

1 Like

Flipping through Ember.StateManager it appears that even though this became a plugin, the API for separating hierarchy from behavior as proposed by Tom never moved forward.

Is anybody aware of active work on this topic? We’re getting ready to implement something like this for handling route transition eligibility…

I could really use this today…

It would be nice to have access to a generic state object within ember. Knowing you guys are working on this makes me reconsider the plans for my apps implementation.

+1

As far as I know, no one has had the time to drive this. If you’re up for it, I’m happy to assist however i can.