summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js')
-rw-r--r--www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js455
1 files changed, 455 insertions, 0 deletions
diff --git a/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js
new file mode 100644
index 00000000..026626ff
--- /dev/null
+++ b/www/wiki/extensions/MultimediaViewer/resources/mmv/logging/mmv.logging.PerformanceLogger.js
@@ -0,0 +1,455 @@
+/*
+ * 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, $, oo ) {
+ var PL;
+
+ /**
+ * Measures the network performance
+ * See <https://meta.wikimedia.org/wiki/Schema:MultimediaViewerNetworkPerformance>
+ *
+ * @class mw.mmv.logging.PerformanceLogger
+ * @extends mw.mmv.logging.Logger
+ * @constructor
+ */
+ function PerformanceLogger() {}
+
+ oo.inheritClass( PerformanceLogger, mw.mmv.logging.Logger );
+
+ PL = PerformanceLogger.prototype;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).networkPerformanceSamplingFactor;
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.schema = 'MultimediaViewerNetworkPerformance';
+
+ /**
+ * Global setup that should be done while the page loads
+ */
+ PL.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 && 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 {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to the extra stats.
+ * @return {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
+ */
+ PL.record = function ( type, url, extraStatsDeferred ) {
+ var deferred = $.Deferred(),
+ request,
+ perf = this,
+ start;
+
+ try {
+ request = this.newXHR();
+
+ request.onprogress = function ( e ) {
+ var percent;
+
+ if ( e.lengthComputable ) {
+ percent = ( e.loaded / e.total ) * 100;
+ }
+
+ deferred.notify( request.response, percent );
+ };
+
+ request.onreadystatechange = function () {
+ var total = $.now() - start;
+
+ if ( request.readyState === 4 ) {
+ deferred.notify( request.response, 100 );
+ deferred.resolve( request.response );
+ perf.recordEntryDelayed( type, total, url, request, extraStatsDeferred );
+ }
+ };
+
+ start = $.now();
+ request.open( 'GET', url, true );
+ request.send();
+ } catch ( e ) {
+ // old browser not supporting XMLHttpRequest or CORS, or CORS is not permitted
+ 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
+ * @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to extra stats to be included.
+ * @return {jQuery.Promise}
+ */
+ PL.recordEntry = function ( type, total, url, request, extraStatsDeferred ) {
+ var matches,
+ logger = this,
+ stats = { type: type,
+ contentHost: window.location.host,
+ isHttps: window.location.protocol === 'https:',
+ total: total },
+ connection = this.getNavigatorConnection();
+
+ if ( !this.performanceChecked ) {
+ this.performanceChecked = {};
+ }
+
+ if ( url && url.length ) {
+ // There is no need to measure the same url more than once
+ if ( url in this.performanceChecked ) {
+ return $.Deferred().reject();
+ }
+
+ 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 );
+
+ // 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;
+ }
+ }
+
+ return ( extraStatsDeferred || $.Deferred().reject() ).done( function ( extraStats ) {
+ stats = $.extend( stats, extraStats );
+ } ).always( function () {
+ logger.log( stats );
+ } );
+ };
+
+ /**
+ * Processes an XMLHttpRequest (or jqXHR) object
+ *
+ * @param {Object} stats stats object to extend with additional statistics fields
+ * @param {XMLHttpRequest} request
+ */
+ PL.populateStatsFromXhr = function ( stats, request ) {
+ var age,
+ contentLength,
+ xcache,
+ xvarnish,
+ varnishXCache,
+ lastModified;
+
+ 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;
+
+ lastModified = request.getResponseHeader( 'Last-Modified' );
+ if ( lastModified ) {
+ stats.lastModified = new Date( lastModified ).getTime() / 1000;
+ }
+ };
+
+ /**
+ * Populates statistics based on the Request Timing API
+ *
+ * @param {Object} stats
+ * @param {string} url
+ */
+ PL.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 && this.isEnabled() ) {
+ // 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
+ */
+ PL.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
+ * @param {jQuery.Promise.<string>} extraStatsDeferred A promise which resolves to extra stats.
+ */
+ PL.recordEntryDelayed = function ( type, total, url, request, extraStatsDeferred ) {
+ 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, extraStatsDeferred );
+ }, 0 );
+ };
+
+ /**
+ * 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
+ */
+ PL.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
+ * @return {Object} The parsed X-Cache data
+ */
+ PL.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
+ *
+ * @return {Object} The window's Performance object
+ */
+ PL.getWindowPerformance = function () {
+ return window.performance;
+ };
+
+ /**
+ * Returns the navigator's Connection object
+ * Allows us to override for unit tests
+ *
+ * @return {Object} The navigator's Connection object
+ */
+ PL.getNavigatorConnection = function () {
+ return navigator.connection || navigator.mozConnection || navigator.webkitConnection;
+ };
+
+ /**
+ * Returns a new XMLHttpRequest object
+ * Allows us to override for unit tests
+ *
+ * @return {XMLHttpRequest} New XMLHttpRequest
+ */
+ PL.newXHR = function () {
+ return new XMLHttpRequest();
+ };
+
+ /**
+ * @override
+ * @inheritdoc
+ */
+ PL.log = function ( data ) {
+ var trackedWidths = mw.mmv.ThumbnailWidthCalculator.prototype.defaultOptions.widthBuckets.slice( 0 );
+ trackedWidths.push( 600 ); // Most common non-bucket size
+
+ // Track thumbnail load time with statsv, sampled
+ if ( this.isInSample() &&
+ data.type === 'image' &&
+ data.imageWidth > 0 &&
+ data.total > 20 &&
+ $.inArray( data.imageWidth, trackedWidths ) !== -1
+ ) {
+ mw.track( 'timing.media.thumbnail.client.' + data.imageWidth, data.total );
+ }
+
+ if ( this.isEnabled() ) {
+ mw.log( 'mw.mmv.logging.PerformanceLogger', data );
+ }
+ return mw.mmv.logging.Logger.prototype.log.call( this, data );
+ };
+
+ new PerformanceLogger().init();
+
+ mw.mmv.logging.PerformanceLogger = PerformanceLogger;
+
+}( mediaWiki, jQuery, OO ) );