X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=resources%2Fsrc%2Fstartup%2Fmediawiki.js;h=ba8869bd28fb128c043ce2ba58d817ab1f37c095;hb=01cdb1762c7207bd261ad03726a88cb9afc97bfb;hp=9e40db9374cc4ddc483454efcec3b0ad0a5ba5bb;hpb=d464e307242465e71c4eff30c34c7bbcc2a69559;p=lhc%2Fweb%2Fwiklou.git diff --git a/resources/src/startup/mediawiki.js b/resources/src/startup/mediawiki.js index 9e40db9374..ba8869bd28 100644 --- a/resources/src/startup/mediawiki.js +++ b/resources/src/startup/mediawiki.js @@ -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 @@ -326,16 +326,21 @@ 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) @@ -345,17 +350,11 @@ 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; } } ); @@ -382,19 +381,24 @@ /** * Get the current time, measured in milliseconds since January 1, 1970 (UTC). * - * On browsers that implement the Navigation Timing API, this function will produce floating-point - * values with microsecond precision that are guaranteed to be monotonic. On all other browsers, - * it will fall back to using `Date`. + * On browsers that implement the Navigation Timing API, this function will produce + * floating-point values with microsecond precision that are guaranteed to be monotonic. + * On all other browsers, it will fall back to using `Date`. * * @return {number} Current time */ - now: ( function () { + now: function () { + // Optimisation: Define the shortcut on first call, not at module definition. var perf = window.performance, navStart = perf && perf.timing && perf.timing.navigationStart; - return navStart && typeof perf.now === 'function' ? + + // Define the relevant shortcut + mw.now = navStart && typeof perf.now === 'function' ? function () { return navStart + perf.now(); } : - function () { return Date.now(); }; - }() ), + Date.now; + + return mw.now(); + }, /** * List of all analytic events emitted so far. @@ -511,8 +515,8 @@ * - resolve: failed to sort dependencies for a module in mw.loader.load * - store-eval: could not evaluate module code cached in localStorage * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init - * - store-localstorage-json: JSON conversion error in mw.loader.store.set - * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update + * - store-localstorage-json: JSON conversion error in mw.loader.store + * - store-localstorage-update: localStorage conversion error in mw.loader.store. */ /** @@ -845,8 +849,8 @@ // The current module became 'ready'. if ( registry[ module ].state === 'ready' ) { - // Save it to the module store. - mw.loader.store.set( module, registry[ module ] ); + // Queue it for later syncing to the module store. + mw.loader.store.add( module ); // Recursively execute all dependent modules that were already loaded // (waiting for execution) and no longer have unsatisfied dependencies. for ( m in registry ) { @@ -1141,8 +1145,8 @@ * @param {string} module Module name to execute */ function execute( module ) { - var key, value, media, i, urls, cssHandle, checkCssHandles, runScript, - cssHandlesRegistered = false; + var key, value, media, i, urls, cssHandle, siteDeps, siteDepErr, runScript, + cssPending = 0; if ( !hasOwn.call( registry, module ) ) { throw new Error( 'Module has not been registered yet: ' + module ); @@ -1232,46 +1236,34 @@ mw.templates.set( module, registry[ module ].templates ); } - // Make sure we don't run the scripts until all stylesheet insertions have completed. - ( function () { - var pending = 0; - checkCssHandles = function () { - var ex, dependencies; - // cssHandlesRegistered ensures we don't take off too soon, e.g. when - // one of the cssHandles is fired while we're still creating more handles. - if ( cssHandlesRegistered && pending === 0 && runScript ) { - if ( module === 'user' ) { - // Implicit dependency on the site module. Not real dependency because - // it should run after 'site' regardless of whether it succeeds or fails. - // Note: This is a simplified version of mw.loader.using(), inlined here - // as using() depends on jQuery (T192623). - try { - dependencies = resolve( [ 'site' ] ); - } catch ( e ) { - ex = e; - runScript(); - } - if ( ex === undefined ) { - enqueue( dependencies, runScript, runScript ); - } - } else { - runScript(); - } - runScript = undefined; // Revoke + // Adding of stylesheets is asynchronous via addEmbeddedCSS(). + // The below function uses a counting semaphore to make sure we don't call + // runScript() until after this module's stylesheets have been inserted + // into the DOM. + cssHandle = function () { + // Increase semaphore, when creating a callback for addEmbeddedCSS. + cssPending++; + return function () { + var runScriptCopy; + // Decrease semaphore, when said callback is invoked. + cssPending--; + if ( cssPending === 0 ) { + // Paranoia: + // This callback is exposed to addEmbeddedCSS, which is outside the execute() + // function and is not concerned with state-machine integrity. In turn, + // addEmbeddedCSS() actually exposes stuff further into the browser (rAF). + // If increment and decrement callbacks happen in the wrong order, or start + // again afterwards, then this branch could be reached multiple times. + // To protect the integrity of the state-machine, prevent that from happening + // by making runScript() cannot be called more than once. We store a private + // reference when we first reach this branch, then deference the original, and + // call our reference to it. + runScriptCopy = runScript; + runScript = undefined; + runScriptCopy(); } }; - cssHandle = function () { - var check = checkCssHandles; - pending++; - return function () { - if ( check ) { - pending--; - check(); - check = undefined; // Revoke - } - }; - }; - }() ); + }; // Process styles (see also mw.loader.implement) // * back-compat: { : css } @@ -1325,14 +1317,29 @@ } } - // End profiling of execute()-self before we call checkCssHandles(), - // which (sometimes asynchronously) calls runScript(), which we want - // to measure separately without overlap. + // End profiling of execute()-self before we call runScript(), + // which we want to measure separately without overlap. $CODE.profileExecuteEnd(); - // Kick off. - cssHandlesRegistered = true; - checkCssHandles(); + if ( module === 'user' ) { + // Implicit dependency on the site module. Not a real dependency because it should + // run after 'site' regardless of whether it succeeds or fails. + // Note: This is a simplified version of mw.loader.using(), inlined here because + // mw.loader.using() is part of mediawiki.base (depends on jQuery; T192623). + try { + siteDeps = resolve( [ 'site' ] ); + } catch ( e ) { + siteDepErr = e; + runScript(); + } + if ( siteDepErr === undefined ) { + enqueue( siteDeps, runScript, runScript ); + } + } else if ( cssPending === 0 ) { + // Regular module without styles + runScript(); + } + // else: runScript will get called via cssHandle() } function sortQuery( o ) { @@ -1390,7 +1397,7 @@ /** * Resolve indexed dependencies. * - * ResourceLoader uses an optimization to save space which replaces module names in + * ResourceLoader uses an optimisation to save space which replaces module names in * dependency lists with the index of that module within the array of module * registration data if it exists. The benefit is a significant reduction in the data * size of the startup module. This function changes those dependency lists back to @@ -1608,6 +1615,34 @@ }; } + /** + * @private + * @param {string} module + * @param {string|number} [version] + * @param {string[]} [dependencies] + * @param {string} [group] + * @param {string} [source] + * @param {string} [skip] + */ + function registerOne( module, version, dependencies, group, source, skip ) { + if ( hasOwn.call( registry, module ) ) { + throw new Error( 'module already registered: ' + module ); + } + registry[ module ] = { + // Exposed to execute() for mw.loader.implement() closures. + // Import happens via require(). + module: { + exports: {} + }, + version: String( version || '' ), + dependencies: dependencies || [], + group: typeof group === 'string' ? group : null, + source: typeof source === 'string' ? source : 'local', + state: 'registered', + skip: typeof skip === 'string' ? skip : null + }; + } + /* Public Members */ return { /** @@ -1633,7 +1668,7 @@ /** * Start loading of all queued module dependencies. * - * @protected + * @private */ work: function () { var q, batch, implementations, sourceModules; @@ -1711,80 +1746,58 @@ * * The #work() method will use this information to split up requests by source. * - * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' ); + * mw.loader.addSource( { mediawikiwiki: 'https://www.mediawiki.org/w/load.php' } ); * - * @param {string|Object} id Source ID, or object mapping ids to load urls - * @param {string} loadUrl Url to a load.php end point + * @private + * @param {Object} ids An object mapping ids to load.php end point urls * @throws {Error} If source id is already registered */ - addSource: function ( id, loadUrl ) { - var source; - // Allow multiple additions - if ( typeof id === 'object' ) { - for ( source in id ) { - mw.loader.addSource( source, id[ source ] ); + addSource: function ( ids ) { + var id; + for ( id in ids ) { + if ( hasOwn.call( sources, id ) ) { + throw new Error( 'source already registered: ' + id ); } - return; + sources[ id ] = ids[ id ]; } - - if ( hasOwn.call( sources, id ) ) { - throw new Error( 'source already registered: ' + id ); - } - - sources[ id ] = loadUrl; }, /** * Register a module, letting the system know about it and its properties. * - * The startup modules contain calls to this method. + * The startup module calls this method. * * When using multiple module registration by passing an array, dependencies that * are specified as references to modules within the array will be resolved before * the modules are registered. * - * @param {string|Array} module Module name or array of arrays, each containing + * @param {string|Array} modules Module name or array of arrays, each containing * a list of arguments compatible with this method - * @param {string|number} version Module version hash (falls backs to empty string) + * @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[]} [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 ) { + register: function ( modules ) { var i; - // Allow multiple registration - if ( typeof module === 'object' ) { - resolveIndexedDependencies( module ); - 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 ] ); - } + if ( typeof modules === 'object' ) { + resolveIndexedDependencies( modules ); + // Optimisation: Up to 55% faster. + // Typically called only once, and with a batch. + // See + // Benchmarks taught us that the code for adding an object to `registry` + // should actually be inline, or in a simple function that does no + // arguments manipulation, and isn't also the caller itself. + // JS semantics make it hard to optimise recursion to a different + // signature of itself. + for ( i = 0; i < modules.length; i++ ) { + registerOne.apply( null, modules[ i ] ); } - return; - } - if ( hasOwn.call( registry, module ) ) { - throw new Error( 'module already registered: ' + module ); + } else { + registerOne.apply( null, arguments ); } - // List the module as registered - registry[ module ] = { - // Exposed to execute() for mw.loader.implement() closures. - // Import happens via require(). - module: { - exports: {} - }, - version: String( version || '' ), - dependencies: dependencies || [], - group: typeof group === 'string' ? group : null, - source: typeof source === 'string' ? source : 'local', - state: 'registered', - skip: typeof skip === 'string' ? skip : null - }; }, /** @@ -1812,7 +1825,7 @@ * The reason css strings are not concatenated anymore is T33676. We now check * whether it's safe to extend the stylesheet. * - * @protected + * @private * @param {Object} [messages] List of key/value pairs to be added to mw#messages. * @param {Object} [templates] List of key/value pairs to be added to mw#templates. */ @@ -1825,7 +1838,7 @@ 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 ) { @@ -2006,6 +2019,10 @@ // to module implementations. items: {}, + // Names of modules to be stored during the next update. + // See add() and update(). + queue: [], + // Cache hit stats stats: { hits: 0, misses: 0, expired: 0, failed: 0 }, @@ -2053,7 +2070,7 @@ init: function () { var raw, data; - if ( mw.loader.store.enabled !== null ) { + if ( this.enabled !== null ) { // Init already ran return; } @@ -2062,31 +2079,31 @@ // Disabled because localStorage quotas are tight and (in Firefox's case) // shared by multiple origins. // See T66721, and . - /Firefox|Opera/.test( navigator.userAgent ) || + /Firefox/.test( navigator.userAgent ) || // Disabled by configuration. !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) { // Clear any previous store to free up space. (T66721) - mw.loader.store.clear(); - mw.loader.store.enabled = false; + this.clear(); + this.enabled = false; return; } if ( mw.config.get( 'debug' ) ) { // Disable module store in debug mode - mw.loader.store.enabled = false; + this.enabled = false; return; } try { // This a string we stored, or `null` if the key does not (yet) exist. - raw = localStorage.getItem( mw.loader.store.getStoreKey() ); + raw = localStorage.getItem( this.getStoreKey() ); // If we get here, localStorage is available; mark enabled - mw.loader.store.enabled = true; + this.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 ( data && typeof data.items === 'object' && data.vary === this.getVary() ) { + this.items = data.items; return; } } catch ( e ) { @@ -2115,7 +2132,7 @@ // We will disable the store below. if ( raw === undefined ) { // localStorage failed; disable store - mw.loader.store.enabled = false; + this.enabled = false; } }, @@ -2128,38 +2145,56 @@ get: function ( module ) { var key; - if ( !mw.loader.store.enabled ) { + if ( !this.enabled ) { return false; } key = getModuleKey( module ); - if ( key in mw.loader.store.items ) { - mw.loader.store.stats.hits++; - return mw.loader.store.items[ key ]; + if ( key in this.items ) { + this.stats.hits++; + return this.items[ key ]; } - mw.loader.store.stats.misses++; + this.stats.misses++; return false; }, /** - * Stringify a module and queue it for storage. + * Queue the name of a module that the next update should consider storing. * + * @since 1.32 * @param {string} module Module name - * @param {Object} descriptor The module's descriptor as set in the registry */ - set: function ( module, descriptor ) { - var args, key, src; - - if ( !mw.loader.store.enabled ) { + add: function ( module ) { + if ( !this.enabled ) { return; } + this.queue.push( module ); + this.requestUpdate(); + }, + + /** + * Add the contents of the named module to the in-memory store. + * + * This method does not guarantee that the module will be stored. + * Inspection of the module's meta data and size will ultimately decide that. + * + * This method is considered internal to mw.loader.store and must only + * be called if the store is enabled. + * + * @private + * @param {string} module Module name + */ + set: function ( module ) { + var key, args, src, + descriptor = mw.loader.moduleRegistry[ module ]; key = getModuleKey( module ); if ( // Already stored a copy of this exact version - key in mw.loader.store.items || + key in this.items || // Module failed to load + !descriptor || descriptor.state !== 'ready' || // Unversioned, private, or site-/user-specific !descriptor.version || @@ -2193,11 +2228,10 @@ } src = 'mw.loader.implement(' + args.join( ',' ) + ');'; - if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) { + if ( src.length > this.MODULE_SIZE_MAX ) { return; } - mw.loader.store.items[ key ] = src; - mw.loader.store.update(); + this.items[ key ] = src; }, /** @@ -2207,14 +2241,14 @@ prune: function () { var key, module; - for ( key in mw.loader.store.items ) { + for ( key in this.items ) { module = key.slice( 0, key.indexOf( '@' ) ); if ( getModuleKey( module ) !== key ) { - mw.loader.store.stats.expired++; - delete mw.loader.store.items[ key ]; - } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) { + this.stats.expired++; + delete this.items[ key ]; + } else if ( this.items[ key ].length > this.MODULE_SIZE_MAX ) { // This value predates the enforcement of a size limit on cached modules. - delete mw.loader.store.items[ key ]; + delete this.items[ key ]; } } }, @@ -2223,37 +2257,57 @@ * Clear the entire module store right now. */ clear: function () { - mw.loader.store.items = {}; + this.items = {}; try { - localStorage.removeItem( mw.loader.store.getStoreKey() ); + localStorage.removeItem( this.getStoreKey() ); } catch ( e ) {} }, /** - * Sync in-memory store back to localStorage. + * Request a sync of the in-memory store back to persisted localStorage. + * + * This function debounces updates. The debouncing logic should account + * for the following factors: + * + * - 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 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 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. * - * 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. + * - 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. + * + * @private * @method */ - update: ( function () { - var timer, hasPendingWrites = false; + requestUpdate: ( function () { + var hasPendingWrites = false; function flushWrites() { var data, key; - if ( !mw.loader.store.enabled ) { - return; - } + // Remove anything from the in-memory store that came from previous page + // loads that no longer corresponds with current module names and versions. mw.loader.store.prune(); + // Process queued module names, serialise their contents to the in-memory store. + while ( mw.loader.store.queue.length ) { + mw.loader.store.set( mw.loader.store.queue.shift() ); + } + key = mw.loader.store.getStoreKey(); try { // Replacing the content of the module store might fail if the new @@ -2270,23 +2324,25 @@ } ); } + // Let the next call to requestUpdate() create a new timer. hasPendingWrites = false; } - 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 ); - } + 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 ); + } }; }() ) }