From 82365e392ea471c504ebcd3787d2d71578ec644b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:04:11 -0400 Subject: [PATCH 1/7] internal Collection#_add and Collection#_remove, should return the model, in case they're overridden. --- backbone.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backbone.js b/backbone.js index 20e5fa6b..cb407ea9 100644 --- a/backbone.js +++ b/backbone.js @@ -441,6 +441,7 @@ model.bind('all', this._boundOnModelEvent); this.length++; if (!options.silent) this.trigger('add', model); + return model; }, // Internal implementation of removing a single model from the set, updating @@ -456,6 +457,7 @@ model.unbind('all', this._boundOnModelEvent); this.length--; if (!options.silent) this.trigger('remove', model); + return model; }, // Internal method called every time a model in the set fires an event. From 7c901e2245f26f8c14bfe35df7ca333ae6874f02 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:15:25 -0400 Subject: [PATCH 2/7] Slightly shallower namespaced export for CommonJS. --- backbone.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/backbone.js b/backbone.js index cb407ea9..4d1a6c4b 100644 --- a/backbone.js +++ b/backbone.js @@ -8,21 +8,24 @@ // Initial Setup // ------------- - // The top-level namespace. - var Backbone = {}; + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both CommonJS and the browser. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = this.Backbone = {}; + } - // Keep the version here in sync with `package.json`. + // Current version of the library. Keep in sync with `package.json`. Backbone.VERSION = '0.1.1'; - // Export for both CommonJS and the browser. - (typeof exports !== 'undefined' ? exports : this).Backbone = Backbone; - // Require Underscore, if we're on the server, and it's not already present. var _ = this._; if (!_ && (typeof require !== 'undefined')) _ = require("underscore")._; // For Backbone's purposes, jQuery owns the `$` variable. - var $ = this.$; + var $ = this.jQuery; // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and From 9c535ca5a55fa2003146270e745258415b6083a4 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:31:19 -0400 Subject: [PATCH 3/7] expand inherits helper child constructor creation, for clarity. --- backbone.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backbone.js b/backbone.js index 4d1a6c4b..064b497b 100644 --- a/backbone.js +++ b/backbone.js @@ -31,8 +31,12 @@ // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. var inherits = function(parent, protoProps, classProps) { - var child = protoProps.hasOwnProperty('constructor') ? protoProps.constructor : - function(){ return parent.apply(this, arguments); }; + var child; + if (protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } var ctor = function(){}; ctor.prototype = parent.prototype; child.prototype = new ctor(); From 2ae60985eee091504c9bd23d209229805e78ce13 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:34:00 -0400 Subject: [PATCH 4/7] Moving all helper functions down to the bottom. --- backbone.js | 56 +++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/backbone.js b/backbone.js index 064b497b..e9999352 100644 --- a/backbone.js +++ b/backbone.js @@ -27,31 +27,6 @@ // For Backbone's purposes, jQuery owns the `$` variable. var $ = this.jQuery; - // Helper function to correctly set up the prototype chain, for subclasses. - // Similar to `goog.inherits`, but uses a hash of prototype properties and - // class properties to be extended. - var inherits = function(parent, protoProps, classProps) { - var child; - if (protoProps.hasOwnProperty('constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } - var ctor = function(){}; - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - _.extend(child.prototype, protoProps); - if (classProps) _.extend(child, classProps); - child.prototype.constructor = child; - return child; - }; - - // Helper function to get a URL from a Model or Collection as a property - // or as a function. - var getUrl = function(object) { - return _.isFunction(object.url) ? object.url() : object.url; - }; - // Backbone.Events // ----------------- @@ -616,6 +591,9 @@ 'read' : 'GET' }; + // Backbone.sync + // ------------- + // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, uses jQuery to make a RESTful Ajax request @@ -636,4 +614,32 @@ }); }; + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain, for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var inherits = function(parent, protoProps, classProps) { + var child; + if (protoProps.hasOwnProperty('constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + var ctor = function(){}; + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + _.extend(child.prototype, protoProps); + if (classProps) _.extend(child, classProps); + child.prototype.constructor = child; + return child; + }; + + // Helper function to get a URL from a Model or Collection as a property + // or as a function. + var getUrl = function(object) { + return _.isFunction(object.url) ? object.url() : object.url; + }; + })(); From 3560062c11a7919688c861c2b4c5dd86ff3e13c5 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 13:49:01 -0400 Subject: [PATCH 5/7] removing redundant assignment in Events#trigger --- backbone.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backbone.js b/backbone.js index e9999352..58b6bdd5 100644 --- a/backbone.js +++ b/backbone.js @@ -79,7 +79,6 @@ // Listening for `"all"` passes the true event name as the first argument. trigger : function(ev) { var list, calls, i, l; - var calls = this._callbacks; if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { From e7ce57cc1dbb2b5d3048428333153453baf3817a Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 14:46:53 -0400 Subject: [PATCH 6/7] Adding the beginnings of a speed suite to the Test page. --- backbone.js | 2 +- test/collection.js | 22 +- test/{bindable.js => events.js} | 8 +- test/model.js | 22 +- test/speed.js | 25 ++ test/test.html | 7 +- test/vendor/jslitmus.js | 649 ++++++++++++++++++++++++++++++++ test/view.js | 15 +- 8 files changed, 715 insertions(+), 35 deletions(-) rename test/{bindable.js => events.js} (84%) create mode 100644 test/speed.js create mode 100644 test/vendor/jslitmus.js diff --git a/backbone.js b/backbone.js index 58b6bdd5..d0a1acc9 100644 --- a/backbone.js +++ b/backbone.js @@ -82,7 +82,7 @@ if (!(calls = this._callbacks)) return this; if (list = calls[ev]) { for (i = 0, l = list.length; i < l; i++) { - list[i].apply(this, _.rest(arguments)); + list[i].apply(this, Array.prototype.slice.call(arguments, 1)); } } if (list = calls['all']) { diff --git a/test/collection.js b/test/collection.js index fb9cec85..de5c974f 100644 --- a/test/collection.js +++ b/test/collection.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Backbone collections"); + module("Backbone.Collection"); window.lastRequest = null; @@ -15,7 +15,7 @@ $(document).ready(function() { var e = null; var col = window.col = new Backbone.Collection([a,b,c,d]); - test("collections: new and sort", function() { + test("Collection: new and sort", function() { equals(col.first(), a, "a should be first"); equals(col.last(), d, "d should be last"); col.comparator = function(model) { return model.id; }; @@ -25,21 +25,21 @@ $(document).ready(function() { equals(col.length, 4); }); - test("collections: get, getByCid", function() { + test("Collection: get, getByCid", function() { equals(col.get(1), d); equals(col.get(3), b); equals(col.getByCid(col.first().cid), col.first()); }); - test("collections: at", function() { + test("Collection: at", function() { equals(col.at(2), b); }); - test("collections: pluck", function() { + test("Collection: pluck", function() { equals(col.pluck('label').join(' '), 'd c b a'); }); - test("collections: add", function() { + test("Collection: add", function() { var added = null; col.bind('add', function(model){ added = model.get('label'); }); e = new Backbone.Model({id: 0, label : 'e'}); @@ -49,7 +49,7 @@ $(document).ready(function() { equals(col.first(), e); }); - test("collections: remove", function() { + test("Collection: remove", function() { var removed = null; col.bind('remove', function(model){ removed = model.get('label'); }); col.remove(e); @@ -58,13 +58,13 @@ $(document).ready(function() { equals(col.first(), d); }); - test("collections: fetch", function() { + test("Collection: fetch", function() { col.fetch(); equals(lastRequest[0], 'read'); equals(lastRequest[1], col); }); - test("collections: create", function() { + test("Collection: create", function() { var model = col.create({label: 'f'}); equals(lastRequest[0], 'create'); equals(lastRequest[1], model); @@ -82,7 +82,7 @@ $(document).ready(function() { equals(coll.one, 1); }); - test("collections: Underscore methods", function() { + test("Collection: Underscore methods", function() { equals(col.map(function(model){ return model.get('label'); }).join(' '), 'd c b a'); equals(col.any(function(model){ return model.id === 100; }), false); equals(col.any(function(model){ return model.id === 1; }), true); @@ -97,7 +97,7 @@ $(document).ready(function() { equals(col.min(function(model){ return model.id; }).id, 1); }); - test("collections: refresh", function() { + test("Collection: refresh", function() { var refreshed = 0; var models = col.models; col.bind('refresh', function() { refreshed += 1; }); diff --git a/test/bindable.js b/test/events.js similarity index 84% rename from test/bindable.js rename to test/events.js index 1d92bfab..83808428 100644 --- a/test/bindable.js +++ b/test/events.js @@ -1,8 +1,8 @@ $(document).ready(function() { - module("Backbone bindable"); + module("Backbone.Events"); - test("bindable: bind and trigger", function() { + test("Events: bind and trigger", function() { var obj = { counter: 0 }; _.extend(obj,Backbone.Events); obj.bind('event', function() { obj.counter += 1; }); @@ -15,7 +15,7 @@ $(document).ready(function() { equals(obj.counter, 5, 'counter should be incremented five times.'); }); - test("bindable: bind, then unbind all functions", function() { + test("Events: bind, then unbind all functions", function() { var obj = { counter: 0 }; _.extend(obj,Backbone.Events); var callback = function() { obj.counter += 1; }; @@ -26,7 +26,7 @@ $(document).ready(function() { equals(obj.counter, 1, 'counter should have only been incremented once.'); }); - test("bindable: bind two callbacks, unbind only one", function() { + test("Events: bind two callbacks, unbind only one", function() { var obj = { counterA: 0, counterB: 0 }; _.extend(obj,Backbone.Events); var callback = function() { obj.counterA += 1; }; diff --git a/test/model.js b/test/model.js index f10e9145..cedb0050 100644 --- a/test/model.js +++ b/test/model.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Backbone model"); + module("Backbone.Model"); // Variable to catch the last request. window.lastRequest = null; @@ -26,7 +26,7 @@ $(document).ready(function() { var collection = new klass(); collection.add(doc); - test("model: initialize", function() { + test("Model: initialize", function() { var Model = Backbone.Model.extend({ initialize: function() { this.one = 1; @@ -36,11 +36,11 @@ $(document).ready(function() { equals(model.one, 1); }); - test("model: url", function() { + test("Model: url", function() { equals(doc.url(), '/collection/1-the-tempest'); }); - test("model: clone", function() { + test("Model: clone", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); b = a.clone(); @@ -55,7 +55,7 @@ $(document).ready(function() { equals(b.get('foo'), 1, "Changing a parent attribute does not change the clone."); }); - test("model: isNew", function() { + test("Model: isNew", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); ok(a.isNew(), "it should be new"); @@ -63,12 +63,12 @@ $(document).ready(function() { ok(a.isNew(), "any defined ID is legal, negative or positive"); }); - test("model: get", function() { + test("Model: get", function() { equals(doc.get('title'), 'The Tempest'); equals(doc.get('author'), 'Bill Shakespeare'); }); - test("model: set and unset", function() { + test("Model: set and unset", function() { attrs = { 'foo': 1, 'bar': 2, 'baz': 3}; a = new Backbone.Model(attrs); var changeCount = 0; @@ -85,7 +85,7 @@ $(document).ready(function() { ok(changeCount == 2, "Change count should have incremented for unset."); }); - test("model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() { + test("Model: changed, hasChanged, changedAttributes, previous, previousAttributes", function() { var model = new Backbone.Model({name : "Tim", age : 10}); model.bind('change', function() { ok(model.hasChanged('name'), 'name changed'); @@ -99,19 +99,19 @@ $(document).ready(function() { equals(model.get('name'), 'Rob'); }); - test("model: save", function() { + test("Model: save", function() { doc.save({title : "Henry V"}); equals(lastRequest[0], 'update'); ok(_.isEqual(lastRequest[1], doc)); }); - test("model: destroy", function() { + test("Model: destroy", function() { doc.destroy(); equals(lastRequest[0], 'delete'); ok(_.isEqual(lastRequest[1], doc)); }); - test("model: validate", function() { + test("Model: validate", function() { var lastError; var model = new Backbone.Model(); model.validate = function(attrs) { diff --git a/test/speed.js b/test/speed.js new file mode 100644 index 00000000..0f29d47f --- /dev/null +++ b/test/speed.js @@ -0,0 +1,25 @@ +(function(){ + + var object = {}; + _.extend(object, Backbone.Events); + var fn = function(){}; + + JSLitmus.test('Events: bind + unbind', function() { + object.bind("event", fn); + object.unbind("event", fn); + }); + + object.bind('test:trigger', fn); + + JSLitmus.test('Events: trigger', function() { + object.trigger('test:trigger'); + }); + + object.bind('test:trigger2', fn); + object.bind('test:trigger2', fn); + + JSLitmus.test('Events: trigger 2 functions, passing 5 arguments', function() { + object.trigger('test:trigger2', 1, 2, 3, 4, 5); + }); + +})(); \ No newline at end of file diff --git a/test/test.html b/test/test.html index 4ecd3863..22d16f04 100644 --- a/test/test.html +++ b/test/test.html @@ -5,18 +5,23 @@ + - + +

Backbone Test Suite

    +

    +

    Backbone Speed Suite

    +
    diff --git a/test/vendor/jslitmus.js b/test/vendor/jslitmus.js new file mode 100644 index 00000000..a4111791 --- /dev/null +++ b/test/vendor/jslitmus.js @@ -0,0 +1,649 @@ +// JSLitmus.js +// +// Copyright (c) 2010, Robert Kieffer, http://broofa.com +// Available under MIT license (http://en.wikipedia.org/wiki/MIT_License) + +(function() { + // Private methods and state + + // Get platform info but don't go crazy trying to recognize everything + // that's out there. This is just for the major platforms and OSes. + var platform = 'unknown platform', ua = navigator.userAgent; + + // Detect OS + var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|'); + var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null; + if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null; + + // Detect browser + var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null; + + // Detect version + var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)'); + var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null; + var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform'; + + /** + * A smattering of methods that are needed to implement the JSLitmus testbed. + */ + var jsl = { + /** + * Enhanced version of escape() + */ + escape: function(s) { + s = s.replace(/,/g, '\\,'); + s = escape(s); + s = s.replace(/\+/g, '%2b'); + s = s.replace(/ /g, '+'); + return s; + }, + + /** + * Get an element by ID. + */ + $: function(id) { + return document.getElementById(id); + }, + + /** + * Null function + */ + F: function() {}, + + /** + * Set the status shown in the UI + */ + status: function(msg) { + var el = jsl.$('jsl_status'); + if (el) el.innerHTML = msg || ''; + }, + + /** + * Convert a number to an abbreviated string like, "15K" or "10M" + */ + toLabel: function(n) { + if (n == Infinity) { + return 'Infinity'; + } else if (n > 1e9) { + n = Math.round(n/1e8); + return n/10 + 'B'; + } else if (n > 1e6) { + n = Math.round(n/1e5); + return n/10 + 'M'; + } else if (n > 1e3) { + n = Math.round(n/1e2); + return n/10 + 'K'; + } + return n; + }, + + /** + * Copy properties from src to dst + */ + extend: function(dst, src) { + for (var k in src) dst[k] = src[k]; return dst; + }, + + /** + * Like Array.join(), but for the key-value pairs in an object + */ + join: function(o, delimit1, delimit2) { + if (o.join) return o.join(delimit1); // If it's an array + var pairs = []; + for (var k in o) pairs.push(k + delimit1 + o[k]); + return pairs.join(delimit2); + }, + + /** + * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution + */ + indexOf: function(arr, o) { + if (arr.indexOf) return arr.indexOf(o); + for (var i = 0; i < this.length; i++) if (arr[i] === o) return i; + return -1; + } + }; + + /** + * Test manages a single test (created with + * JSLitmus.test()) + * + * @private + */ + var Test = function (name, f) { + if (!f) throw new Error('Undefined test function'); + if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) { + throw new Error('"' + name + '" test: Test is not a valid Function object'); + } + this.loopArg = RegExp.$1; + this.name = name; + this.f = f; + }; + + jsl.extend(Test, /** @lends Test */ { + /** Calibration tests for establishing iteration loop overhead */ + CALIBRATIONS: [ + new Test('calibrating loop', function(count) {while (count--);}), + new Test('calibrating function', jsl.F) + ], + + /** + * Run calibration tests. Returns true if calibrations are not yet + * complete (in which case calling code should run the tests yet again). + * onCalibrated - Callback to invoke when calibrations have finished + */ + calibrate: function(onCalibrated) { + for (var i = 0; i < Test.CALIBRATIONS.length; i++) { + var cal = Test.CALIBRATIONS[i]; + if (cal.running) return true; + if (!cal.count) { + cal.isCalibration = true; + cal.onStop = onCalibrated; + //cal.MIN_TIME = .1; // Do calibrations quickly + cal.run(2e4); + return true; + } + } + return false; + } + }); + + jsl.extend(Test.prototype, {/** @lends Test.prototype */ + /** Initial number of iterations */ + INIT_COUNT: 10, + /** Max iterations allowed (i.e. used to detect bad looping functions) */ + MAX_COUNT: 1e9, + /** Minimum time a test should take to get valid results (secs) */ + MIN_TIME: .5, + + /** Callback invoked when test state changes */ + onChange: jsl.F, + + /** Callback invoked when test is finished */ + onStop: jsl.F, + + /** + * Reset test state + */ + reset: function() { + delete this.count; + delete this.time; + delete this.running; + delete this.error; + }, + + /** + * Run the test (in a timeout). We use a timeout to make sure the browser + * has a chance to finish rendering any UI changes we've made, like + * updating the status message. + */ + run: function(count) { + count = count || this.INIT_COUNT; + jsl.status(this.name + ' x ' + count); + this.running = true; + var me = this; + setTimeout(function() {me._run(count);}, 200); + }, + + /** + * The nuts and bolts code that actually runs a test + */ + _run: function(count) { + var me = this; + + // Make sure calibration tests have run + if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return; + this.error = null; + + try { + var start, f = this.f, now, i = count; + + // Start the timer + start = new Date(); + + // Now for the money shot. If this is a looping function ... + if (this.loopArg) { + // ... let it do the iteration itself + f(count); + } else { + // ... otherwise do the iteration for it + while (i--) f(); + } + + // Get time test took (in secs) + this.time = Math.max(1,new Date() - start)/1000; + + // Store iteration count and per-operation time taken + this.count = count; + this.period = this.time/count; + + // Do we need to do another run? + this.running = this.time <= this.MIN_TIME; + + // ... if so, compute how many times we should iterate + if (this.running) { + // Bump the count to the nearest power of 2 + var x = this.MIN_TIME/this.time; + var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2)))); + count *= pow; + if (count > this.MAX_COUNT) { + throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.'); + } + } + } catch (e) { + // Exceptions are caught and displayed in the test UI + this.reset(); + this.error = e; + } + + // Figure out what to do next + if (this.running) { + me.run(count); + } else { + jsl.status(''); + me.onStop(me); + } + + // Finish up + this.onChange(this); + }, + + /** + * Get the number of operations per second for this test. + * + * @param normalize if true, iteration loop overhead taken into account + */ + getHz: function(/**Boolean*/ normalize) { + var p = this.period; + + // Adjust period based on the calibration test time + if (normalize && !this.isCalibration) { + var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1]; + + // If the period is within 20% of the calibration time, then zero the + // it out + p = p < cal.period*1.2 ? 0 : p - cal.period; + } + + return Math.round(1/p); + }, + + /** + * Get a friendly string describing the test + */ + toString: function() { + return this.name + ' - ' + this.time/this.count + ' secs'; + } + }); + + // CSS we need for the UI + var STYLESHEET = ''; + + // HTML markup for the UI + var MARKUP = '
    \ + \ + \ +
    \ +
    \ + Normalize results \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
    ' + platform + '
    TestOps/sec
    \ +
    \ + \ + Powered by JSLitmus \ +
    '; + + /** + * The public API for creating and running tests + */ + window.JSLitmus = { + /** The list of all tests that have been registered with JSLitmus.test */ + _tests: [], + /** The queue of tests that need to be run */ + _queue: [], + + /** + * The parsed query parameters the current page URL. This is provided as a + * convenience for test functions - it's not used by JSLitmus proper + */ + params: {}, + + /** + * Initialize + */ + _init: function() { + // Parse query params into JSLitmus.params[] hash + var match = (location + '').match(/([^?#]*)(#.*)?$/); + if (match) { + var pairs = match[1].split('&'); + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + if (pair.length > 1) { + var key = pair.shift(); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + this.params[key] = value; + } + } + } + + // Write out the stylesheet. We have to do this here because IE + // doesn't honor sheets written after the document has loaded. + document.write(STYLESHEET); + + // Setup the rest of the UI once the document is loaded + if (window.addEventListener) { + window.addEventListener('load', this._setup, false); + } else if (document.addEventListener) { + document.addEventListener('load', this._setup, false); + } else if (window.attachEvent) { + window.attachEvent('onload', this._setup); + } + + return this; + }, + + /** + * Set up the UI + */ + _setup: function() { + var el = jsl.$('jslitmus_container'); + if (!el) document.body.appendChild(el = document.createElement('div')); + + el.innerHTML = MARKUP; + + // Render the UI for all our tests + for (var i=0; i < JSLitmus._tests.length; i++) + JSLitmus.renderTest(JSLitmus._tests[i]); + }, + + /** + * (Re)render all the test results + */ + renderAll: function() { + for (var i = 0; i < JSLitmus._tests.length; i++) + JSLitmus.renderTest(JSLitmus._tests[i]); + JSLitmus.renderChart(); + }, + + /** + * (Re)render the chart graphics + */ + renderChart: function() { + var url = JSLitmus.chartUrl(); + jsl.$('chart_link').href = url; + jsl.$('chart_image').src = url; + jsl.$('chart').style.display = ''; + + // Update the tiny URL + jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url); + }, + + /** + * (Re)render the results for a specific test + */ + renderTest: function(test) { + // Make a new row if needed + if (!test._row) { + var trow = jsl.$('test_row_template'); + if (!trow) return; + + test._row = trow.cloneNode(true); + test._row.style.display = ''; + test._row.id = ''; + test._row.onclick = function() {JSLitmus._queueTest(test);}; + test._row.title = 'Run ' + test.name + ' test'; + trow.parentNode.appendChild(test._row); + test._row.cells[0].innerHTML = test.name; + } + + var cell = test._row.cells[1]; + var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping']; + + if (test.error) { + cns.push('test_error'); + cell.innerHTML = + '
    ' + test.error + '
    ' + + '
    • ' + + jsl.join(test.error, ': ', '
    • ') + + '
    '; + } else { + if (test.running) { + cns.push('test_running'); + cell.innerHTML = 'running'; + } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) { + cns.push('test_pending'); + cell.innerHTML = 'pending'; + } else if (test.count) { + cns.push('test_done'); + var hz = test.getHz(jsl.$('test_normalize').checked); + cell.innerHTML = hz != Infinity ? hz : '∞'; + cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds'; + } else { + cell.innerHTML = 'ready'; + } + } + cell.className = cns.join(' '); + }, + + /** + * Create a new test + */ + test: function(name, f) { + // Create the Test object + var test = new Test(name, f); + JSLitmus._tests.push(test); + + // Re-render if the test state changes + test.onChange = JSLitmus.renderTest; + + // Run the next test if this one finished + test.onStop = function(test) { + if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test); + JSLitmus.currentTest = null; + JSLitmus._nextTest(); + }; + + // Render the new test + this.renderTest(test); + }, + + /** + * Add all tests to the run queue + */ + runAll: function(e) { + e = e || window.event; + var reverse = e && e.shiftKey, len = JSLitmus._tests.length; + for (var i = 0; i < len; i++) { + JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]); + } + }, + + /** + * Remove all tests from the run queue. The current test has to finish on + * it's own though + */ + stop: function() { + while (JSLitmus._queue.length) { + var test = JSLitmus._queue.shift(); + JSLitmus.renderTest(test); + } + }, + + /** + * Run the next test in the run queue + */ + _nextTest: function() { + if (!JSLitmus.currentTest) { + var test = JSLitmus._queue.shift(); + if (test) { + jsl.$('stop_button').disabled = false; + JSLitmus.currentTest = test; + test.run(); + JSLitmus.renderTest(test); + if (JSLitmus.onTestStart) JSLitmus.onTestStart(test); + } else { + jsl.$('stop_button').disabled = true; + JSLitmus.renderChart(); + } + } + }, + + /** + * Add a test to the run queue + */ + _queueTest: function(test) { + if (jsl.indexOf(JSLitmus._queue, test) >= 0) return; + JSLitmus._queue.push(test); + JSLitmus.renderTest(test); + JSLitmus._nextTest(); + }, + + /** + * Generate a Google Chart URL that shows the data for all tests + */ + chartUrl: function() { + var n = JSLitmus._tests.length, markers = [], data = []; + var d, min = 0, max = -1e10; + var normalize = jsl.$('test_normalize').checked; + + // Gather test data + for (var i=0; i < JSLitmus._tests.length; i++) { + var test = JSLitmus._tests[i]; + if (test.count) { + var hz = test.getHz(normalize); + var v = hz != Infinity ? hz : 0; + data.push(v); + markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' + + markers.length + ',10'); + max = Math.max(v, max); + } + } + if (markers.length <= 0) return null; + + // Build chart title + var title = document.getElementsByTagName('title'); + title = (title && title.length) ? title[0].innerHTML : null; + var chart_title = []; + if (title) chart_title.push(title); + chart_title.push('Ops/sec (' + platform + ')'); + + // Build labels + var labels = [jsl.toLabel(min), jsl.toLabel(max)]; + + var w = 250, bw = 15; + var bs = 5; + var h = markers.length*(bw + bs) + 30 + chart_title.length*20; + + var params = { + chtt: escape(chart_title.join('|')), + chts: '000000,10', + cht: 'bhg', // chart type + chd: 't:' + data.join(','), // data set + chds: min + ',' + max, // max/min of data + chxt: 'x', // label axes + chxl: '0:|' + labels.join('|'), // labels + chsp: '0,1', + chm: markers.join('|'), // test names + chbh: [bw, 0, bs].join(','), // bar widths + // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient + chs: w + 'x' + h + }; + return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&'); + } + }; + + JSLitmus._init(); +})(); \ No newline at end of file diff --git a/test/view.js b/test/view.js index e05b2bd6..bf7302c8 100644 --- a/test/view.js +++ b/test/view.js @@ -1,32 +1,33 @@ $(document).ready(function() { - module("Backbone View"); + module("Backbone.View"); var view = new Backbone.View({ id : 'test-view', className : 'test-view' }); - test("view: constructor", function() { + test("View: constructor", function() { equals(view.el.id, 'test-view'); equals(view.el.className, 'test-view'); equals(view.options.id, 'test-view'); equals(view.options.className, 'test-view'); }); - test("view: jQuery", function() { + test("View: jQuery", function() { view.el = document.body; - equals(view.$('#qunit-header').text(), 'Backbone Test Suite'); + equals(view.$('#qunit-header')[0].innerHTML, 'Backbone Test Suite'); + equals(view.$('#qunit-header')[1].innerHTML, 'Backbone Speed Suite'); }); - test("view: make", function() { + test("View: make", function() { var div = view.make('div', {id: 'test-div'}, "one two three"); equals(div.tagName.toLowerCase(), 'div'); equals(div.id, 'test-div'); equals($(div).text(), 'one two three'); }); - test("view: initialize", function() { + test("View: initialize", function() { var View = Backbone.View.extend({ initialize: function() { this.one = 1; @@ -36,7 +37,7 @@ $(document).ready(function() { equals(view.one, 1); }); - test("view: handleEvents", function() { + test("View: handleEvents", function() { var counter = 0; view.el = document.body; view.increment = function() { From 500d66bd58ae472dafe9759658592b9433937109 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 14 Oct 2010 15:11:56 -0400 Subject: [PATCH 7/7] a handful of model speed tests. --- backbone.js | 2 +- test/speed.js | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/backbone.js b/backbone.js index d0a1acc9..c219f516 100644 --- a/backbone.js +++ b/backbone.js @@ -135,7 +135,7 @@ // Extract attributes and options. options || (options = {}); if (!attrs) return this; - attrs = attrs.attributes || attrs; + if (attrs.attributes) attrs = attrs.attributes; var now = this.attributes; // Run validation if `validate` is defined. diff --git a/test/speed.js b/test/speed.js index 0f29d47f..6cd0b66c 100644 --- a/test/speed.js +++ b/test/speed.js @@ -22,4 +22,24 @@ object.trigger('test:trigger2', 1, 2, 3, 4, 5); }); + var model = new Backbone.Model; + + JSLitmus.test('Model: set Math.random()', function() { + model.set({number: Math.random()}); + }); + + var eventModel = new Backbone.Model; + eventModel.bind('change', fn); + + JSLitmus.test('Model: set Math.random() with a change event', function() { + eventModel.set({number: Math.random()}); + }); + + var keyModel = new Backbone.Model; + keyModel.bind('change:number', fn); + + JSLitmus.test('Model: set Math.random() with a key-value observer', function() { + keyModel.set({number: Math.random()}); + }); + })(); \ No newline at end of file