From 2905943e54dfc9067dbb332b054e32eef72088f2 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Wed, 21 Oct 2015 02:52:52 +0100 Subject: [PATCH] Implement mw.requestIdleCallback for deferred background tasks MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit We often use the idiom "window.onload" or "$(window).on('load')". Since code loads asynchronous, this is problematic because the event won't always be observed as it may fire before the event handler is attached. Most tasks also don't really want to wait until the page is loaded (in which case it would run immediately if the page is already loaded). Rather their intent is just to defer it to a later point in time – to avoid disrupting user events. Bug: T111456 Change-Id: Ieba0440c6d83086762c777dfbbc167f1c314a751 --- resources/Resources.php | 1 + .../mediawiki.requestIdleCallback.js | 50 ++++++++ tests/qunit/QUnitTestResources.php | 1 + .../mediawiki.requestIdleCallback.test.js | 121 ++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 resources/src/mediawiki/mediawiki.requestIdleCallback.js create mode 100644 tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js diff --git a/resources/Resources.php b/resources/Resources.php index 08c695d3fc..fa04e41262 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -826,6 +826,7 @@ return array( '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 0000000000..796639f9bf --- /dev/null +++ b/resources/src/mediawiki/mediawiki.requestIdleCallback.js @@ -0,0 +1,50 @@ +/*! + * An interface for scheduling background tasks. + * + * Loosely based on https://w3c.github.io/requestidlecallback/ + */ +( function ( mw, $ ) { + var tasks = [], + maxIdleDuration = 50, + timeout = null; + + function schedule( trigger ) { + clearTimeout( timeout ); + timeout = setTimeout( trigger, 700 ); + } + + function triggerIdle() { + var elapsed, + start = mw.now(); + + while ( tasks.length ) { + elapsed = mw.now() - start; + if ( elapsed < maxIdleDuration ) { + tasks.shift().callback(); + } else { + // Idle moment expired, try again later + schedule( triggerIdle ); + break; + } + } + } + + mw.requestIdleCallbackInternal = function ( callback ) { + var task = { callback: callback }; + tasks.push( task ); + + $( function () { schedule( triggerIdle ); } ); + }; + + /** + * Schedule a deferred task to run in the background. + * + * @member mw + * @param {Function} callback + */ + mw.requestIdleCallback = window.requestIdleCallback + ? function ( callback ) { + window.requestIdleCallback( callback ); + } + : mw.requestIdleCallbackInternal; +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 26c4e08185..1db0eebc42 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -63,6 +63,7 @@ return array( '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 0000000000..3772097df4 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js @@ -0,0 +1,121 @@ +( function ( mw ) { + QUnit.module( 'mediawiki.requestIdleCallback', QUnit.newMwEnvironment( { + setup: function () { + var time = mw.now(), + clock = this.clock = this.sandbox.useFakeTimers(); + + this.tick = function ( forward ) { + time += forward; + clock.tick( forward ); + }; + this.sandbox.stub( mw, 'now', function () { + return time; + } ); + + // 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', 3, function ( assert ) { + var sequence, + tick = this.tick; + + mw.requestIdleCallback( function () { + sequence.push( 'x' ); + tick( 30 ); + } ); + mw.requestIdleCallback( function () { + tick( 5 ); + sequence.push( 'y' ); + tick( 30 ); + } ); + // Task Z is not run in the first sequence because the + // first two tasks consumed the available 50ms budget. + mw.requestIdleCallback( function () { + sequence.push( 'z' ); + tick( 30 ); + } ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [ 'x', 'y' ] ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [ 'z' ] ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [] ); + } ); + + // Schedule new callbacks within a callback that tick + // the clock. If the budget is exceeded, the newly scheduled + // task is delayed until the next idle period. + QUnit.test( 'nest-tick', 3, function ( assert ) { + var sequence, + tick = this.tick; + + mw.requestIdleCallback( function () { + sequence.push( 'x' ); + tick( 30 ); + } ); + // Task Y is a task that schedules another task. + mw.requestIdleCallback( function () { + function other() { + sequence.push( 'y' ); + tick( 35 ); + } + mw.requestIdleCallback( other ); + } ); + mw.requestIdleCallback( function () { + sequence.push( 'z' ); + tick( 30 ); + } ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [ 'x', 'z' ] ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [ 'y' ] ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [] ); + } ); + + // Schedule new callbacks within a callback that run quickly. + // Note how the newly scheduled task gets to run as part of the + // current idle period (budget allowing). + QUnit.test( 'nest-quick', 2, function ( assert ) { + var sequence, + tick = this.tick; + + mw.requestIdleCallback( function () { + sequence.push( 'x' ); + mw.requestIdleCallback( function () { + sequence.push( 'x-expand' ); + } ); + } ); + mw.requestIdleCallback( function () { + sequence.push( 'y' ); + } ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [ 'x', 'y', 'x-expand' ] ); + + sequence = []; + tick( 1000 ); + assert.deepEqual( sequence, [] ); + } ); + +}( mediaWiki ) ); -- 2.20.1