Merge "resourceloader: Avoid clear/set timer overhead in mw.loader.store.add"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 4 Sep 2018 18:59:53 +0000 (18:59 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 4 Sep 2018 18:59:53 +0000 (18:59 +0000)
1  2 
resources/src/startup/mediawiki.js

@@@ -1,7 -1,7 +1,7 @@@
  /**
   * Base library for MediaWiki.
   *
 - * Exposed globally as `mediaWiki` with `mw` as shortcut.
 + * Exposed globally as `mw`, with `mediaWiki` as alias.
   *
   * @class mw
   * @alternateClassName mediaWiki
                log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
                        obj[ key ] = val;
                } : function ( obj, key, val, msg, logName ) {
 -                      var logged = new StringSet();
 -                      logName = logName || key;
 -                      msg = 'Use of "' + logName + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
 -                      function uniqueTrace() {
 -                              var trace = new Error().stack;
 -                              if ( logged.has( trace ) ) {
 -                                      return false;
 +                      var stacks;
 +                      function maybeLog() {
 +                              var name,
 +                                      trace = new Error().stack;
 +                              if ( !stacks ) {
 +                                      stacks = new StringSet();
 +                              }
 +                              if ( !stacks.has( trace ) ) {
 +                                      stacks.add( trace );
 +                                      name = logName || key;
 +                                      mw.track( 'mw.deprecate', name );
 +                                      mw.log.warn(
 +                                              'Use of "' + name + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' )
 +                                      );
                                }
 -                              logged.add( trace );
 -                              return true;
                        }
                        // Support: Safari 5.0
                        // Throws "not supported on DOM Objects" for Node or Element objects (incl. document)
                                        configurable: true,
                                        enumerable: true,
                                        get: function () {
 -                                              if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', logName );
 -                                                      mw.log.warn( msg );
 -                                              }
 +                                              maybeLog();
                                                return val;
                                        },
                                        set: function ( newVal ) {
 -                                              if ( uniqueTrace() ) {
 -                                                      mw.track( 'mw.deprecate', logName );
 -                                                      mw.log.warn( msg );
 -                                              }
 +                                              maybeLog();
                                                val = newVal;
                                        }
                                } );
                                        // Allow multiple registration
                                        if ( typeof module === 'object' ) {
                                                resolveIndexedDependencies( module );
 +                                              // module is an array of arrays
                                                for ( i = 0; i < module.length; i++ ) {
                                                        // module is an array of module names
 -                                                      if ( typeof module[ i ] === 'string' ) {
 -                                                              mw.loader.register( module[ i ] );
 -                                                      // module is an array of arrays
 -                                                      } else if ( typeof module[ i ] === 'object' ) {
 -                                                              mw.loader.register.apply( mw.loader, module[ i ] );
 -                                                      }
 +                                                      mw.loader.register.apply( mw.loader, module[ i ] );
                                                }
                                                return;
                                        }
                                                mw.loader.register( name );
                                        }
                                        // Check for duplicate implementation
 -                                      if ( hasOwn.call( registry, name ) && registry[ name ].script !== undefined ) {
 +                                      if ( registry[ name ].script !== undefined ) {
                                                throw new Error( 'module already implemented: ' + name );
                                        }
                                        if ( version ) {
                                        /**
                                         * Request a sync of the in-memory store back to persisted localStorage.
                                         *
-                                        * This function debounces updates. When called with a flush already pending, the
-                                        * scheduled flush is postponed. The call to localStorage.setItem will be keep
-                                        * being deferred until the page is quiescent for 2 seconds.
+                                        * This function debounces updates. The debouncing logic should account
+                                        * for the following factors:
                                         *
-                                        * Because localStorage is shared by all pages from the same origin, if multiple
-                                        * pages are loaded with different module sets, the possibility exists that
-                                        * modules saved by one page will be clobbered by another. The only impact of this
-                                        * is minor (merely a less efficient cache use) and the problem would be corrected
-                                        * by subsequent page views.
+                                        * - Writing to localStorage is an expensive operation that must not happen
+                                        *   during the critical path of initialising and executing module code.
+                                        *   Instead, it should happen at a later time after modules have been given
+                                        *   time and priority to do their thing first.
+                                        *
+                                        * - This method is called from mw.loader.store.add(), which will be called
+                                        *   hundreds of times on a typical page, including within the same call-stack
+                                        *   and eventloop-tick. This is because responses from load.php happen in
+                                        *   batches. As such, we want to allow all modules from the same load.php
+                                        *   response to be written to disk with a single flush, not many.
+                                        *
+                                        * - Repeatedly deleting and creating timers is non-trivial.
+                                        *
+                                        * - localStorage is shared by all pages from the same origin, if multiple
+                                        *   pages are loaded with different module sets, the possibility exists that
+                                        *   modules saved by one page will be clobbered by another. The impact of
+                                        *   this is minor, it merely causes a less efficient cache use, and the
+                                        *   problem would be corrected by subsequent page views.
                                         *
                                         * This method is considered internal to mw.loader.store and must only
                                         * be called if the store is enabled.
                                         * @method
                                         */
                                        requestUpdate: ( function () {
-                                               var timer, hasPendingWrites = false;
+                                               var hasPendingWrites = false;
  
                                                function flushWrites() {
                                                        var data, key;
                                                                } );
                                                        }
  
+                                                       // Let the next call to requestUpdate() create a new timer.
                                                        hasPendingWrites = false;
                                                }
  
-                                               function request() {
-                                                       // If another mw.loader.store.requestUpdate() call happens in the narrow
-                                                       // time window between requestIdleCallback() and flushWrites firing, ignore it.
-                                                       // It'll be saved by the already-scheduled flush.
-                                                       if ( !hasPendingWrites ) {
-                                                               hasPendingWrites = true;
-                                                               mw.requestIdleCallback( flushWrites );
-                                                       }
+                                               function onTimeout() {
+                                                       // Defer the actual write via requestIdleCallback
+                                                       mw.requestIdleCallback( flushWrites );
                                                }
  
                                                return function () {
-                                                       // Cancel the previous timer (if it hasn't fired yet)
-                                                       clearTimeout( timer );
-                                                       timer = setTimeout( request, 2000 );
+                                                       // On the first call to requestUpdate(), create a timer that
+                                                       // waits at least two seconds, then calls onTimeout.
+                                                       // The main purpose is to allow the current batch of load.php
+                                                       // responses to complete before we do anything. This batch can
+                                                       // trigger many hundreds of calls to requestUpdate().
+                                                       if ( !hasPendingWrites ) {
+                                                               hasPendingWrites = true;
+                                                               setTimeout( onTimeout, 2000 );
+                                                       }
                                                };
                                        }() )
                                }