Merge "resourceloader: Restore mw.loader.store update postponing logic"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 27 Aug 2018 21:44:23 +0000 (21:44 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 27 Aug 2018 21:44:23 +0000 (21:44 +0000)
1  2 
maintenance/jsduck/categories.json
resources/src/startup/mediawiki.js

@@@ -8,7 -8,6 +8,6 @@@
                                        "mw",
                                        "mw.Message",
                                        "mw.loader",
-                                       "mw.loader.store",
                                        "mw.html",
                                        "mw.html.Cdata",
                                        "mw.html.Raw",
@@@ -83,6 -82,7 +82,6 @@@
                                "classes": [
                                        "mw.log",
                                        "mw.inspect",
 -                                      "mw.inspect.reports",
                                        "mw.Debug"
                                ]
                        }
@@@ -7,7 -7,7 +7,7 @@@
   * @alternateClassName mediaWiki
   * @singleton
   */
 -/* global $VARS */
 +/* global $VARS, $CODE */
  
  ( function () {
        'use strict';
                         * See also #work().
                         *
                         * @private
 -                       * @param {string|string[]} dependencies Module name or array of string module names
 +                       * @param {string[]} dependencies Array of module names in the registry
                         * @param {Function} [ready] Callback to execute when all dependencies are ready
                         * @param {Function} [error] Callback to execute when any dependency fails
                         */
                        function enqueue( dependencies, ready, error ) {
 -                              // Allow calling by single module name
 -                              if ( typeof dependencies === 'string' ) {
 -                                      dependencies = [ dependencies ];
 -                              }
 -
                                if ( allReady( dependencies ) ) {
                                        // Run ready immediately
                                        if ( ready !== undefined ) {
                                }
  
                                registry[ module ].state = 'executing';
 +                              $CODE.profileExecuteStart();
  
                                runScript = function () {
                                        var script, markModuleReady, nestedAddScript;
  
 +                                      $CODE.profileScriptStart();
                                        script = registry[ module ].script;
                                        markModuleReady = function () {
 +                                              $CODE.profileScriptEnd();
                                                registry[ module ].state = 'ready';
                                                handlePending( module );
                                        };
                                                // Use mw.track instead of mw.log because these errors are common in production mode
                                                // (e.g. undefined variable), and mw.log is only enabled in debug mode.
                                                registry[ module ].state = 'error';
 +                                              $CODE.profileScriptEnd();
                                                mw.trackError( 'resourceloader.exception', {
                                                        exception: e, module:
                                                        module, source: 'module-execute'
                                        }
                                }
  
 +                              // End profiling of execute()-self before we call checkCssHandles(),
 +                              // which (sometimes asynchronously) calls runScript(), which we want
 +                              // to measure separately without overlap.
 +                              $CODE.profileExecuteEnd();
 +
                                // Kick off.
                                cssHandlesRegistered = true;
                                checkCssHandles();
                         * @param {string[]} batch
                         */
                        function batchRequest( batch ) {
 -                              var reqBase, splits, maxQueryLength, b, bSource, bGroup, bSourceGroup,
 +                              var reqBase, splits, maxQueryLength, b, bSource, bGroup,
                                        source, group, i, modules, sourceLoadScript,
                                        currReqBase, currReqBaseLength, moduleMap, currReqModules, l,
                                        lastDotIndex, prefix, suffix, bytesAdded;
                                maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
  
                                // Split module list by source and by group.
 -                              splits = {};
 +                              splits = Object.create( null );
                                for ( b = 0; b < batch.length; b++ ) {
                                        bSource = registry[ batch[ b ] ].source;
                                        bGroup = registry[ batch[ b ] ].group;
 -                                      if ( !hasOwn.call( splits, bSource ) ) {
 -                                              splits[ bSource ] = {};
 +                                      if ( !splits[ bSource ] ) {
 +                                              splits[ bSource ] = Object.create( null );
                                        }
 -                                      if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
 +                                      if ( !splits[ bSource ][ bGroup ] ) {
                                                splits[ bSource ][ bGroup ] = [];
                                        }
 -                                      bSourceGroup = splits[ bSource ][ bGroup ];
 -                                      bSourceGroup.push( batch[ b ] );
 +                                      splits[ bSource ][ bGroup ].push( batch[ b ] );
                                }
  
                                for ( source in splits ) {
 -
                                        sourceLoadScript = sources[ source ];
  
                                        for ( group in splits[ source ] ) {
                                                // We may need to split up the request to honor the query string length limit,
                                                // so build it piece by piece.
                                                l = currReqBaseLength;
 -                                              moduleMap = {}; // { prefix: [ suffixes ] }
 +                                              moduleMap = Object.create( null ); // { prefix: [ suffixes ] }
                                                currReqModules = [];
  
                                                for ( i = 0; i < modules.length; i++ ) {
                                                        // If lastDotIndex is -1, substr() returns an empty string
                                                        prefix = modules[ i ].substr( 0, lastDotIndex );
                                                        suffix = modules[ i ].slice( lastDotIndex + 1 );
 -                                                      bytesAdded = hasOwn.call( moduleMap, prefix ) ?
 +                                                      bytesAdded = moduleMap[ prefix ] ?
                                                                suffix.length + 3 : // '%2C'.length == 3
                                                                modules[ i ].length + 3; // '%7C'.length == 3
  
                                                                doRequest();
                                                                // .. and start again.
                                                                l = currReqBaseLength;
 -                                                              moduleMap = {};
 +                                                              moduleMap = Object.create( null );
                                                                currReqModules = [];
  
                                                                mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
                                                        }
 -                                                      if ( !hasOwn.call( moduleMap, prefix ) ) {
 +                                                      if ( !moduleMap[ prefix ] ) {
                                                                moduleMap[ prefix ] = [];
                                                        }
                                                        l += bytesAdded;
                                 *  a list of arguments compatible with this method
                                 * @param {string|number} version Module version hash (falls backs to empty string)
                                 *  Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
 -                               * @param {string|Array} dependencies One string or array of strings of module
 -                               *  names on which this module depends.
 +                               * @param {string[]} [dependencies] Array of module names on which this module depends.
                                 * @param {string} [group=null] Group which the module is in
                                 * @param {string} [source='local'] Name of the source
                                 * @param {string} [skip=null] Script body of the skip function
                                 */
                                register: function ( module, version, dependencies, group, source, skip ) {
 -                                      var i, deps;
 +                                      var i;
                                        // Allow multiple registration
                                        if ( typeof module === 'object' ) {
                                                resolveIndexedDependencies( module );
                                        if ( hasOwn.call( registry, module ) ) {
                                                throw new Error( 'module already registered: ' + module );
                                        }
 -                                      if ( typeof dependencies === 'string' ) {
 -                                              // A single module name
 -                                              deps = [ dependencies ];
 -                                      } else if ( typeof dependencies === 'object' ) {
 -                                              // Array of module names
 -                                              deps = dependencies;
 -                                      }
                                        // List the module as registered
                                        registry[ module ] = {
                                                // Exposed to execute() for mw.loader.implement() closures.
                                                module: {
                                                        exports: {}
                                                },
 -                                              version: version !== undefined ? String( version ) : '',
 -                                              dependencies: deps || [],
 +                                              version: String( version || '' ),
 +                                              dependencies: dependencies || [],
                                                group: typeof group === 'string' ? group : null,
                                                source: typeof source === 'string' ? source : 'local',
                                                state: 'registered',
                                /**
                                 * Change the state of one or more modules.
                                 *
 -                               * @param {Object|string} modules Object of module name/state pairs
 +                               * @param {Object} modules Object of module name/state pairs
                                 */
                                state: function ( modules ) {
                                        var module, state;
                                 * modules and cache each of them separately, using each module's versioning scheme
                                 * to determine when the cache should be invalidated.
                                 *
+                                * @private
                                 * @singleton
                                 * @class mw.loader.store
                                 */
                                         * @return {string} String of concatenated vary conditions.
                                         */
                                        getVary: function () {
-                                               return [
-                                                       mw.config.get( 'skin' ),
-                                                       mw.config.get( 'wgResourceLoaderStorageVersion' ),
-                                                       mw.config.get( 'wgUserLanguage' )
-                                               ].join( ':' );
+                                               return mw.config.get( 'skin' ) + ':' +
+                                                       mw.config.get( 'wgResourceLoaderStorageVersion' ) + ':' +
+                                                       mw.config.get( 'wgUserLanguage' );
                                        },
  
                                        /**
                                                }
  
                                                try {
+                                                       // This a string we stored, or `null` if the key does not (yet) exist.
                                                        raw = localStorage.getItem( mw.loader.store.getStoreKey() );
                                                        // If we get here, localStorage is available; mark enabled
                                                        mw.loader.store.enabled = true;
+                                                       // If null, JSON.parse() will cast to string and re-parse, still null.
                                                        data = JSON.parse( raw );
                                                        if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
                                                                mw.loader.store.items = data.items;
                                                        } );
                                                }
  
+                                               // If we get here, one of four things happened:
+                                               //
+                                               // 1. localStorage did not contain our store key.
+                                               //    This means `raw` is `null`, and we're on a fresh page view (cold cache).
+                                               //    The store was enabled, and `items` starts fresh.
+                                               //
+                                               // 2. localStorage contained parseable data under our store key,
+                                               //    but it's not applicable to our current context (see getVary).
+                                               //    The store was enabled, and `items` starts fresh.
+                                               //
+                                               // 3. JSON.parse threw (localStorage contained corrupt data).
+                                               //    This means `raw` contains a string.
+                                               //    The store was enabled, and `items` starts fresh.
+                                               //
+                                               // 4. localStorage threw (disabled or otherwise unavailable).
+                                               //    This means `raw` was never assigned.
+                                               //    We will disable the store below.
                                                if ( raw === undefined ) {
                                                        // localStorage failed; disable store
                                                        mw.loader.store.enabled = false;
-                                               } else {
-                                                       mw.loader.store.update();
                                                }
                                        },
  
                                         *
                                         * @param {string} module Module name
                                         * @param {Object} descriptor The module's descriptor as set in the registry
-                                        * @return {boolean} Module was set
                                         */
                                        set: function ( module, descriptor ) {
                                                var args, key, src;
  
                                                if ( !mw.loader.store.enabled ) {
-                                                       return false;
+                                                       return;
                                                }
  
                                                key = getModuleKey( module );
                                                                descriptor.templates ].indexOf( undefined ) !== -1
                                                ) {
                                                        // Decline to store
-                                                       return false;
+                                                       return;
                                                }
  
                                                try {
                                                                exception: e,
                                                                source: 'store-localstorage-json'
                                                        } );
-                                                       return false;
+                                                       return;
                                                }
  
                                                src = 'mw.loader.implement(' + args.join( ',' ) + ');';
                                                if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
-                                                       return false;
+                                                       return;
                                                }
                                                mw.loader.store.items[ key ] = src;
                                                mw.loader.store.update();
-                                               return true;
                                        },
  
                                        /**
                                         * Iterate through the module store, removing any item that does not correspond
                                         * (in name and version) to an item in the module registry.
-                                        *
-                                        * @return {boolean} Store was pruned
                                         */
                                        prune: function () {
                                                var key, module;
  
-                                               if ( !mw.loader.store.enabled ) {
-                                                       return false;
-                                               }
                                                for ( key in mw.loader.store.items ) {
                                                        module = key.slice( 0, key.indexOf( '@' ) );
                                                        if ( getModuleKey( module ) !== key ) {
                                                                delete mw.loader.store.items[ key ];
                                                        }
                                                }
-                                               return true;
                                        },
  
                                        /**
                                                mw.loader.store.items = {};
                                                try {
                                                        localStorage.removeItem( mw.loader.store.getStoreKey() );
-                                               } catch ( ignored ) {}
+                                               } catch ( e ) {}
                                        },
  
                                        /**
                                         * Sync in-memory store back to localStorage.
                                         *
-                                        * This function debounces updates. When called with a flush already pending,
-                                        * the call is coalesced into the pending update. The call to
-                                        * localStorage.setItem will be naturally deferred until the page is quiescent.
+                                        * 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.
                                         *
                                         * 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. But the impact would
-                                        * be minor and the problem would be corrected by subsequent page views.
+                                        * 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.
                                         *
                                         * @method
                                         */
                                        update: ( function () {
-                                               var hasPendingWrite = false;
+                                               var timer, hasPendingWrites = false;
  
                                                function flushWrites() {
                                                        var data, key;
-                                                       if ( !hasPendingWrite || !mw.loader.store.enabled ) {
+                                                       if ( !mw.loader.store.enabled ) {
                                                                return;
                                                        }
  
                                                                } );
                                                        }
  
-                                                       hasPendingWrite = false;
+                                                       hasPendingWrites = false;
                                                }
  
-                                               return function () {
-                                                       if ( !hasPendingWrite ) {
-                                                               hasPendingWrite = true;
+                                               function request() {
+                                                       // If another mw.loader.store.set()/update() 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 );
                                                        }
+                                               }
+                                               return function () {
+                                                       // Cancel the previous timer (if it hasn't fired yet)
+                                                       clearTimeout( timer );
+                                                       timer = setTimeout( request, 2000 );
                                                };
                                        }() )
                                }