mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer
synced 2024-11-12 09:27:36 +00:00
5df979bd65
Change-Id: Id0def78beb0231270557037a9c530c770b100ce2
431 lines
13 KiB
JavaScript
Executable file
431 lines
13 KiB
JavaScript
Executable file
/*
|
|
* This file is part of the MediaWiki extension MultimediaViewer.
|
|
*
|
|
* MultimediaViewer is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* MultimediaViewer is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
( function ( mw, $ ) {
|
|
var P;
|
|
|
|
/**
|
|
* Measures the network performance
|
|
* See <https://meta.wikimedia.org/wiki/Schema:MultimediaViewerNetworkPerformance>
|
|
* @class mw.mmv.Performance
|
|
* @constructor
|
|
*/
|
|
function Performance() {}
|
|
|
|
P = Performance.prototype;
|
|
|
|
/**
|
|
* How long to wait to ensure window.performance is populated
|
|
* @property {number}
|
|
*/
|
|
P.delay = 1000;
|
|
|
|
/**
|
|
* Global setup that should be done while the page loads
|
|
*/
|
|
P.init = function () {
|
|
var performance = this.getWindowPerformance();
|
|
|
|
// by default logging is cut off after 150 resources, which is not enough in debug mode
|
|
// only supported by IE
|
|
if ( mw.config.get( 'debug' ) && performance.setResourceTimingBufferSize ) {
|
|
performance.setResourceTimingBufferSize( 500 );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gather network performance for a given URL
|
|
* Will only run on a sample of users/requests. Avoid using this on URLs that aren't
|
|
* cached by the browser, as it will consume unnecessary bandwidth for the user.
|
|
* @param {string} type the type of request to be measured
|
|
* @param {string} url URL to be measured
|
|
* @param {string} responseType responseType for the XHR options
|
|
* @returns {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
|
|
*/
|
|
P.record = function ( type, url, responseType ) {
|
|
var deferred = $.Deferred(),
|
|
request,
|
|
perf = this,
|
|
start;
|
|
|
|
request = this.newXHR();
|
|
|
|
request.onreadystatechange = function () {
|
|
var total = $.now() - start;
|
|
|
|
if ( request.readyState === 4 ) {
|
|
deferred.resolve( request.response );
|
|
|
|
perf.recordEntryDelayed( type, total, url, request );
|
|
}
|
|
};
|
|
|
|
start = $.now();
|
|
|
|
try {
|
|
request.open( 'GET', url, true );
|
|
|
|
if ( responseType !== undefined ) {
|
|
request.responseType = responseType;
|
|
}
|
|
|
|
request.send();
|
|
} catch ( e ) {
|
|
// This happens on old browsers that don't support CORS
|
|
return deferred.reject();
|
|
}
|
|
|
|
return deferred;
|
|
};
|
|
|
|
/**
|
|
* Records network performance results for a given url
|
|
* Will record if enough data is present and it's not a local cache hit
|
|
* @param {string} type the type of request to be measured
|
|
* @param {number} total the total load time tracked with a basic technique
|
|
* @param {string} url URL of that was measured
|
|
* @param {XMLHttpRequest} request HTTP request that just completed
|
|
*/
|
|
P.recordEntry = function ( type, total, url, request ) {
|
|
var matches,
|
|
stats = { type: type,
|
|
contentHost: window.location.host,
|
|
userAgent: navigator.userAgent,
|
|
isHttps: window.location.protocol === 'https:',
|
|
total: total },
|
|
connection = this.getNavigatorConnection();
|
|
|
|
// If eventLog isn't present there is nowhere to record to
|
|
if ( !mw.eventLog ) {
|
|
return;
|
|
}
|
|
|
|
if ( !this.performanceChecked ) {
|
|
this.performanceChecked = {};
|
|
}
|
|
|
|
// Don't record if we're not in the sample
|
|
if ( !this.isInSample() ) {
|
|
return;
|
|
}
|
|
|
|
// Don't record entries that hit the browser cache on undetailed requests
|
|
if ( total !== undefined && total < 1 ) {
|
|
return;
|
|
}
|
|
|
|
if ( url && url.length ) {
|
|
// There is no need to measure the same url more than once
|
|
if ( url in this.performanceChecked ) {
|
|
return;
|
|
}
|
|
|
|
this.performanceChecked[ url ] = true;
|
|
|
|
matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
|
|
stats.isHttps = url.indexOf( 'https' ) === 0;
|
|
}
|
|
|
|
if ( !matches || matches.length !== 2 ) {
|
|
stats.urlHost = stats.contentHost;
|
|
} else {
|
|
stats.urlHost = matches[ 1 ];
|
|
}
|
|
|
|
this.populateStatsFromXhr( stats, request );
|
|
|
|
this.populateStatsFromPerformance( stats, url );
|
|
// Don't record entries that hit the browser cache
|
|
if ( stats.request === 0 ) {
|
|
return;
|
|
}
|
|
|
|
// Add connection information if there's any
|
|
if ( connection ) {
|
|
if ( connection.bandwidth ) {
|
|
if ( connection.bandwidth === Infinity ) {
|
|
stats.bandwidth = -1;
|
|
} else {
|
|
stats.bandwidth = Math.round( connection.bandwidth );
|
|
}
|
|
}
|
|
|
|
if ( connection.metered ) {
|
|
stats.metered = connection.metered;
|
|
}
|
|
}
|
|
|
|
// Add Geo information if there's any
|
|
if ( $.isPlainObject( window.Geo ) && typeof window.Geo.country === 'string' ) {
|
|
stats.country = window.Geo.country;
|
|
}
|
|
|
|
mw.eventLog.logEvent( 'MultimediaViewerNetworkPerformance', stats );
|
|
};
|
|
|
|
/**
|
|
* Processes an XMLHttpRequest (or jqXHR) object
|
|
* @param {Object} stats stats object to extend with additional statistics fields
|
|
* @param {XMLHttpRequest} request
|
|
*/
|
|
P.populateStatsFromXhr = function ( stats, request ) {
|
|
var age,
|
|
contentLength,
|
|
xcache,
|
|
xvarnish,
|
|
varnishXCache;
|
|
|
|
if ( !request ) {
|
|
return;
|
|
}
|
|
|
|
stats.status = request.status;
|
|
|
|
// Chrome disallows header access for CORS image requests, even if the responose has the
|
|
// proper header :-/
|
|
contentLength = request.getResponseHeader( 'Content-Length' );
|
|
if ( contentLength === null ) {
|
|
return;
|
|
}
|
|
|
|
xcache = request.getResponseHeader( 'X-Cache' );
|
|
if ( xcache ) {
|
|
stats.XCache = xcache;
|
|
varnishXCache = this.parseVarnishXCacheHeader( xcache );
|
|
|
|
$.each( varnishXCache, function( key, value ) {
|
|
stats[ key ] = value;
|
|
} );
|
|
}
|
|
|
|
xvarnish = request.getResponseHeader( 'X-Varnish' );
|
|
if ( xvarnish ) {
|
|
stats.XVarnish = xvarnish;
|
|
}
|
|
|
|
stats.contentLength = parseInt( contentLength, 10 );
|
|
|
|
age = parseInt( request.getResponseHeader( 'Age' ), 10 );
|
|
if ( !isNaN( age ) ) {
|
|
stats.age = age;
|
|
}
|
|
|
|
stats.timestamp = new Date( request.getResponseHeader( 'Date' ) ).getTime() / 1000;
|
|
};
|
|
|
|
/**
|
|
* Populates statistics based on the Request Timing API
|
|
* @param {Object} stats
|
|
* @param {string} url
|
|
*/
|
|
P.populateStatsFromPerformance = function( stats, url ) {
|
|
var performance = this.getWindowPerformance(),
|
|
timingEntries, timingEntry;
|
|
|
|
// If we're given an xhr and we have access to the Navigation Timing API, use it
|
|
if ( performance && performance.getEntriesByName ) {
|
|
// This could be tricky as we need to match encoding (the Request Timing API uses
|
|
// percent-encoded UTF-8). The main use case we are interested in is thumbnails and
|
|
// jQuery AJAX. jQuery uses encodeURIComponent to construct URL parameters, and
|
|
// thumbnail URLs come from MediaWiki API which also encodes them, so both should be
|
|
// all right.
|
|
timingEntries = performance.getEntriesByName( url );
|
|
|
|
if ( timingEntries.length ) {
|
|
// Let's hope it's the first request for the given URL we are interested in.
|
|
// This could fail in exotic cases (e.g. we send an AJAX request for a thumbnail,
|
|
// but it exists on the page as a normal thumbnail with the exact same size),
|
|
// but it's unlikely.
|
|
timingEntry = timingEntries[0];
|
|
|
|
stats.total = Math.round( timingEntry.duration );
|
|
stats.redirect = Math.round( timingEntry.redirectEnd - timingEntry.redirectStart );
|
|
stats.dns = Math.round( timingEntry.domainLookupEnd - timingEntry.domainLookupStart );
|
|
stats.tcp = Math.round( timingEntry.connectEnd - timingEntry.connectStart );
|
|
stats.request = Math.round( timingEntry.responseStart - timingEntry.requestStart );
|
|
stats.response = Math.round( timingEntry.responseEnd - timingEntry.responseStart );
|
|
stats.cache = Math.round( timingEntry.domainLookupStart - timingEntry.fetchStart );
|
|
} else if ( performance.getEntriesByType( 'resource' ).length === 150 ) {
|
|
// browser stops logging after 150 entries
|
|
mw.log( 'performance buffer full, results are probably incorrect' );
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Like recordEntry, but takes a jqXHR argument instead of a normal XHR one.
|
|
* Due to the way some parameters are retrieved, this will work best if the context option
|
|
* for the ajax request was not used.
|
|
* @param {string} type the type of request to be measured
|
|
* @param {number} total the total load time tracked with a basic technique
|
|
* @param {jqXHR} jqxhr
|
|
*/
|
|
P.recordJQueryEntry = function ( type, total, jqxhr ) {
|
|
var perf = this;
|
|
|
|
// We take advantage of the fact that the context of the jqXHR deferred is the AJAX
|
|
// settings object. The deferred has already resolved so chaining to it does not influence
|
|
// the timing.
|
|
jqxhr.done( function () {
|
|
var url;
|
|
|
|
if ( !this.url ) {
|
|
mw.log.warn( 'Cannot find URL - did you use context option?' );
|
|
} else {
|
|
url = this.url;
|
|
// The performance API returns absolute URLs, but the one in the settings object is
|
|
// usually relative.
|
|
if ( !url.match( /^(\w+:)?\/\// ) ) {
|
|
url = location.protocol + '//' + location.host + url;
|
|
}
|
|
}
|
|
|
|
if ( this.crossDomain && this.dataType === 'jsonp' ) {
|
|
// Cross-domain jQuery requests return a fake jqXHR object which is useless and
|
|
// would only cause logging errors.
|
|
jqxhr = undefined;
|
|
}
|
|
|
|
// jQuery does not expose the original XHR object, but the jqXHR wrapper is similar
|
|
// enogh that we will probably get away by passing it instead.
|
|
perf.recordEntry( type, total, url, jqxhr );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Records network performance results for a given url
|
|
* Will record if enough data is present and it's not a local cache hit
|
|
* Will run after a delay to make sure the window.performance entry is present
|
|
* @param {string} type the type of request to be measured
|
|
* @param {number} total the total load time tracked with a basic technique
|
|
* @param {string} url URL of that was measured
|
|
* @param {XMLHttpRequest} request HTTP request that just completed
|
|
*/
|
|
P.recordEntryDelayed = function ( type, total, url, request ) {
|
|
var perf = this;
|
|
|
|
// The timeout is necessary because if there's an entry in window.performance,
|
|
// it hasn't been added yet at this point
|
|
setTimeout( function() {
|
|
perf.recordEntry( type, total, url, request );
|
|
}, this.delay );
|
|
};
|
|
|
|
/**
|
|
* Like recordEntryDelayed, but for jQuery AJAX requests.
|
|
* @param {string} type the type of request to be measured
|
|
* @param {number} total the total load time tracked with a basic technique
|
|
* @param {jqXHR} jqxhr
|
|
*/
|
|
P.recordJQueryEntryDelayed = function ( type, total, jqxhr ) {
|
|
var perf = this;
|
|
|
|
// The timeout is necessary because if there's an entry in window.performance,
|
|
// it hasn't been added yet at this point
|
|
setTimeout( function() {
|
|
perf.recordJQueryEntry( type, total, jqxhr );
|
|
}, 0 );
|
|
};
|
|
|
|
/**
|
|
* Parses an X-Cache header from Varnish and extracts varnish information
|
|
* @param {string} header The X-Cache header from the request
|
|
* @returns {Object} The parsed X-Cache data
|
|
*/
|
|
P.parseVarnishXCacheHeader = function ( header ) {
|
|
var parts,
|
|
part,
|
|
subparts,
|
|
i,
|
|
results = {},
|
|
matches;
|
|
|
|
if ( !header || !header.length ) {
|
|
return results;
|
|
}
|
|
|
|
parts = header.split( ',' );
|
|
|
|
for ( i = 0; i < parts.length; i++ ) {
|
|
part = parts[ i ];
|
|
subparts = part.trim().split( ' ' );
|
|
|
|
// If the subparts aren't space-separated, it's an unknown format, skip
|
|
if ( subparts.length < 2 ) {
|
|
continue;
|
|
}
|
|
|
|
matches = part.match( /\(([0-9]+)\)/ );
|
|
|
|
// If there is no number between parenthesis for a given server
|
|
// it's an unknown format, skip
|
|
if ( !matches || matches.length !== 2 ) {
|
|
continue;
|
|
}
|
|
|
|
results[ 'varnish' + ( i + 1 ) ] = subparts[ 0 ];
|
|
results[ 'varnish' + ( i + 1 ) + 'hits' ] = parseInt( matches[ 1 ], 10 );
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* Returns the window's Performance object
|
|
* Allows us to override for unit tests
|
|
* @returns {Object} The window's Performance object
|
|
*/
|
|
P.getWindowPerformance = function () {
|
|
return window.performance;
|
|
};
|
|
|
|
/**
|
|
* Returns whether or not we should measure this request
|
|
* @returns {boolean} True if this request needs to be sampled
|
|
*/
|
|
P.isInSample = function () {
|
|
var factor = mw.config.get( 'wgNetworkPerformanceSamplingFactor' );
|
|
|
|
if ( !$.isNumeric( factor ) || factor < 1 ) {
|
|
return false;
|
|
}
|
|
return Math.floor( Math.random() * factor ) === 0;
|
|
};
|
|
|
|
/**
|
|
* Returns the navigator's Connection object
|
|
* Allows us to override for unit tests
|
|
* @returns {Object} The navigator's Connection object
|
|
*/
|
|
P.getNavigatorConnection = function () {
|
|
return navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
};
|
|
|
|
/**
|
|
* Returns a new XMLHttpRequest object
|
|
* Allows us to override for unit tests
|
|
* @returns {XMLHttpRequest} New XMLHttpRequest
|
|
*/
|
|
P.newXHR = function () {
|
|
return new XMLHttpRequest();
|
|
};
|
|
|
|
new Performance().init();
|
|
|
|
mw.mmv.Performance = Performance;
|
|
}( mediaWiki, jQuery ) );
|