YUI attrs overhead

JavaScript performance comparison

Revision 2 of this test case created

Info

Every time I've tried to go full-on MVC with YUI with a formal Y.Model that updates the view on model changes, I've wound up not doing it. Naïve updates on change cause reflow thrashing but even after working that out, I've canceled several prototypes after seeing models with hundreds of varying ATTRS introduce noticeable processing delays. My conclusion is that "YUI Attrs are slow" and I've shifted personal development to Ember and now Angular.

At work we're tied to YUI. Main product is currently 125k sloc of first party js in yui3. I believe the product has an essential complexity around 20-25k sloc but I need to standardize on a model, implement two way databinding, and move some pieces around to get the code reduction. My desire has been to do a rewrite in another framework but I've been told that absolutely nobody else wants to try this. I know about Luke's pull request 386 for the databinding and I know Attribute perf is on the roadmap I need this sooner than later. In the interest of keeping changes to YUI minimal, I decided to monkeypatch Model to not use Attribute.

A couple hours later and this is the low quality code result. With both the change events firing, it passes all Model unit tests except for the one that uses a subattr get (model.get('foo.a')) since the get doesn't support subattrs.

The main slowdown turned out to be the Event system, as shown below. Not shown are faster options that don't match Model semantics. Skipping the object copying required to implement .lastChange and the event prevVal/nextVal attributes is around twice as fast as the no-event version of this code. At the top end, using get/set as pure accessor/modifiers on an object is 20x as fast as the no-event benchmark here.

Put this here and wrote it up in case it's helpful. I have a significant amount of work to get to a production grade Y.Model workalike but in the meantime I think just using this and requestAnimationFrame polling the .changed attr will work for our perf-sensitive models.

Preparation code

<script src="http://yui.yahooapis.com/3.8.1/build/yui/yui-min.js"></script>
<script>
YUI().use('model', function(Y) {

  var isUndef = Y.Lang.isUndefined;
  window.doAttrChangeEvents = false;
  window.doChangeEvents = false;

  var Model = Y.Base.create('model', Y.Model, [], {
    initializer: function(o) {
      var idAttribute = this.__idAttribute = this.constructor.prototype.idAttribute;
      var sc = this.constructor.superclass;
      var self = this;
      var get = this.get;

      this.__attrs = Y.merge(o || {});
      this.__oldAttrs = {};
      this.__oldChangeName = '';
      this.__new = true;

      this.publish('change', { preventable: false });

      if (idAttribute) {
        if (o && o[idAttribute]) {
          this._set('id', this.get(idAttribute));
        } else if (o && o.id) {
          this._set(idAttribute, o.id);
        }
      }

      // the isModified tests check for this, not quite sure why
      if (o && (o.id || o[idAttribute])) {
        this.__new = false;
      }
    },
    getPostInit: function(name) {
      return this.__attrs[name];
    },
    _set: function(name, val) {
      if (name === 'initialized' && val) {
        // Force ATTRS initialization and lazy instantiation
        // since lazy attrs force a set and we're trapping that, our attrs trap
        // object gets initialized
        this.getAttrs();
        this.get = this.getPostInit;
        this.getAttrs = this.getAttrsPostInit;
        this.changed = {};
        this.lastChange = {};
      }
      this.__attrs[name] = val;
      return this;
    },
    save: function() {
      Y.Model.prototype.save.apply(this, arguments);
      this.changed = {};
      this.__new = false;
      return this;
    },
    isModified: function() {
      for( var k in this.changed ) { return true; }
      return this.__new;
    },
    _buildChange: function(keys, prev, curr, opts){
      var out = {};
      for (var i = 0, ii = keys.length; i < ii; i++) {
        var k = keys[i];
        if (prev[k] !== curr[k]) {
          out[k] = Y.mix({ prevVal: prev[k], newVal: curr[k], src: null }, opts, true);
        }
      }
      return out;
    },
    set: function(name, val, opts) {
      this.changed[name] = val;
      this.__oldAttrs[name] = this.__attrs[name];
      this.__oldChangeName = name;
      this.__attrs[name] = val;
      var updatedKeys;

      if (this.__idAttribute && name === 'id' || name === this.__idAttribute) {
        this.__attrs.id = this.__attrs[this.__idAttribute] = val;
        updatedKeys = ['id', this.__idAttribute];
      }

      var changeObj = this._buildChange(updatedKeys || [name], this.__oldAttrs, this.__attrs, opts);

      for (var k in changeObj) {
        this.lastChange = changeObj
        if (!opts || !opts.silent) {
          if (doAttrChangeEvents) { this.fire(name+'Change', this.lastChange[name]); }
          if (doChangeEvents) { this.fire('change', Y.mix({changed: this.lastChange}, opts, true)); }
        }
        break;
      }
      return this;
    },
    getAttrsPostInit: function() {
      return Y.merge(this.__attrs);
    },
    setAttrs: function(nv, opts) {

      if (!nv) { return this; }

      if (this.__idAttribute && nv.id || nv[this.__idAttribute]) {
        nv.id = nv[this.__idAttribute] || nv.id;
        nv[this.__idAttribute] = nv.id
      }

      Y.mix(this.changed, nv, true);
      this.__oldAttrs = this.__attrs;
      this.__oldChangeName = '';
      this.__attrs = Y.merge(this.__attrs, nv);

      var changeObj = this._buildChange(Y.Object.keys(this.__oldAttrs), this.__oldAttrs, this.__attrs, opts);
      var hasChanges = false;

        for (var k in changeObj) {
          hasChanges = true;
          if ((!opts || !opts.silent) && doAttrChangeEvents) {
            this.fire(name+'Change', this.lastChange[name]);
          } else {
            break;
          }
        }
      if (hasChanges) {
        this.lastChange = changeObj;
        if ((!opts || !opts.silent) && doChangeEvents) {
          this.fire('change', Y.mix({changed: this.lastChange}, opts, true));
        }
      }
      return this;
    },
    undo: function(attrNames, opts) {
      if (!this.__oldAttrs) { return this; }

      attrNames || (attrNames = Y.Object.keys(this.__oldAttrs));
      if (this.__oldChangeName && !isUndef(this.__oldAttrs[this.__oldChangeName])) {
        attrNames = [this.__oldChangeName];
      }

      var toUndo = {}, needUndo = false;
      for (var i = 0, ii = attrNames.length; i < ii; i++) {
        if (!isUndef(this.__oldAttrs[attrNames[i]])) {
          toUndo[attrNames[i]] = this.__oldAttrs[attrNames[i]];
          needUndo = true;
        }
      }

      return needUndo? this.setAttrs(toUndo, opts) : this;
    },
  });

  window.modelAttrs = new Y.Model({foo: 0});
  window.modelNoAttrs = new Model({foo: 0});

});
</script>
<script>
Benchmark.prototype.setup = function() {
    modelAttrs.setAttrs({foo: 0});
    modelNoAttrs.setAttrs({foo: 0});
};
</script>

Preparation code output

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in unknown unknown
Test Ops/sec
attrs
modelAttrs.set('foo', modelAttrs.get('foo') + 1);
pending…
no attributeChange events
doAttrChangeEvents = false;
doChangeEvents = true;
modelNoAttrs.set('foo', modelNoAttrs.get('foo') + 1);
pending…
no events
doAttrChangeEvents = false;
doChangeEvents = false;
modelNoAttrs.set('foo', modelNoAttrs.get('foo') + 1);
pending…

Compare results of other browsers

Revisions

You can edit these tests or add even more tests to this page by appending /edit to the URL. Here’s a list of current revisions for this page:

0 comments

Add a comment