Skip to content

Commit 6dcca5a

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 6dcca5a

File tree

3 files changed

+122
-29
lines changed

3 files changed

+122
-29
lines changed

src/ng/raf.js

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

33
function $$RAFProvider() { //rAF
4+
5+
var provider = this;
6+
provider.$$bufferLimit = 10;
7+
48
this.$get = ['$window', '$timeout', function($window, $timeout) {
59
var requestAnimationFrame = $window.requestAnimationFrame ||
610
$window.webkitRequestAnimationFrame;
@@ -26,42 +30,73 @@ function $$RAFProvider() { //rAF
2630

2731
queueFn.supported = rafSupported;
2832

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;
33+
function RAFTaskQueue(fn) {
34+
this.queue = [];
35+
this.count = 0;
36+
this.afterFlushFn = fn;
4337
}
4438

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

48-
taskCount++;
49-
taskQueue.push(asyncFn);
43+
self.queue.push(fn);
44+
self.count++;
5045

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

84+
function queueFn(fn) {
85+
var lastTask = tasks.length && tasks[tasks.length - 1];
86+
if (!lastTask || lastTask.count === provider.$$bufferLimit) {
87+
lastTask = tasks[tasks.length] = new RAFTaskQueue(function(self) {
88+
var taskIndex = tasks.indexOf(self);
89+
if (taskIndex >= 0) {
90+
tasks.splice(taskIndex, 1);
91+
}
92+
});
93+
}
94+
lastTask.push(fn);
95+
var index = lastTask.count - 1;
5596
return function cancelQueueFn() {
5697
if (index >= 0) {
57-
taskQueue[index] = null;
98+
lastTask.remove(index);
5899
index = null;
59-
60-
if (--taskCount === 0 && cancelLastRAF) {
61-
cancelLastRAF();
62-
cancelLastRAF = null;
63-
taskQueue.length = 0;
64-
}
65100
}
66101
};
67102
}

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

+58
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,64 @@ describe('$$rAF', function() {
7171
expect(rafLog.length).toBe(2);
7272
}));
7373

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

0 commit comments

Comments
 (0)