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:
Gilles Dubuc 2014-02-13 16:48:02 +01:00 committed by Mark Holmquist
parent 7afbc5ce92
commit 8a8d74f01d
3 changed files with 149 additions and 28 deletions

View file

@ -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 ) );

View file

@ -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 ) );

View file

@ -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 ) );