Skip to content

Commit 55fc2a9

Browse files
Krinkleatdt
authored andcommitted
mediawiki.requestIdleCallback: Implement timeRemaining()
This matches the native API. This allows callers to better batch and spread out expensive operations based on actual execution speed. Right now CentralNotice is manually creating arbitrarily sized batches in kvStoreMaintenance. Instead this can use a while loop with timeRemaining() to run as quickly as possible whilst still being able to stop and yield when it runs for too long. This way will naturally take more iterations on slow devices and less iterations on faster ones - to be least disruptive. While timeRemaining() is already available in the native interface, it was previously unsafe to call because the fallback didn't implement it. * Remove redundant QUnit.test() expect numbers. * Add a test for the native one if available. This will catch silly mistakes like assigning the native one to mw.requestIdleCallback directly that result in 'Uncaught TypeError: Illegal invocation' due to missing call context. Change-Id: I9721fab9e89c561e31101b5556a3748431353548
1 parent 337bf08 commit 55fc2a9

File tree

2 files changed

+65
-98
lines changed

2 files changed

+65
-98
lines changed

resources/src/mediawiki/mediawiki.requestIdleCallback.js

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,19 @@
33
*
44
* Loosely based on https://w3c.github.io/requestidlecallback/
55
*/
6-
( function ( mw, $ ) {
7-
var tasks = [],
8-
maxIdleDuration = 50,
9-
timeout = null;
10-
11-
function schedule( trigger ) {
12-
clearTimeout( timeout );
13-
timeout = setTimeout( trigger, 700 );
14-
}
15-
16-
function triggerIdle() {
17-
var elapsed,
18-
start = mw.now();
19-
20-
while ( tasks.length ) {
21-
elapsed = mw.now() - start;
22-
if ( elapsed < maxIdleDuration ) {
23-
tasks.shift().callback();
24-
} else {
25-
// Idle moment expired, try again later
26-
schedule( triggerIdle );
27-
break;
28-
}
29-
}
30-
}
6+
( function ( mw ) {
7+
var maxBusy = 50;
318

329
mw.requestIdleCallbackInternal = function ( callback ) {
33-
var task = { callback: callback };
34-
tasks.push( task );
35-
36-
$( function () { schedule( triggerIdle ); } );
10+
setTimeout( function () {
11+
var start = mw.now();
12+
callback( {
13+
didTimeout: false,
14+
timeRemaining: function () {
15+
return Math.max( 0, maxBusy - ( mw.now() - start ) );
16+
}
17+
} );
18+
}, 1 );
3719
};
3820

3921
/**
@@ -43,8 +25,7 @@
4325
* @param {Function} callback
4426
*/
4527
mw.requestIdleCallback = window.requestIdleCallback
46-
? function ( callback ) {
47-
window.requestIdleCallback( callback );
48-
}
28+
// Bind because it throws TypeError if context is not window
29+
? window.requestIdleCallback.bind( window )
4930
: mw.requestIdleCallbackInternal;
5031
}( mediaWiki, jQuery ) );
Lines changed: 52 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,107 @@
11
( function ( mw ) {
22
QUnit.module( 'mediawiki.requestIdleCallback', QUnit.newMwEnvironment( {
33
setup: function () {
4-
var time = mw.now(),
5-
clock = this.clock = this.sandbox.useFakeTimers();
4+
var clock = this.clock = this.sandbox.useFakeTimers();
65

7-
this.tick = function ( forward ) {
8-
time += forward;
9-
clock.tick( forward );
10-
};
116
this.sandbox.stub( mw, 'now', function () {
12-
return time;
7+
return +new Date();
138
} );
149

15-
// Don't test the native version (if available)
16-
this.mwRIC = mw.requestIdleCallback;
17-
mw.requestIdleCallback = mw.requestIdleCallbackInternal;
18-
},
19-
teardown: function () {
20-
mw.requestIdleCallback = this.mwRIC;
10+
this.tick = function ( forward ) {
11+
return clock.tick( forward || 1 );
12+
};
13+
14+
// Always test the polyfill, not native
15+
this.sandbox.stub( mw, 'requestIdleCallback', mw.requestIdleCallbackInternal );
2116
}
2217
} ) );
2318

24-
// Basic scheduling of callbacks
25-
QUnit.test( 'callback', 3, function ( assert ) {
26-
var sequence,
27-
tick = this.tick;
19+
QUnit.test( 'callback', function ( assert ) {
20+
var sequence;
2821

2922
mw.requestIdleCallback( function () {
3023
sequence.push( 'x' );
31-
tick( 30 );
3224
} );
3325
mw.requestIdleCallback( function () {
34-
tick( 5 );
3526
sequence.push( 'y' );
36-
tick( 30 );
3727
} );
38-
// Task Z is not run in the first sequence because the
39-
// first two tasks consumed the available 50ms budget.
4028
mw.requestIdleCallback( function () {
4129
sequence.push( 'z' );
42-
tick( 30 );
4330
} );
4431

4532
sequence = [];
46-
tick( 1000 );
47-
assert.deepEqual( sequence, [ 'x', 'y' ] );
48-
49-
sequence = [];
50-
tick( 1000 );
51-
assert.deepEqual( sequence, [ 'z' ] );
52-
53-
sequence = [];
54-
tick( 1000 );
55-
assert.deepEqual( sequence, [] );
33+
this.tick();
34+
assert.deepEqual( sequence, [ 'x', 'y', 'z' ] );
5635
} );
5736

58-
// Schedule new callbacks within a callback that tick
59-
// the clock. If the budget is exceeded, the newly scheduled
60-
// task is delayed until the next idle period.
61-
QUnit.test( 'nest-tick', 3, function ( assert ) {
62-
var sequence,
63-
tick = this.tick;
37+
QUnit.test( 'nested', function ( assert ) {
38+
var sequence;
6439

6540
mw.requestIdleCallback( function () {
6641
sequence.push( 'x' );
67-
tick( 30 );
6842
} );
6943
// Task Y is a task that schedules another task.
7044
mw.requestIdleCallback( function () {
7145
function other() {
7246
sequence.push( 'y' );
73-
tick( 35 );
7447
}
7548
mw.requestIdleCallback( other );
7649
} );
7750
mw.requestIdleCallback( function () {
7851
sequence.push( 'z' );
79-
tick( 30 );
8052
} );
8153

8254
sequence = [];
83-
tick( 1000 );
55+
this.tick();
8456
assert.deepEqual( sequence, [ 'x', 'z' ] );
8557

8658
sequence = [];
87-
tick( 1000 );
59+
this.tick();
8860
assert.deepEqual( sequence, [ 'y' ] );
89-
90-
sequence = [];
91-
tick( 1000 );
92-
assert.deepEqual( sequence, [] );
9361
} );
9462

95-
// Schedule new callbacks within a callback that run quickly.
96-
// Note how the newly scheduled task gets to run as part of the
97-
// current idle period (budget allowing).
98-
QUnit.test( 'nest-quick', 2, function ( assert ) {
63+
QUnit.test( 'timeRemaining', function ( assert ) {
9964
var sequence,
100-
tick = this.tick;
101-
102-
mw.requestIdleCallback( function () {
103-
sequence.push( 'x' );
104-
mw.requestIdleCallback( function () {
105-
sequence.push( 'x-expand' );
106-
} );
107-
} );
108-
mw.requestIdleCallback( function () {
109-
sequence.push( 'y' );
65+
tick = this.tick,
66+
jobs = [
67+
{ time: 10, key: 'a' },
68+
{ time: 20, key: 'b' },
69+
{ time: 10, key: 'c' },
70+
{ time: 20, key: 'd' },
71+
{ time: 10, key: 'e' }
72+
];
73+
74+
mw.requestIdleCallback( function doWork( deadline ) {
75+
var job;
76+
while ( jobs[ 0 ] && deadline.timeRemaining() > 15 ) {
77+
job = jobs.shift();
78+
tick( job.time );
79+
sequence.push( job.key );
80+
}
81+
if ( jobs[ 0 ] ) {
82+
mw.requestIdleCallback( doWork );
83+
}
11084
} );
11185

11286
sequence = [];
113-
tick( 1000 );
114-
assert.deepEqual( sequence, [ 'x', 'y', 'x-expand' ] );
87+
tick();
88+
assert.deepEqual( sequence, [ 'a', 'b', 'c' ] );
11589

11690
sequence = [];
117-
tick( 1000 );
118-
assert.deepEqual( sequence, [] );
91+
tick();
92+
assert.deepEqual( sequence, [ 'd', 'e' ] );
11993
} );
12094

95+
if ( window.requestIdleCallback ) {
96+
QUnit.test( 'native', function ( assert ) {
97+
var done = assert.async();
98+
// Remove polyfill
99+
mw.requestIdleCallback.restore();
100+
mw.requestIdleCallback( function () {
101+
assert.expect( 0 );
102+
done();
103+
} );
104+
} );
105+
}
106+
121107
}( mediaWiki ) );

0 commit comments

Comments
 (0)