mediawiki.requestIdleCallback: Implement timeRemaining()
authorTimo Tijhof <krinklemail@gmail.com>
Sat, 12 Mar 2016 02:19:35 +0000 (02:19 +0000)
committerOri.livneh <ori@wikimedia.org>
Wed, 23 Mar 2016 22:29:44 +0000 (22:29 +0000)
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

resources/src/mediawiki/mediawiki.requestIdleCallback.js
tests/qunit/suites/resources/mediawiki/mediawiki.requestIdleCallback.test.js

index 796639f..d0e21ae 100644 (file)
@@ -3,37 +3,19 @@
  *
  * 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;
-                       }
-               }
-       }
+( function ( mw ) {
+       var maxBusy = 50;
 
        mw.requestIdleCallbackInternal = function ( callback ) {
-               var task = { callback: callback };
-               tasks.push( task );
-
-               $( function () { schedule( triggerIdle ); } );
+               setTimeout( function () {
+                       var start = mw.now();
+                       callback( {
+                               didTimeout: false,
+                               timeRemaining: function () {
+                                       return Math.max( 0, maxBusy - ( mw.now() - start ) );
+                               }
+                       } );
+               }, 1 );
        };
 
        /**
@@ -43,8 +25,7 @@
         * @param {Function} callback
         */
        mw.requestIdleCallback = window.requestIdleCallback
-               ? function ( callback ) {
-                       window.requestIdleCallback( callback );
-               }
+               // Bind because it throws TypeError if context is not window
+               ? window.requestIdleCallback.bind( window )
                : mw.requestIdleCallbackInternal;
 }( mediaWiki, jQuery ) );
index 3772097..7a09964 100644 (file)
 ( function ( mw ) {
        QUnit.module( 'mediawiki.requestIdleCallback', QUnit.newMwEnvironment( {
                setup: function () {
-                       var time = mw.now(),
-                               clock = this.clock = this.sandbox.useFakeTimers();
+                       var clock = this.clock = this.sandbox.useFakeTimers();
 
-                       this.tick = function ( forward ) {
-                               time += forward;
-                               clock.tick( forward );
-                       };
                        this.sandbox.stub( mw, 'now', function () {
-                               return time;
+                               return +new Date();
                        } );
 
-                       // Don't test the native version (if available)
-                       this.mwRIC = mw.requestIdleCallback;
-                       mw.requestIdleCallback = mw.requestIdleCallbackInternal;
-               },
-               teardown: function () {
-                       mw.requestIdleCallback = this.mwRIC;
+                       this.tick = function ( forward ) {
+                               return clock.tick( forward || 1 );
+                       };
+
+                       // Always test the polyfill, not native
+                       this.sandbox.stub( mw, 'requestIdleCallback', mw.requestIdleCallbackInternal );
                }
        } ) );
 
-       // Basic scheduling of callbacks
-       QUnit.test( 'callback', 3, function ( assert ) {
-               var sequence,
-                       tick = this.tick;
+       QUnit.test( 'callback', function ( assert ) {
+               var sequence;
 
                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, [] );
+               this.tick();
+               assert.deepEqual( sequence, [ 'x', 'y', 'z' ] );
        } );
 
-       // 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;
+       QUnit.test( 'nested', function ( assert ) {
+               var sequence;
 
                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 );
+               this.tick();
                assert.deepEqual( sequence, [ 'x', 'z' ] );
 
                sequence = [];
-               tick( 1000 );
+               this.tick();
                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 ) {
+       QUnit.test( 'timeRemaining', 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' );
+                       tick = this.tick,
+                       jobs = [
+                               { time: 10, key: 'a' },
+                               { time: 20, key: 'b' },
+                               { time: 10, key: 'c' },
+                               { time: 20, key: 'd' },
+                               { time: 10, key: 'e' }
+                       ];
+
+               mw.requestIdleCallback( function doWork( deadline ) {
+                       var job;
+                       while ( jobs[ 0 ] && deadline.timeRemaining() > 15 ) {
+                               job = jobs.shift();
+                               tick( job.time );
+                               sequence.push( job.key );
+                       }
+                       if ( jobs[ 0 ] ) {
+                               mw.requestIdleCallback( doWork );
+                       }
                } );
 
                sequence = [];
-               tick( 1000 );
-               assert.deepEqual( sequence, [ 'x', 'y', 'x-expand' ] );
+               tick();
+               assert.deepEqual( sequence, [ 'a', 'b', 'c' ] );
 
                sequence = [];
-               tick( 1000 );
-               assert.deepEqual( sequence, [] );
+               tick();
+               assert.deepEqual( sequence, [ 'd', 'e' ] );
        } );
 
+       if ( window.requestIdleCallback ) {
+               QUnit.test( 'native', function ( assert ) {
+                       var done = assert.async();
+                       // Remove polyfill
+                       mw.requestIdleCallback.restore();
+                       mw.requestIdleCallback( function () {
+                               assert.expect( 0 );
+                               done();
+                       } );
+               } );
+       }
+
 }( mediaWiki ) );