Saturday, October 1, 2011

Sproutcore Bindings, Observers, and Performance

Here's another Sproutcore performance tip that applies when you have computed properties depending on other computed properties. Consider this controller: Assume that the subpath property is updated to reflect our current URL (I'm not showing that code here, but check out sproutcore-routing). We figure out what month and year to show by parsing this URL fragment, or by providing suitable defaults based on the current time. Then we combine them into a date, which we use elsewhere to draw the appropriate calendar.

This all works fine, but it suffers a performance problem. To understand why, you must understand a little about how computed properties are implemented, and the difference between Observers and Bindings.

Computed Properties are Invalidated by Observers

An Observer is essentially a hook that runs immediately whenever you change a property. Computed properties like month, year, and firstDate are immediately invalidated by Observers whenever their dependencies change, which is good because it prevent a lot of potential race conditions. However, this means that every time subpath changes, firstDate will get invalidated even if month and year haven't really changed values. This is an inevitable side-effect of the fact that we want both

  1. immediate invalidation of cached properties when their dependencies change, and
  2. lazy evaluation of computed properties.

Bindings to the Rescue

This is one of the reasons we have Bindings. Bindings are implemented on top of Observers, but they are smarter and lazier. A Binding connects two properties (hereafter called "left" and "right"). The Binding will notice (through an Observer) that its left property has been invalidated. It will then wait until the end of the current run loop (which helps aggregate changes), and then it will read the left property (triggering re-evaluation). If it sees the same value it already had before, it will do nothing to the right property, and any computed properties that depend on the right property will not get invalidated.

Which brings us to the solution: By inserting bindings between our computed properties, we ensure that firstDate only re-evaluates when the month or year has truly changed. This in turn prevents everything else that depends on firstDate from getting re-evaluated and re-rendered every time an irrelevant change is made to subpath.

Classes like Date make this technique even more important, because two Date objects representing the exact same time are not === to each other, or even ==. So even if your drawing code uses a Binding to watch the date, it will detect a change every time firstDate re-evaluates, regardless of whether it truly represents a different date.