Skip to content

Commit d7f27b7

Browse files
authored
perf: setTimeout and requestAnimationFrame memory leak (videojs#5294)
Our setTimeout and requestAnimationFrame methods added dispose handlers so that they get cancelled if the component is disposed before they get a chance to run. However, we were only clearing out these dispose handlers if we cleared the timeout or the rAF manually. Instead make sure that we remove the dispose handler when it is no longer needed. Fixes videojs#5199.
1 parent 444b271 commit d7f27b7

File tree

2 files changed

+69
-4
lines changed

2 files changed

+69
-4
lines changed

src/js/component.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,10 +1245,18 @@ class Component {
12451245
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
12461246
*/
12471247
setTimeout(fn, timeout) {
1248+
// declare as variables so they are properly available in timeout function
1249+
// eslint-disable-next-line
1250+
var timeoutId, disposeFn;
1251+
12481252
fn = Fn.bind(this, fn);
12491253

1250-
const timeoutId = window.setTimeout(fn, timeout);
1251-
const disposeFn = () => this.clearTimeout(timeoutId);
1254+
timeoutId = window.setTimeout(() => {
1255+
this.off('dispose', disposeFn);
1256+
fn();
1257+
}, timeout);
1258+
1259+
disposeFn = () => this.clearTimeout(timeoutId);
12521260

12531261
disposeFn.guid = `vjs-timeout-${timeoutId}`;
12541262

@@ -1371,11 +1379,19 @@ class Component {
13711379
* @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
13721380
*/
13731381
requestAnimationFrame(fn) {
1382+
// declare as variables so they are properly available in rAF function
1383+
// eslint-disable-next-line
1384+
var id, disposeFn;
1385+
13741386
if (this.supportsRaf_) {
13751387
fn = Fn.bind(this, fn);
13761388

1377-
const id = window.requestAnimationFrame(fn);
1378-
const disposeFn = () => this.cancelAnimationFrame(id);
1389+
id = window.requestAnimationFrame(() => {
1390+
this.off('dispose', disposeFn);
1391+
fn();
1392+
});
1393+
1394+
disposeFn = () => this.cancelAnimationFrame(id);
13791395

13801396
disposeFn.guid = `vjs-raf-${id}`;
13811397
this.on('dispose', disposeFn);

test/unit/component.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,55 @@ QUnit.test('*AnimationFrame methods fall back to timers if rAF not supported', f
957957
window.cancelAnimationFrame = oldCAF;
958958
});
959959

960+
QUnit.test('setTimeout should remove dispose handler on trigger', function(assert) {
961+
const comp = new Component(getFakePlayer());
962+
const el = comp.el();
963+
const data = DomData.getData(el);
964+
965+
comp.setTimeout(() => {}, 1);
966+
967+
assert.equal(data.handlers.dispose.length, 2, 'we got a new dispose handler');
968+
assert.ok(/vjs-timeout-\d/.test(data.handlers.dispose[1].guid), 'we got a new dispose handler');
969+
970+
this.clock.tick(1);
971+
972+
assert.equal(data.handlers.dispose.length, 1, 'we removed our dispose handle');
973+
974+
comp.dispose();
975+
});
976+
977+
QUnit.test('requestAnimationFrame should remove dispose handler on trigger', function(assert) {
978+
const comp = new Component(getFakePlayer());
979+
const el = comp.el();
980+
const data = DomData.getData(el);
981+
const oldRAF = window.requestAnimationFrame;
982+
const oldCAF = window.cancelAnimationFrame;
983+
984+
// Stub the window.*AnimationFrame methods with window.setTimeout methods
985+
// so we can control when the callbacks are called via sinon's timer stubs.
986+
window.requestAnimationFrame = (fn) => window.setTimeout(fn, 1);
987+
window.cancelAnimationFrame = (id) => window.clearTimeout(id);
988+
989+
// Make sure the component thinks it supports rAF.
990+
comp.supportsRaf_ = true;
991+
992+
const spyRAF = sinon.spy();
993+
994+
comp.requestAnimationFrame(spyRAF);
995+
996+
assert.equal(data.handlers.dispose.length, 2, 'we got a new dispose handler');
997+
assert.ok(/vjs-raf-\d/.test(data.handlers.dispose[1].guid), 'we got a new dispose handler');
998+
999+
this.clock.tick(1);
1000+
1001+
assert.equal(data.handlers.dispose.length, 1, 'we removed our dispose handle');
1002+
1003+
comp.dispose();
1004+
1005+
window.requestAnimationFrame = oldRAF;
1006+
window.cancelAnimationFrame = oldCAF;
1007+
});
1008+
9601009
QUnit.test('$ and $$ functions', function(assert) {
9611010
const comp = new Component(getFakePlayer());
9621011
const contentEl = document.createElement('div');

0 commit comments

Comments
 (0)