Krinkle has uploaded a new change for review.
https://gerrit.wikimedia.org/r/247775
Change subject: [WIP] Implement mw.requestIdleCallback for deferred background
tasks
......................................................................
[WIP] Implement mw.requestIdleCallback for deferred background tasks
Main rationale: Many important code bits make use of the idom
"window.onload" or "$(window).on('load')". Since code loads
asynchronous now, this is problematic since the event may no
longer be observed (as it can be fired before the event handler
is attached).
There are also tasks that don't really want to wait until the
page is loaded (in which case it would run immediately if the task
is scheduled when the page is already loaded), but rather they want
to defer it to a later point in time to avoid disrupting user events.
TODO:
* Find out whether the W3C requestIdleCallback is allowed to run
before document-ready (DOMInteractive) or window-onload (DOMComplete).
I'm hoping it isn't allowed to run that early, but if it is,
we should match that and wrap the native API.
Bug: T111456
Change-Id: Ieba0440c6d83086762c777dfbbc167f1c314a751
---
M resources/Resources.php
A resources/src/mediawiki/mediawiki.requestIdleCallback.js
M tests/qunit/QUnitTestResources.php
A tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js
4 files changed, 269 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core
refs/changes/75/247775/1
diff --git a/resources/Resources.php b/resources/Resources.php
index fee1e7c..4a73e44 100644
--- a/resources/Resources.php
+++ b/resources/Resources.php
@@ -826,6 +826,7 @@
'scripts' => array(
'resources/lib/phpjs-sha1/sha1.js',
'resources/src/mediawiki/mediawiki.js',
+
'resources/src/mediawiki/mediawiki.requestIdleCallback.js',
'resources/src/mediawiki/mediawiki.errorLogger.js',
),
'debugScripts' => 'resources/src/mediawiki/mediawiki.log.js',
diff --git a/resources/src/mediawiki/mediawiki.requestIdleCallback.js
b/resources/src/mediawiki/mediawiki.requestIdleCallback.js
new file mode 100644
index 0000000..3db9703
--- /dev/null
+++ b/resources/src/mediawiki/mediawiki.requestIdleCallback.js
@@ -0,0 +1,74 @@
+/*!
+ * An interface for scheduling background.
+ *
+ * Loosely based on https://w3c.github.io/requestidlecallback/
+ */
+( function ( mw, $ ) {
+ var tasks = [],
+ // How long (in milliseconds) a single flush should take at
most.
+ maxIterationDuration = 50,
+ timeout = null;
+
+ function runTask( task ) {
+ var index = tasks.indexOf( task );
+ if ( index === -1 ) {
+ // Already run by regular schedule
+ return;
+ }
+ tasks.splice( index, 1 );
+ task.callback( {
+ didTimeout: true,
+ timeRemaining: function () {
+ return 0;
+ }
+ } );
+ }
+
+ function schedule( runner ) {
+ clearTimeout( timeout );
+ timeout = setTimeout( runner, 700 );
+ }
+
+ function run() {
+ var elapsed,
+ start = +new Date(),
+ timeRemaining = function () {
+ var elapsedNow = +new Date() - start;
+ return Math.max( 0, maxIterationDuration -
elapsedNow );
+ };
+
+ while ( tasks.length ) {
+ elapsed = +new Date() - start;
+ if ( elapsed < maxIterationDuration ) {
+ tasks.shift().callback( {
+ timeRemaining: timeRemaining,
+ didTimeout: false
+ } );
+ } else {
+ schedule( run );
+ break;
+ }
+ }
+ }
+ mw.requestIdleCallbackInternal = function ( callback, options ) {
+ var task = { callback: callback };
+ tasks.push( task );
+ $( function () { schedule( run ); } );
+ if ( options && options.timeout ) {
+ setTimeout( function () {
+ runTask( task );
+ }, options.timeout );
+ }
+ };
+
+ /**
+ * Schedule a deferred task to run in the background.
+ *
+ * @param {Function} callback
+ * @param {Object} [options]
+ * @param {number} options.timeout
+ */
+ mw.requestIdleCallback = window.requestIdleCallback &&
window.requestIdleCallback.bind
+ ? window.requestIdleCallback.bind( window )
+ : mw.requestIdleCallbackInternal;
+}( mediaWiki, jQuery ) );
diff --git a/tests/qunit/QUnitTestResources.php
b/tests/qunit/QUnitTestResources.php
index f9ddcf2..163da3d 100644
--- a/tests/qunit/QUnitTestResources.php
+++ b/tests/qunit/QUnitTestResources.php
@@ -63,6 +63,7 @@
'tests/qunit/suites/resources/jquery/jquery.tablesorter.parsers.test.js',
'tests/qunit/suites/resources/jquery/jquery.textSelection.test.js',
'tests/qunit/data/mediawiki.jqueryMsg.data.js',
+
'tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.errorLogger.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
diff --git
a/tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js
b/tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js
new file mode 100644
index 0000000..cc8c39a
--- /dev/null
+++
b/tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js
@@ -0,0 +1,193 @@
+( function ( mw ) {
+ QUnit.module( 'mediawiki.requestIdleCallback', QUnit.newMwEnvironment( {
+ setup: function () {
+ this.clock = this.sandbox.useFakeTimers();
+
+ // Don't test the native version (if available)
+ this.mwRIC = mw.requestIdleCallback;
+ mw.requestIdleCallback = mw.requestIdleCallbackInternal;
+ },
+ teardown: function () {
+ mw.requestIdleCallback = this.mwRIC;
+ }
+ } ) );
+
+ // Basic scheduling of callbacks
+ QUnit.test( 'callback', 5, function ( assert ) {
+ var sequence, context,
+ clock = this.clock;
+
+ mw.requestIdleCallback( function ( deadline ) {
+ sequence.push( 'x' );
+ context.x = {
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 30 );
+ } );
+ mw.requestIdleCallback( function ( deadline ) {
+ clock.tick( 5 );
+ sequence.push( 'y' );
+ context.y = {
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 30 );
+ } );
+ // Task Z is not run in the first sequence because the
+ // first two tasks consumed the available 50ms budget.
+ mw.requestIdleCallback( function ( deadline ) {
+ sequence.push( 'z' );
+ context.z = {
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 30 );
+ } );
+
+ sequence = [];
+ context = {};
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'x', 'y' ] );
+ assert.deepEqual( context, {
+ x: { left: 50 },
+ y: { left: 15 }
+ } );
+
+ sequence = [];
+ context = {};
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'z' ] );
+ assert.deepEqual( context, {
+ z: { left: 50 }
+ } );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [] );
+ } );
+
+ // One of the callbacks has a timeout restriction
+ QUnit.test( 'timeout', 5, function ( assert ) {
+ var sequence, context,
+ clock = this.clock;
+
+ mw.requestIdleCallback( function ( deadline ) {
+ sequence.push( 'x' );
+ context.x = {
+ didTimeout: deadline.didTimeout,
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 60 );
+ } );
+ mw.requestIdleCallback( function ( deadline ) {
+ sequence.push( 'y' );
+ context.y = {
+ didTimeout: deadline.didTimeout,
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 60 );
+ } );
+ // Task Z runs after 2s even though the budge is reached
+ // because it has a timeout restriction.
+ mw.requestIdleCallback( function ( deadline ) {
+ sequence.push( 'z' );
+ context.z = {
+ didTimeout: deadline.didTimeout,
+ left: deadline.timeRemaining()
+ };
+ clock.tick( 60 );
+ }, { timeout: 1500 } );
+
+ sequence = [];
+ context = {};
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'x' ] );
+ assert.deepEqual( context, {
+ x: {
+ didTimeout: false,
+ left: 50
+ }
+ } );
+
+ sequence = [];
+ context = {};
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'y', 'z' ] );
+ assert.deepEqual( context, {
+ y: {
+ didTimeout: false,
+ left: 50
+ },
+ z: {
+ didTimeout: true,
+ left: 0
+ }
+ } );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [] );
+ } );
+
+ // Reschedule the same callback within a callback
+ QUnit.test( 'nest-reschedule', 3, function ( assert ) {
+ var sequence,
+ clock = this.clock;
+
+ mw.requestIdleCallback( function () {
+ sequence.push( 'x' );
+ clock.tick( 30 );
+ } );
+ // Task Y is a friendly task that checks timeRemaining before
+ // doing its work. It reschedules itself because the remaining
+ // time is too small.
+ mw.requestIdleCallback( function myWork( deadline ) {
+ // Pretend we need 35ms of working space
+ if ( deadline.timeRemaining() < 35 ) {
+ mw.requestIdleCallback( myWork );
+ return;
+ }
+ sequence.push( 'y' );
+ clock.tick( 35 );
+ } );
+ mw.requestIdleCallback( function () {
+ sequence.push( 'z' );
+ clock.tick( 30 );
+ } );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'x', 'z' ] );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'y' ] );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [] );
+ } );
+
+ // Schedule new callbacks within a callback
+ QUnit.test( 'nest-expand', 2, function ( assert ) {
+ var sequence,
+ clock = this.clock;
+
+ mw.requestIdleCallback( function () {
+ sequence.push( 'x' );
+ mw.requestIdleCallback( function () {
+ sequence.push( 'x-expand' );
+ } );
+ } );
+ mw.requestIdleCallback( function () {
+ sequence.push( 'y' );
+ } );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [ 'x', 'y', 'x-expand' ] );
+
+ sequence = [];
+ clock.tick( 1000 );
+ assert.deepEqual( sequence, [] );
+ } );
+
+}( mediaWiki ) );
--
To view, visit https://gerrit.wikimedia.org/r/247775
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ieba0440c6d83086762c777dfbbc167f1c314a751
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits