Gilles has uploaded a new change for review. https://gerrit.wikimedia.org/r/113344
Change subject: Record detailed network performance of API calls ...................................................................... Record detailed network performance of API calls Also fixes some issues with incorrect values passed to event logging Change-Id: I2cc730727d30fb4f87a4fbe81725d6859690589b Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/211 --- M MultimediaViewer.php M resources/mmv/mmv.performance.js M resources/mmv/provider/mmv.provider.Api.js M resources/mmv/provider/mmv.provider.FileRepoInfo.js M resources/mmv/provider/mmv.provider.GlobalUsage.js M resources/mmv/provider/mmv.provider.ImageInfo.js M resources/mmv/provider/mmv.provider.ImageUsage.js M resources/mmv/provider/mmv.provider.ThumbnailInfo.js M resources/mmv/provider/mmv.provider.UserInfo.js M tests/qunit/provider/mmv.provider.Api.test.js 10 files changed, 196 insertions(+), 64 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/MultimediaViewer refs/changes/44/113344/1 diff --git a/MultimediaViewer.php b/MultimediaViewer.php index ca9d0af..6e777bd 100644 --- a/MultimediaViewer.php +++ b/MultimediaViewer.php @@ -518,7 +518,7 @@ $wgResourceModules['schema.MultimediaViewerNetworkPerformance'] = array( 'class' => 'ResourceLoaderSchemaModule', 'schema' => 'MultimediaViewerNetworkPerformance', - 'revision' => 7393226, + 'revision' => 7488625, ); $wgResourceModules['mmv']['dependencies'][] = 'ext.eventLogging'; diff --git a/resources/mmv/mmv.performance.js b/resources/mmv/mmv.performance.js index 9fec186..45b420b 100755 --- a/resources/mmv/mmv.performance.js +++ b/resources/mmv/mmv.performance.js @@ -48,12 +48,7 @@ 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 ); + perf.recordEntryDelayed( type, total, url, request ); } }; @@ -75,6 +70,9 @@ P.recordEntry = function ( type, total, url, request ) { var timingList, timingEntry, i, matches, + age, + xcache, + xvarnish, stats = { type: type, contentHost: window.location.host, userAgent: navigator.userAgent, @@ -117,10 +115,19 @@ } if ( request ) { - stats.XCache = request.getResponseHeader( 'X-Cache' ); - stats.XVarnish = request.getResponseHeader( 'X-Varnish' ); + xcache = request.getResponseHeader( 'X-Cache' ); - if ( stats.XCache && stats.XCache.length ) { + if ( xcache ) { + stats.XCache = xcache; + } + + xvarnish = request.getResponseHeader( 'X-Varnish' ); + + if ( xvarnish ) { + stats.XVarnish = xvarnish; + } + + if ( stats.XCache && stats.XCache.length && stats.XCache ) { varnishXCache = this.parseVarnishXCacheHeader( stats.XCache ); $.each( varnishXCache, function( key, value ) { @@ -129,7 +136,13 @@ } stats.contentLength = parseInt( request.getResponseHeader( 'Content-Length' ), 10 ); - stats.age = parseInt( request.getResponseHeader( 'Age' ), 10 ); + + age = parseInt( request.getResponseHeader( 'Age' ), 10 ); + + if ( !isNaN( age ) ) { + stats.age = age; + } + stats.timestamp = new Date( request.getResponseHeader( 'Date' ) ).getTime() / 1000; stats.status = request.status; } @@ -141,7 +154,7 @@ for ( i = 0; i < timingList.length; i++ ) { timingEntry = timingList[ i ]; if ( timingEntry.initiatorType === 'xmlhttprequest' - && timingEntry.name === url ) { + && decodeURIComponent( 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 ); @@ -161,7 +174,11 @@ // Add connection information if there's any if ( connection ) { if ( connection.bandwidth ) { - stats.bandwidth = Math.round( connection.bandwidth ); + if ( connection.bandwidth === Infinity ) { + stats.bandwidth = -1; + } else { + stats.bandwidth = Math.round( connection.bandwidth ); + } } if ( connection.metered ) { @@ -178,6 +195,25 @@ }; /** + * 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 ); + } ); + }; + + /** * 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 diff --git a/resources/mmv/provider/mmv.provider.Api.js b/resources/mmv/provider/mmv.provider.Api.js index 3baa841..e40e08d 100644 --- a/resources/mmv/provider/mmv.provider.Api.js +++ b/resources/mmv/provider/mmv.provider.Api.js @@ -143,6 +143,46 @@ return $.Deferred().reject( this.getErrorMessage( data ), data ); }; + /** + * @method + * Reconstructs the URL reached by an API call based on the parameters passed to mw.api + * @param {Object} ajaxParameters AJAX parameters passed to mw.api.get + * @param {Object} ajaxOptions AJAX options passed to mw.api.get + * @returns {string} the URL for the corresponding API HTTP request + */ + Api.prototype.makeURL = function( ajaxParameters, ajaxOptions ) { + if ( !$.isPlainObject( ajaxParameters ) ) { + throw 'ajaxParameters must be an object'; + } + + // We have to reproduce the defaults of mw.api + // Otherwise the parameters object keys won't be in the right order + var url, + defaults = { action: 'query', format: 'json' }, + combinedParameters = $.extend( defaults, ajaxParameters ), + parameters = []; + + if ( ajaxOptions && $.isPlainObject( ajaxOptions ) && ajaxOptions.url ) { + url = ajaxOptions.url + '?'; + } else { + url = location.protocol + '//' + location.host + mw.util.wikiScript( 'api' ) + '?'; + } + + $.each( combinedParameters, function( key, value ) { + var i; + + if ( value instanceof Array ) { + for (i = 0; i < value.length; i++ ) { + parameters.push( key + '[]=' + value[ i ] ); + } + } else { + parameters.push( key + '=' + value ); + } + } ); + + return url + parameters.join( '&' ); + }; + mw.mmv.provider = {}; mw.mmv.provider.Api = Api; }( mediaWiki, jQuery ) ); diff --git a/resources/mmv/provider/mmv.provider.FileRepoInfo.js b/resources/mmv/provider/mmv.provider.FileRepoInfo.js index e256893..3dd4274 100644 --- a/resources/mmv/provider/mmv.provider.FileRepoInfo.js +++ b/resources/mmv/provider/mmv.provider.FileRepoInfo.js @@ -37,16 +37,21 @@ */ FileRepoInfo.prototype.get = function() { var provider = this, - start; - - if ( !this.cache['*'] ) { - start = $.now(); - this.cache['*'] = this.api.get( { + start, + ajax = { action: 'query', meta: 'filerepoinfo', format: 'json' - } ).then( function( data ) { - provider.performance.recordEntry( 'filerepoinfo', $.now() - start ); + }; + + if ( !this.cache['*'] ) { + start = $.now(); + this.cache['*'] = this.api.get( ajax ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'filerepoinfo', + $.now() - start, + provider.makeURL( ajax ), + xhr); + return provider.getQueryField( 'repos', data ); } ).then( function( reposArray ) { var reposHash = {}; diff --git a/resources/mmv/provider/mmv.provider.GlobalUsage.js b/resources/mmv/provider/mmv.provider.GlobalUsage.js index db439d0..67dc979 100644 --- a/resources/mmv/provider/mmv.provider.GlobalUsage.js +++ b/resources/mmv/provider/mmv.provider.GlobalUsage.js @@ -56,7 +56,16 @@ var provider = this, cacheKey = file.getPrefixedDb(), fileUsage, - start; + start, + ajax = { + action: 'query', + prop: 'globalusage', + titles: file.getPrefixedDb(), + guprop: ['url', 'namespace'], + gufilterlocal: 1, + gulimit: this.options.apiLimit, + format: 'json' + }; if ( this.options.doNotUseApi ) { fileUsage = new mw.mmv.model.FileUsage( file, mw.mmv.model.FileUsage.Scope.GLOBAL, @@ -67,16 +76,12 @@ if ( !this.cache[cacheKey] ) { start = $.now(); - this.cache[cacheKey] = this.api.get( { - action: 'query', - prop: 'globalusage', - titles: file.getPrefixedDb(), - guprop: ['url', 'namespace'], - gufilterlocal: 1, - gulimit: this.options.apiLimit, - format: 'json' - } ).then( function( data ) { - provider.performance.recordEntry( 'globalusage', $.now() - start ); + this.cache[cacheKey] = this.api.get( ajax ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'globalusage', + $.now() - start, + provider.makeURL( ajax ), + xhr ); + return provider.getQueryPage( file, data ); } ).then( function( pageData, data ) { var pages; diff --git a/resources/mmv/provider/mmv.provider.ImageInfo.js b/resources/mmv/provider/mmv.provider.ImageInfo.js index 26b5f0d..d524c05 100644 --- a/resources/mmv/provider/mmv.provider.ImageInfo.js +++ b/resources/mmv/provider/mmv.provider.ImageInfo.js @@ -75,11 +75,8 @@ ImageInfo.prototype.get = function( file ) { var provider = this, cacheKey = file.getPrefixedDb(), - start; - - if ( !this.cache[cacheKey] ) { - start = $.now(); - this.cache[cacheKey] = this.api.get( { + start, + ajax = { action: 'query', prop: 'imageinfo', titles: file.getPrefixedDb(), @@ -87,8 +84,16 @@ iiextmetadatafilter: this.iiextmetadatafilter, iiextmetadatalanguage: this.options.language, format: 'json' - } ).then( function( data ) { - provider.performance.recordEntry( 'imageinfo', $.now() - start ); + }; + + if ( !this.cache[cacheKey] ) { + start = $.now(); + this.cache[cacheKey] = this.api.get( ajax ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'imageinfo', + $.now() - start, + provider.makeURL( ajax ), + xhr ); + return provider.getQueryPage( file, data ); } ).then( function( page ) { if ( page.imageinfo && page.imageinfo.length ) { diff --git a/resources/mmv/provider/mmv.provider.ImageUsage.js b/resources/mmv/provider/mmv.provider.ImageUsage.js index 697b078..f39c8d5 100644 --- a/resources/mmv/provider/mmv.provider.ImageUsage.js +++ b/resources/mmv/provider/mmv.provider.ImageUsage.js @@ -50,19 +50,24 @@ ImageUsage.prototype.get = function( file ) { var provider = this, cacheKey = file.getPrefixedDb(), - start; - - if ( !this.cache[cacheKey] ) { - start = $.now(); - this.cache[cacheKey] = this.api.get( { + start, + ajax = { action: 'query', list: 'imageusage', iutitle: file.getPrefixedDb(), iunamespace: this.options.namespaces, iulimit: this.options.apiLimit, format: 'json' - } ).then( function( data ) { - provider.performance.recordEntry( 'imageusage', $.now() - start ); + }; + + if ( !this.cache[cacheKey] ) { + start = $.now(); + this.cache[cacheKey] = this.api.get( ajax ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'imageusage', + $.now() - start, + provider.makeURL( ajax ), + xhr ); + return provider.getQueryField( 'imageusage', data ); } ).then( function( imageusage, data ) { var pages; diff --git a/resources/mmv/provider/mmv.provider.ThumbnailInfo.js b/resources/mmv/provider/mmv.provider.ThumbnailInfo.js index 1c80cb7..58b68d2 100644 --- a/resources/mmv/provider/mmv.provider.ThumbnailInfo.js +++ b/resources/mmv/provider/mmv.provider.ThumbnailInfo.js @@ -45,11 +45,8 @@ ThumbnailInfo.prototype.get = function( file, width, height ) { var provider = this, cacheKey = file.getPrefixedDb() + '|' + ( width || '' ) + '|' + ( height || '' ), - start; - - if ( !this.cache[cacheKey] ) { - start = $.now(); - this.cache[cacheKey] = this.api.get( { + start, + ajax = { action: 'query', prop: 'imageinfo', titles: file.getPrefixedDb(), @@ -57,8 +54,16 @@ iiurlwidth: width, // mw.Api will omit null/undefined parameters iiurlheight: height, format: 'json' - } ).then( function( data ) { - provider.performance.recordEntry( 'thumbnailinfo', $.now() - start ); + }; + + if ( !this.cache[cacheKey] ) { + start = $.now(); + this.cache[cacheKey] = this.api.get( ajax ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'thumbnailinfo', + $.now() - start, + provider.makeURL( ajax ), + xhr ); + return provider.getQueryPage( file, data ); } ).then( function( page ) { if ( page.imageinfo && page.imageinfo[0] ) { diff --git a/resources/mmv/provider/mmv.provider.UserInfo.js b/resources/mmv/provider/mmv.provider.UserInfo.js index 590c110..82540e3 100644 --- a/resources/mmv/provider/mmv.provider.UserInfo.js +++ b/resources/mmv/provider/mmv.provider.UserInfo.js @@ -41,7 +41,13 @@ var provider = this, ajaxOptions = {}, cacheKey = username, - start; + start, + ajax = { + action: 'query', + list: 'users', + ususers: username, + usprop: 'gender' + }; // For local/shared db images the user should be visible via a local API request, // maybe. (In practice we have Wikimedia users who haven't completed the SUL @@ -56,13 +62,12 @@ if ( !this.cache[cacheKey] ) { start = $.now(); - this.cache[cacheKey] = this.api.get( { - action: 'query', - list: 'users', - ususers: username, - usprop: 'gender' - }, ajaxOptions ).then( function( data ) { - provider.performance.recordEntry( 'userinfo', $.now() - start ); + this.cache[cacheKey] = this.api.get( ajax, ajaxOptions ).then( function( data, xhr ) { + provider.performance.recordEntryDelayed( 'userinfo', + $.now() - start, + provider.makeURL( ajax, ajaxOptions ), + xhr ); + return provider.getQueryField( 'users', data ); } ).then( function( users ) { if ( users[0] && users[0].name && users[0].gender ) { diff --git a/tests/qunit/provider/mmv.provider.Api.test.js b/tests/qunit/provider/mmv.provider.Api.test.js index 1602e8a..9285bb2 100644 --- a/tests/qunit/provider/mmv.provider.Api.test.js +++ b/tests/qunit/provider/mmv.provider.Api.test.js @@ -28,7 +28,7 @@ assert.ok( ApiProviderWithNoOptions ); } ); - QUnit.test( 'getErrorMessage test', 2, function ( assert ) { + QUnit.test( 'getErrorMessage', 2, function ( assert ) { var api = { get: function() {} }, apiProvider = new mw.mmv.provider.Api( api ), errorMessage; @@ -47,7 +47,7 @@ assert.strictEqual( apiProvider.getErrorMessage( {} ), 'unknown error', 'missing error message is handled'); } ); - QUnit.test( 'getNormalizedTitle test', 3, function ( assert ) { + QUnit.test( 'getNormalizedTitle', 3, function ( assert ) { var api = { get: function() {} }, apiProvider = new mw.mmv.provider.Api( api ), title = new mw.Title( 'Image:Stuff.jpg' ), @@ -81,7 +81,7 @@ assert.strictEqual( normalizedTitle.getPrefixedDb(), 'File:Stuff.jpg', 'normalization happens' ); } ); - QUnit.test( 'getQueryField test', 3, function ( assert ) { + QUnit.test( 'getQueryField', 3, function ( assert ) { var api = { get: function() {} }, apiProvider = new mw.mmv.provider.Api( api ), data; @@ -116,7 +116,7 @@ } ); } ); - QUnit.test( 'getQueryPage test', 6, function ( assert ) { + QUnit.test( 'getQueryPage', 6, function ( assert ) { var api = { get: function() {} }, apiProvider = new mw.mmv.provider.Api( api ), title = new mw.Title( 'File:Stuff.jpg' ), @@ -174,4 +174,30 @@ QUnit.start(); } ); } ); + + QUnit.test( 'makeURL', 5, function ( assert ) { + var api = { get: function() {} }, + apiProvider = new mw.mmv.provider.Api( api ), + localAPIURL = location.protocol + '//' + location.host + mw.util.wikiScript( 'api' ); + + assert.strictEqual( apiProvider.makeURL( {}, { url: 'foo' } ), + 'foo?action=query&format=json', + 'URLs match when using custom URL'); + + assert.strictEqual( apiProvider.makeURL( {}, {} ), + localAPIURL + '?action=query&format=json', + 'URLs match when passing empty options object'); + + assert.strictEqual( apiProvider.makeURL( { foo: 'bar', baz: 'boz' } ), + localAPIURL + '?action=query&format=json&foo=bar&baz=boz', + 'URLs match with basic parameters'); + + assert.strictEqual( apiProvider.makeURL( { foo: [ 0, 2, 1 ], baz: 'boz' } ), + localAPIURL + '?action=query&format=json&foo[]=0&foo[]=2&foo[]=1&baz=boz', + 'URLs match with array parameter'); + + assert.strictEqual( apiProvider.makeURL( { a: 1, format: 'jsonp', b: 2 } ), + localAPIURL + '?action=query&format=jsonp&a=1&b=2', + 'URLs match with overriding default parameter'); + } ); }( mediaWiki ) ); -- To view, visit https://gerrit.wikimedia.org/r/113344 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I2cc730727d30fb4f87a4fbe81725d6859690589b Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/MultimediaViewer Gerrit-Branch: master Gerrit-Owner: Gilles <gdu...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits