mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/MultimediaViewer
synced 2024-11-12 09:27:36 +00:00
Avoid double requests when measuring performance of image load
Change-Id: Ib5ec4c3e4e4a410a6ee520b11bf025d7447cb542 Mingle: https://wikimedia.mingle.thoughtworks.com/projects/multimedia/cards/207
This commit is contained in:
parent
7afbc5ce92
commit
8a8d74f01d
|
@ -28,6 +28,8 @@
|
|||
|
||||
P = Performance.prototype;
|
||||
|
||||
P.delay = 1000;
|
||||
|
||||
/**
|
||||
* Global setup that should be done while the page loads
|
||||
*/
|
||||
|
@ -47,27 +49,41 @@
|
|||
* 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 ) {
|
||||
P.record = function ( type, url, responseType ) {
|
||||
var deferred = $.Deferred(),
|
||||
request,
|
||||
perf = this,
|
||||
start;
|
||||
|
||||
request = new XMLHttpRequest();
|
||||
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();
|
||||
request.open( 'GET', url, true );
|
||||
request.send();
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -103,6 +119,11 @@
|
|||
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 ) {
|
||||
|
@ -297,7 +318,7 @@
|
|||
// it hasn't been added yet at this point
|
||||
setTimeout( function() {
|
||||
perf.recordEntry( type, total, url, request );
|
||||
}, 0 );
|
||||
}, this.delay );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -390,8 +411,16 @@
|
|||
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 ) );
|
||||
|
|
|
@ -42,27 +42,78 @@
|
|||
* @param {string} url
|
||||
* @return {jQuery.Promise.<HTMLImageElement>} a promise which resolves to the image object
|
||||
*/
|
||||
Image.prototype.get = function( url ) {
|
||||
var img = new window.Image(),
|
||||
Image.prototype.get = function ( url ) {
|
||||
var deferred, start,
|
||||
img = new window.Image(),
|
||||
cacheKey = url,
|
||||
deferred;
|
||||
provider = this;
|
||||
|
||||
if ( !this.cache[cacheKey] ) {
|
||||
deferred = $.Deferred();
|
||||
this.cache[cacheKey] = deferred.promise();
|
||||
this.performance.record( 'image', url ).then( function() {
|
||||
|
||||
img.onload = function() {
|
||||
// Start is only defined for old browsers
|
||||
if ( start !== undefined ) {
|
||||
provider.performance.recordEntry( 'image', $.now() - start );
|
||||
}
|
||||
deferred.resolve( img );
|
||||
};
|
||||
|
||||
img.onerror = function() {
|
||||
deferred.reject( 'could not load image from ' + url );
|
||||
};
|
||||
|
||||
// We can only measure detailed image performance on browsers that are capable of
|
||||
// reading the XHR binary results as data URI. Otherwise loading the image as XHR
|
||||
// and then assigning the same URL to an image's src attribute
|
||||
// would cause a double request (won't hit browser cache).
|
||||
if ( this.browserSupportsBinaryOperations() ) {
|
||||
this.performance.record( 'image', url, 'arraybuffer' ).then( function( response ) {
|
||||
img.src = provider.binaryToDataURI( response );
|
||||
} );
|
||||
} else {
|
||||
// On old browsers we just do oldschool timing without details
|
||||
start = $.now();
|
||||
img.src = url;
|
||||
img.onload = function() {
|
||||
deferred.resolve( img );
|
||||
};
|
||||
img.onerror = function() {
|
||||
deferred.reject( 'could not load image from ' + url );
|
||||
};
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
return this.cache[cacheKey];
|
||||
};
|
||||
|
||||
/**
|
||||
* @method
|
||||
* Converts a binary image into a data URI
|
||||
* @param {string} binary Binary image
|
||||
* @returns {string} base64-encoded data URI representing the image
|
||||
*/
|
||||
Image.prototype.binaryToDataURI = function ( binary ) {
|
||||
var i,
|
||||
bytes = new Uint8Array( binary ),
|
||||
raw = '';
|
||||
|
||||
for ( i = 0; i < bytes.length; i++ ) {
|
||||
raw += String.fromCharCode( bytes[ i ] );
|
||||
}
|
||||
|
||||
// I've tested this on Firefox, Chrome, IE10, IE11, Safari, Opera
|
||||
// and for all of them we can get away with not giving the proper mime type
|
||||
// If we run into a browser where that's a problem, we'll need
|
||||
// to read the binary contents to determine the mime type
|
||||
return 'data:image;base64,' + btoa( raw );
|
||||
};
|
||||
|
||||
/**
|
||||
* @method
|
||||
* Checks if the browser is capable of converting binary content to a data URI
|
||||
* @returns {boolean}
|
||||
*/
|
||||
Image.prototype.browserSupportsBinaryOperations = function () {
|
||||
return window.btoa !== undefined &&
|
||||
window.Uint8Array !== undefined &&
|
||||
String.fromCharCode !== undefined;
|
||||
};
|
||||
|
||||
mw.mmv.provider.Image = Image;
|
||||
}( mediaWiki, jQuery ) );
|
||||
|
|
|
@ -16,6 +16,22 @@
|
|||
*/
|
||||
|
||||
( function ( mw, $ ) {
|
||||
var i,
|
||||
binary,
|
||||
dataURI = 'data:image;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0'
|
||||
+ 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH'
|
||||
+ '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC',
|
||||
hex = '89504e470d0a1a0a0000000d4948445200000010000000100103000000253d6d'
|
||||
+ '2200000006504c5445000000ffffffa5d99fdd0000003349444154789c63f8ff'
|
||||
+ '9fe1ff5f86ff9f190eb033dc3fcc707f32c3cdcd0c378d19ee1483d0bdcf0cf7'
|
||||
+ '8152cc0c0fc0e8ff7f0051861728ce5d9b500000000049454e44ae426082';
|
||||
|
||||
binary = new Uint8Array( hex.length / 2 );
|
||||
|
||||
for ( i = 0; i < hex.length; i += 2 ) {
|
||||
binary[ i / 2 ] = parseInt( hex.substr( i, 2 ), 16 );
|
||||
}
|
||||
|
||||
QUnit.module( 'mmv.provider.Image', QUnit.newMwEnvironment() );
|
||||
|
||||
QUnit.test( 'Image constructor sanity check', 1, function ( assert ) {
|
||||
|
@ -24,18 +40,37 @@
|
|||
assert.ok( imageProvider );
|
||||
} );
|
||||
|
||||
QUnit.asyncTest( 'Image load success test', 1, function ( assert ) {
|
||||
var imageProvider = new mw.mmv.provider.Image();
|
||||
imageProvider.performance.record = function() { return $.Deferred().resolve(); };
|
||||
QUnit.asyncTest( 'Image load success test', 5, function ( assert ) {
|
||||
var imageProvider = new mw.mmv.provider.Image(),
|
||||
oldPerformance = imageProvider.performance,
|
||||
fakeURL = 'fakeURL';
|
||||
|
||||
imageProvider.get(
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0'
|
||||
+ 'iAAAABlBMVEUAAAD///+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH'
|
||||
+ '8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC'
|
||||
).then( function( image ) {
|
||||
assert.ok( image instanceof HTMLImageElement,
|
||||
'success handler was called with the image element');
|
||||
QUnit.start();
|
||||
imageProvider.performance.delay = 0;
|
||||
|
||||
imageProvider.performance.newXHR = function () {
|
||||
return { readyState: 4,
|
||||
response: binary,
|
||||
send: function () { this.onreadystatechange(); },
|
||||
open: $.noop };
|
||||
};
|
||||
|
||||
imageProvider.performance.recordEntry = function ( type, total, url ) {
|
||||
assert.strictEqual( type, 'image', 'Type matches' );
|
||||
assert.ok( total < 10, 'Total is less than 10ms' );
|
||||
assert.strictEqual( url, fakeURL, 'URL matches' );
|
||||
|
||||
QUnit.start();
|
||||
|
||||
imageProvider.performance = oldPerformance;
|
||||
|
||||
return $.Deferred().resolve();
|
||||
};
|
||||
|
||||
imageProvider.get( fakeURL ).then( function( image ) {
|
||||
assert.ok( image instanceof HTMLImageElement,
|
||||
'success handler was called with the image element');
|
||||
|
||||
assert.strictEqual( image.src, dataURI );
|
||||
} );
|
||||
} );
|
||||
|
||||
|
@ -47,4 +82,10 @@
|
|||
QUnit.start();
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'binaryToDataURI', 1, function ( assert ) {
|
||||
var imageProvider = new mw.mmv.provider.Image();
|
||||
|
||||
assert.strictEqual( imageProvider.binaryToDataURI( binary ), dataURI, 'Binary is correctly converted to data URI' );
|
||||
} );
|
||||
}( mediaWiki, jQuery ) );
|
||||
|
|
Loading…
Reference in a new issue