Skip to content

Commit

Permalink
Fixes #411 - switch to $evalAsync() for improved performance
Browse files Browse the repository at this point in the history
  • Loading branch information
katowulf committed Apr 21, 2015
1 parent 8cec2e9 commit b7be0b8
Show file tree
Hide file tree
Showing 5 changed files with 26 additions and 108 deletions.
13 changes: 6 additions & 7 deletions src/FirebaseArray.js
Expand Up @@ -624,14 +624,13 @@
}

var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var created = batch(function(snap, prevChild) {
var created = $firebaseUtils.batch(function(snap, prevChild) {
var rec = firebaseArray.$$added(snap, prevChild);
if( rec ) {
firebaseArray.$$process('child_added', rec, prevChild);
}
});
var updated = batch(function(snap) {
var updated = $firebaseUtils.batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var changed = firebaseArray.$$updated(snap);
Expand All @@ -640,7 +639,7 @@
}
}
});
var moved = batch(function(snap, prevChild) {
var moved = $firebaseUtils.batch(function(snap, prevChild) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$moved(snap, prevChild);
Expand All @@ -649,7 +648,7 @@
}
}
});
var removed = batch(function(snap) {
var removed = $firebaseUtils.batch(function(snap) {
var rec = firebaseArray.$getRecord($firebaseUtils.getKey(snap));
if( rec ) {
var confirmed = firebaseArray.$$removed(snap);
Expand All @@ -660,11 +659,11 @@
});

var isResolved = false;
var error = batch(function(err) {
var error = $firebaseUtils.batch(function(err) {
_initComplete(err);
firebaseArray.$$error(err);
});
var initComplete = batch(_initComplete);
var initComplete = $firebaseUtils.batch(_initComplete);

var sync = {
destroy: destroy,
Expand Down
7 changes: 3 additions & 4 deletions src/FirebaseObject.js
Expand Up @@ -433,17 +433,16 @@

var isResolved = false;
var def = $firebaseUtils.defer();
var batch = $firebaseUtils.batch();
var applyUpdate = batch(function(snap) {
var applyUpdate = $firebaseUtils.batch(function(snap) {
var changed = firebaseObject.$$updated(snap);
if( changed ) {
// notifies $watch listeners and
// updates $scope if bound to a variable
firebaseObject.$$notify();
}
});
var error = batch(firebaseObject.$$error, firebaseObject);
var initComplete = batch(_initComplete);
var error = $firebaseUtils.batch(firebaseObject.$$error, firebaseObject);
var initComplete = $firebaseUtils.batch(_initComplete);

var sync = {
isDestroyed: false,
Expand Down
11 changes: 1 addition & 10 deletions src/module.js
Expand Up @@ -5,15 +5,6 @@
// services will live.
angular.module("firebase", [])
//todo use $window
.value("Firebase", exports.Firebase)

// used in conjunction with firebaseUtils.debounce function, this is the
// amount of time we will wait for additional records before triggering
// Angular's digest scope to dirty check and re-render DOM elements. A
// larger number here significantly improves performance when working with
// big data sets that are frequently changing in the DOM, but delays the
// speed at which each record is rendered in real-time. A number less than
// 100ms will usually be optimal.
.value('firebaseBatchDelay', 50 /* milliseconds */);
.value("Firebase", exports.Firebase);

})(window);
94 changes: 13 additions & 81 deletions src/utils.js
Expand Up @@ -23,8 +23,8 @@
}
])

.factory('$firebaseUtils', ["$q", "$timeout", "firebaseBatchDelay",
function($q, $timeout, firebaseBatchDelay) {
.factory('$firebaseUtils', ["$q", "$timeout", "$rootScope",
function($q, $timeout, $rootScope) {

// ES6 style promises polyfill for angular 1.2.x
// Copied from angular 1.3.x implementation: https://github.com/angular/angular.js/blob/v1.3.5/src/ng/q.js#L539
Expand All @@ -50,88 +50,21 @@

var utils = {
/**
* Returns a function which, each time it is invoked, will pause for `wait`
* milliseconds before invoking the original `fn` instance. If another
* request is received in that time, it resets `wait` up until `maxWait` is
* reached.
* Returns a function which, each time it is invoked, will gather up the values until
* the next "tick" in the Angular compiler process. Then they are all run at the same
* time to avoid multiple cycles of the digest loop. Internally, this is done using $evalAsync()
*
* Unlike a debounce function, once wait is received, all items that have been
* queued will be invoked (not just once per execution). It is acceptable to use 0,
* which means to batch all synchronously queued items.
*
* The batch function actually returns a wrap function that should be called on each
* method that is to be batched.
*
* <pre><code>
* var total = 0;
* var batchWrapper = batch(10, 100);
* var fn1 = batchWrapper(function(x) { return total += x; });
* var fn2 = batchWrapper(function() { console.log(total); });
* fn1(10);
* fn2();
* fn1(10);
* fn2();
* console.log(total); // 0 (nothing invoked yet)
* // after 10ms will log "10" and then "20"
* </code></pre>
*
* @param {int} wait number of milliseconds to pause before sending out after each invocation
* @param {int} maxWait max milliseconds to wait before sending out, defaults to wait * 10 or 100
* @param {Function} action
* @param {Object} [context]
* @returns {Function}
*/
batch: function(wait, maxWait) {
wait = typeof('wait') === 'number'? wait : firebaseBatchDelay;
if( !maxWait ) { maxWait = wait*10 || 100; }
var queue = [];
var start;
var cancelTimer;
var runScheduledForNextTick;

// returns `fn` wrapped in a function that queues up each call event to be
// invoked later inside fo runNow()
function createBatchFn(fn, context) {
if( typeof(fn) !== 'function' ) {
throw new Error('Must provide a function to be batched. Got '+fn);
}
return function() {
var args = Array.prototype.slice.call(arguments, 0);
queue.push([fn, context, args]);
resetTimer();
};
}

// clears the current wait timer and creates a new one
// however, if maxWait is exceeded, calls runNow() on the next tick.
function resetTimer() {
if( cancelTimer ) {
cancelTimer();
cancelTimer = null;
}
if( start && Date.now() - start > maxWait ) {
if(!runScheduledForNextTick){
runScheduledForNextTick = true;
utils.compile(runNow);
}
}
else {
if( !start ) { start = Date.now(); }
cancelTimer = utils.wait(runNow, wait);
}
}

// Clears the queue and invokes all of the functions awaiting notification
function runNow() {
cancelTimer = null;
start = null;
runScheduledForNextTick = false;
var copyList = queue.slice(0);
queue = [];
angular.forEach(copyList, function(parts) {
parts[0].apply(parts[1], parts[2]);
batch: function(action, context) {
return function() {
var args = Array.prototype.slice.call(arguments, 0);
$rootScope.$evalAsync(function() {
action.apply(context, args);
});
}

return createBatchFn;
};
},

/**
Expand Down Expand Up @@ -492,7 +425,6 @@
*/
VERSION: '0.0.0',

batchDelay: firebaseBatchDelay,
allPromises: $q.all.bind($q)
};

Expand Down
9 changes: 3 additions & 6 deletions tests/unit/utils.spec.js
Expand Up @@ -46,17 +46,15 @@ describe('$firebaseUtils', function () {

it('should trigger function with arguments', function() {
var spy = jasmine.createSpy();
var batch = $utils.batch();
var b = batch(spy);
var b = $utils.batch(spy);
b('foo', 'bar');
$timeout.flush();
expect(spy).toHaveBeenCalledWith('foo', 'bar');
});

it('should queue up requests until timeout', function() {
var spy = jasmine.createSpy();
var batch = $utils.batch();
var b = batch(spy);
var b = $utils.batch(spy);
for(var i=0; i < 4; i++) {
b(i);
}
Expand All @@ -70,8 +68,7 @@ describe('$firebaseUtils', function () {
var spy = jasmine.createSpy().and.callFake(function() {
b = this;
});
var batch = $utils.batch();
batch(spy, a)();
$utils.batch(spy, a)();
$timeout.flush();
expect(spy).toHaveBeenCalled();
expect(b).toBe(a);
Expand Down

0 comments on commit b7be0b8

Please sign in to comment.