Skip to content

Commit 1cc6317

Browse files
committed
fix(ngAnimate): buffer repeated calls to RAF to improve performance for animations
Prior to this patch ngAnimate buffered all repeated calls to requestAnimationFrame into a task queue that is flushed once the next animation frame has passed. While this works it causes a random lag to jump out when any CPU intensive rAF requests are processed. This patch sets a configurable task range which then offsets followup rAF requests into the next available frame. Closes angular#12280
1 parent 7202bfa commit 1cc6317

File tree

3 files changed

+121
-29
lines changed

3 files changed

+121
-29
lines changed

src/ng/raf.js

+64-27
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
'use strict';
22

33
function $$RAFProvider() { //rAF
4+
5+
var RAF_QUEUE_LIMIT = 10;
6+
7+
var provider = this;
8+
provider.$$bufferLimit = 10;
9+
410
this.$get = ['$window', '$timeout', function($window, $timeout) {
511
var requestAnimationFrame = $window.requestAnimationFrame ||
612
$window.webkitRequestAnimationFrame;
@@ -26,42 +32,73 @@ function $$RAFProvider() { //rAF
2632

2733
queueFn.supported = rafSupported;
2834

29-
var cancelLastRAF;
30-
var taskCount = 0;
31-
var taskQueue = [];
32-
return queueFn;
33-
34-
function flush() {
35-
for (var i = 0; i < taskQueue.length; i++) {
36-
var task = taskQueue[i];
37-
if (task) {
38-
taskQueue[i] = null;
39-
task();
40-
}
41-
}
42-
taskCount = taskQueue.length = 0;
35+
function RAFTaskQueue(fn) {
36+
this.queue = [];
37+
this.count = 0;
38+
this.afterFlushFn = fn;
4339
}
4440

45-
function queueFn(asyncFn) {
46-
var index = taskQueue.length;
41+
RAFTaskQueue.prototype = {
42+
push: function(fn) {
43+
var self = this;
4744

48-
taskCount++;
49-
taskQueue.push(asyncFn);
45+
self.queue.push(fn);
46+
self.count++;
5047

51-
if (index === 0) {
52-
cancelLastRAF = rafFn(flush);
48+
self.rafWait(function() {
49+
self.flush();
50+
});
51+
},
52+
remove: function(index) {
53+
if (this.queue[index] !== noop) {
54+
this.queue[index] = noop;
55+
if (--this.count === 0) {
56+
this.reset();
57+
this.flush();
58+
}
59+
}
60+
},
61+
reset:function() {
62+
(this.cancelRaf || noop)();
63+
this.count = this.queue.length = 0;
64+
},
65+
rafWait: function(fn) {
66+
var self = this;
67+
if (!self.cancelRaf) {
68+
self.cancelRaf = rafFn(function() {
69+
self.cancelRaf = null;
70+
fn();
71+
});
72+
}
73+
},
74+
flush: function() {
75+
for (var i = 0; i < this.queue.length; i++) {
76+
this.queue[i]();
77+
}
78+
this.count = this.queue.length = 0;
79+
this.afterFlushFn(this);
5380
}
81+
};
82+
83+
var tasks = [];
84+
return queueFn;
5485

86+
function queueFn(fn) {
87+
var lastTask = tasks.length && tasks[tasks.length - 1];
88+
if (!lastTask || lastTask.count === provider.$$bufferLimit) {
89+
lastTask = tasks[tasks.length] = new RAFTaskQueue(function(self) {
90+
var taskIndex = tasks.indexOf(self);
91+
if (taskIndex >= 0) {
92+
tasks.splice(taskIndex, 1);
93+
}
94+
});
95+
}
96+
lastTask.push(fn);
97+
var index = lastTask.count - 1;
5598
return function cancelQueueFn() {
5699
if (index >= 0) {
57-
taskQueue[index] = null;
100+
lastTask.remove(index);
58101
index = null;
59-
60-
if (--taskCount === 0 && cancelLastRAF) {
61-
cancelLastRAF();
62-
cancelLastRAF = null;
63-
taskQueue.length = 0;
64-
}
65102
}
66103
};
67104
}

src/ngMock/angular-mocks.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1747,12 +1747,12 @@ angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
17471747

17481748
rafFn.supported = $delegate.supported;
17491749

1750-
rafFn.flush = function() {
1750+
rafFn.flush = function(max) {
17511751
if (queue.length === 0) {
17521752
throw new Error('No rAF callbacks present');
17531753
}
17541754

1755-
var length = queue.length;
1755+
var length = max || queue.length;
17561756
for (var i = 0; i < length; i++) {
17571757
queue[i]();
17581758
}

test/ng/rafSpec.js

+55
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe('$$rAF', function() {
4040
$provide.value('$window', {
4141
location: window.location,
4242
history: window.history,
43+
webkitCancelAnimationFrame: noop,
4344
webkitRequestAnimationFrame: function(fn) {
4445
rafLog.push(fn);
4546
}
@@ -71,6 +72,60 @@ describe('$$rAF', function() {
7172
expect(rafLog.length).toBe(2);
7273
}));
7374

75+
it('should buffer repeated calls to RAF into segments denoted by a limit which is placed on the provider', inject(function($$rAF) {
76+
if (!$$rAF.supported) return;
77+
78+
var BUFFER_LIMIT = 5;
79+
80+
//we need to create our own injector to work around the ngMock overrides
81+
var rafLog = [];
82+
var injector = createInjector(['ng', function($provide, $$rAFProvider) {
83+
$$rAFProvider.$$bufferLimit = BUFFER_LIMIT;
84+
$provide.value('$window', {
85+
location: window.location,
86+
history: window.history,
87+
webkitCancelAnimationFrame: noop,
88+
webkitRequestAnimationFrame: function(fn) {
89+
rafLog.push(fn);
90+
}
91+
});
92+
}]);
93+
94+
$$rAF = injector.get('$$rAF');
95+
96+
var log = [];
97+
function logFn() {
98+
log.push(log.length);
99+
}
100+
101+
for (var i = 0; i < 8; i++) {
102+
$$rAF(logFn);
103+
}
104+
105+
expect(log).toEqual([]);
106+
expect(rafLog.length).toBe(2);
107+
108+
$$rAF(logFn);
109+
expect(rafLog.length).toBe(2);
110+
111+
$$rAF(logFn);
112+
expect(rafLog.length).toBe(2);
113+
114+
$$rAF(logFn);
115+
expect(rafLog.length).toBe(3);
116+
117+
rafLog.shift()();
118+
expect(log).toEqual([0,1,2,3,4]);
119+
120+
rafLog.shift()();
121+
expect(log).toEqual([0,1,2,3,4,5,6,7,8,9]);
122+
123+
rafLog.shift()();
124+
expect(log).toEqual([0,1,2,3,4,5,6,7,8,9,10]);
125+
126+
expect(rafLog.length).toBe(0);
127+
}));
128+
74129
describe('$timeout fallback', function() {
75130
it("it should use a $timeout incase native rAF isn't suppored", function() {
76131
var timeoutSpy = jasmine.createSpy('callback');

0 commit comments

Comments
 (0)