mediawiki-extensions-Multim.../resources/mmv/mmv.performance.js
Gilles Dubuc e74fc33e89 Track detailed content loading network performance
Leverages the W3C Navigation Timing API when available

Change-Id: Ief1d327d1bd8928bf5f2bf0bd4c7141a0a608a53
Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/126
2014-02-07 19:37:22 +00:00

256 lines
7.5 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;
/**
* @class mw.mmv.performance
* Measures the network performance
* See https://meta.wikimedia.org/wiki/Schema:MultimediaViewerNetworkPerformance
* @constructor
*/
function Performance() {}
P = Performance.prototype;
/**
* 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
* @returns {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
*/
P.record = function ( type, url ) {
var deferred = $.Deferred(),
request,
perf = this,
start;
request = new XMLHttpRequest();
request.onreadystatechange = function () {
var total = $.now() - start;
if ( request.readyState === 4 ) {
deferred.resolve( request.response );
// 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 );
}, 1000 );
}
};
start = $.now();
request.open( 'GET', url, true );
request.send();
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 timingList, timingEntry, i,
matches,
stats = { type: type,
contentHost: window.location.host,
userAgent: navigator.userAgent,
isHttps: window.location.protocol === 'https:',
total: total },
varnishXCache,
performance = this.getWindowPerformance(),
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;
}
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 ];
}
if ( request ) {
stats.XCache = request.getResponseHeader( 'X-Cache' );
stats.XVarnish = request.getResponseHeader( 'X-Varnish' );
if ( stats.XCache && stats.XCache.length ) {
varnishXCache = this.parseVarnishXCacheHeader( stats.XCache );
$.each( varnishXCache, function( key, value ) {
stats[ key ] = value;
} );
}
stats.contentLength = parseInt( request.getResponseHeader( 'Content-Length' ), 10 );
stats.age = parseInt( request.getResponseHeader( 'Age' ), 10 );
stats.timestamp = new Date( request.getResponseHeader( 'Date' ) ).getTime() / 1000;
stats.status = request.status;
}
// If we're given an xhr and we have access to the Navigation Timing API, use it
if ( performance && performance.getEntriesByType && request ) {
timingList = performance.getEntriesByType( 'resource' );
for ( i = 0; i < timingList.length; i++ ) {
timingEntry = timingList[ i ];
if ( timingEntry.initiatorType === 'xmlhttprequest'
&& timingEntry.name === url ) {
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 );
}
}
// Don't record entries that hit the browser cache
if ( stats.request < 1 ) {
return;
}
}
// Add connection information if there's any
if ( connection ) {
if ( connection.bandwidth ) {
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 );
};
/**
* 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;
};
mw.mmv.performance = Performance;
}( mediaWiki, jQuery ) );