/* Private Members */
var hasOwn = Object.prototype.hasOwnProperty,
- slice = Array.prototype.slice;
+ slice = Array.prototype.slice,
+ trackCallbacks = $.Callbacks( 'memory' ),
+ trackQueue = [];
/**
* Log a message to window.console, if possible. Useful to force logging of some
return {
/* Public Members */
+ /**
+ * 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`.
+ *
+ * @returns {number} Current time
+ */
+ now: ( function () {
+ var perf = window.performance,
+ navStart = perf && perf.timing && perf.timing.navigationStart;
+ return navStart && typeof perf.now === 'function' ?
+ function () { return navStart + perf.now(); } :
+ function () { return +new Date(); };
+ }() ),
+
+ /**
+ * Track an analytic event.
+ *
+ * This method provides a generic means for MediaWiki JavaScript code to capture state
+ * information for analysis. Each logged event specifies a string topic name that describes
+ * the kind of event that it is. Topic names consist of dot-separated path components,
+ * arranged from most general to most specific. Each path component should have a clear and
+ * well-defined purpose.
+ *
+ * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
+ * events that match their subcription, including those that fired before the handler was
+ * bound.
+ *
+ * @param {string} topic Topic name
+ * @param {Object} [data] Data describing the event, encoded as an object
+ */
+ track: function ( topic, data ) {
+ trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
+ trackCallbacks.fire( trackQueue );
+ },
+
+ /**
+ * Register a handler for subset of analytic events, specified by topic
+ *
+ * Handlers will be called once for each tracked event, including any events that fired before the
+ * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
+ * the exact time at which the event fired, a string 'topic' property naming the event, and a
+ * 'data' property which is an object of event-specific data. The event topic and event data are
+ * also passed to the callback as the first and second arguments, respectively.
+ *
+ * @param {string} topic Handle events whose name starts with this string prefix
+ * @param {Function} callback Handler to call for each matching tracked event
+ */
+ trackSubscribe: function ( topic, callback ) {
+ var seen = 0;
+
+ trackCallbacks.add( function ( trackQueue ) {
+ var event;
+ for ( ; seen < trackQueue.length; seen++ ) {
+ event = trackQueue[ seen ];
+ if ( event.topic.indexOf( topic ) === 0 ) {
+ callback.call( event, event.topic, event.data );
+ }
+ }
+ } );
+ },
+
/**
* Dummy placeholder for {@link mw.log}
* @method
}
return true;
} );
- if ( mw.loader.store.useFunction ) {
- /* jshint -W054 */
- new Function( concatSource.join( ';' ) )();
- } else {
- $.globalEval( concatSource.join( ';' ) );
- }
+ $.globalEval( concatSource.join( ';' ) );
}
// Early exit if there's nothing to load...
/**
* Change the state of one or more modules.
*
- * @param {string|Object} module module name or object of module name/state pairs
- * @param {string} state state name
+ * @param {string|Object} module Module name or object of module name/state pairs
+ * @param {string} state State name
*/
state: function ( module, state ) {
var m;
/**
* Get the state of a module.
*
- * @param {string} module name of module to get state for
+ * @param {string} module Name of module to get state for
*/
getState: function ( module ) {
if ( registry[module] !== undefined && registry[module].state !== undefined ) {
// Cache hit stats
stats: { hits: 0, misses: 0, expired: 0 },
+ // Experiment data
+ experiment: ( function () {
+ var start = ( new Date() ).getTime(), id = 0, seed = 0;
+
+ try {
+ id = JSON.parse( localStorage.getItem( 'moduleStorageExperiment2' ) );
+ if ( typeof id !== 'number' ) {
+ id = Math.floor( Math.random() * Math.random() * 1e16 );
+ localStorage.setItem( 'moduleStorageExperiment2', id );
+ }
+ seed = id % 2000;
+ } catch ( e ) {}
+
+ return {
+ // Unique identifier for this browser. This allows us to group all
+ // datapoints generated by a particular browser, which in turn allows us
+ // to see how the initial load compares to subsequent page loads.
+ id: id,
+
+ // Group assignment may be 0 (not in experiment), 1 (control group), or 2
+ // (experimental group). Browsers that don't implement all the prerequisite APIs
+ // (JSON and Web Storage) are ineligible. Eligible browsers have a 0.1% chance
+ // of being included in the experiment, in which case they are equally likely to
+ // be assigned to either the experimental or control group.
+ group: seed === 1 ? 1 : ( seed === 2 ? 2 : 0 ),
+
+ // Assess module storage performance by measuring the time between this
+ // reference point and the window load event.
+ start: start
+ };
+ }() ),
+
/**
* Construct a JSON-serializable object representing the content of the store.
* @return {Object} Module store contents.
},
/**
- * Initialize the store by retrieving it from localStorage and (if successfully
- * retrieved) decoding the stored JSON value to a plain object.
+ * Initialize the store.
+ *
+ * Retrieves store from localStorage and (if successfully retrieved) decoding
+ * the stored JSON value to a plain object.
*
* The try / catch block is used for JSON & localStorage feature detection.
* See the in-line documentation for Modernizr's localStorage feature detection
- * code for a full account of why we need a try / catch: <http://git.io/4NEwKg>.
+ * code for a full account of why we need a try / catch:
+ * https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796
*/
init: function () {
var raw, data;
if ( mw.loader.store.enabled !== null ) {
- // #init already ran.
+ // Init already ran
return;
}
- if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
- // Disabled by configuration, or because debug mode is set.
+ if ( ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) && mw.loader.store.experiment.group !== 2 )
+ || mw.config.get( 'debug' ) ) {
+ // Disabled by configuration, or because debug mode is set
mw.loader.store.enabled = false;
return;
}
try {
raw = localStorage.getItem( mw.loader.store.getStoreKey() );
- // If we get here, localStorage is available; mark enabled.
+ // If we get here, localStorage is available; mark enabled
mw.loader.store.enabled = true;
- mw.loader.store.useFunction = !!Math.floor( Math.random() * 2 );
data = JSON.parse( raw );
if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
mw.loader.store.items = data.items;
} catch (e) {}
if ( raw === undefined ) {
- mw.loader.store.enabled = false; // localStorage failed; disable store.
+ // localStorage failed; disable store
+ mw.loader.store.enabled = false;
} else {
mw.loader.store.update();
}
get: function ( module ) {
var key;
- if ( mw.loader.store.enabled !== true ) {
+ if ( !mw.loader.store.enabled ) {
return false;
}
set: function ( module, descriptor ) {
var args, key;
- if ( mw.loader.store.enabled !== true ) {
+ if ( !mw.loader.store.enabled ) {
return false;
}
key = mw.loader.store.getModuleKey( module );
- if ( key in mw.loader.store.items ) {
- // Already set; decline to store.
- return false;
- }
-
- if ( descriptor.state !== 'ready' ) {
- // Module failed to load; decline to store.
- return false;
- }
-
- if ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) {
- // Unversioned, private, or site-/user-specific; decline to store.
- return false;
- }
-
- if ( $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1 ) {
- // Partial descriptor; decline to store.
+ if (
+ // Already stored a copy of this exact version
+ key in mw.loader.store.items ||
+ // Module failed to load
+ descriptor.state !== 'ready' ||
+ // Unversioned, private, or site-/user-specific
+ ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
+ // Partial descriptor
+ $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
+ ) {
+ // Decline to store
return false;
}
args = [
JSON.stringify( module ),
typeof descriptor.script === 'function' ?
- String( descriptor.script ) : JSON.stringify( descriptor.script ),
+ String( descriptor.script ) :
+ JSON.stringify( descriptor.script ),
JSON.stringify( descriptor.style ),
JSON.stringify( descriptor.messages )
];
- } catch (e) {
+ // Attempted workaround for a possible Opera bug (bug 57567).
+ // This regex should never match under sane conditions.
+ if ( /^\s*\(/.test( args[1] ) ) {
+ args[1] = 'function' + args[1];
+ log( 'Detected malformed function stringification (bug 57567)' );
+ }
+ } catch ( e ) {
return;
}
+
mw.loader.store.items[key] = 'mw.loader.implement(' + args.join(',') + ');';
mw.loader.store.update();
},
prune: function () {
var key, module;
- if ( mw.loader.store.enabled !== true ) {
+ if ( !mw.loader.store.enabled ) {
return false;
}
var timer;
function flush() {
- var data, key = mw.loader.store.getStoreKey();
- if ( mw.loader.store.enabled !== true ) {
+ var data,
+ key = mw.loader.store.getStoreKey();
+
+ if ( !mw.loader.store.enabled ) {
return false;
}
mw.loader.store.prune();
localStorage.removeItem( key );
data = JSON.stringify( mw.loader.store );
localStorage.setItem( key, data );
- } catch (e) {}
+ } catch ( e ) {}
}
return function () {