2017-03-02 16:58:40 +00:00
/ *
HTML5 Speedtest v4 . 0
by Federico Dossena
https : //github.com/adolfintel/speedtest/
GNU LGPLv3 License
* /
//data reported to main thread
var testStatus = 0 , //0=not started, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort/error
dlStatus = "" , //download speed in megabit/s with 2 decimal digits
ulStatus = "" , //upload speed in megabit/s with 2 decimal digits
pingStatus = "" , //ping in milliseconds with 2 decimal digits
jitterStatus = "" , //jitter in milliseconds with 2 decimal digits
clientIp = "" ; //client's IP address as reported by getIP.php
//test settings. can be overridden by sending specific values with the start command
var settings = {
time _ul : 15 , //duration of upload test in seconds
time _dl : 15 , //duration of download test in seconds
count _ping : 35 , //number of pings to perform in ping test
url _dl : "garbage.php" , //path to a large file or garbage.php, used for download test. must be relative to this js file
url _ul : "empty.dat" , //path to an empty file, used for upload test. must be relative to this js file
url _ping : "empty.dat" , //path to an empty file, used for ping test. must be relative to this js file
url _getIp : "getIP.php" , //path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
xhr _dlMultistream : 10 , //number of download streams to use (can be different if enable_quirks is active)
xhr _ulMultistream : 3 , //number of upload streams to use (can be different if enable_quirks is active)
xhr _dlUseBlob : false , //if set to true, it reduces ram usage but uses the hard drive (useful with large garbagePhp_chunkSize and/or high xhr_dlMultistream)
garbagePhp _chunkSize : 20 , //size of chunks sent by garbage.php (can be different if enable_quirks is active)
enable _quirks : true , //enable quirks for specific browsers. currently it overrides settings to optimize for specific browsers, unless they are already being overridden with the start command
allow _fetchAPI : false , //enables Fetch API. currently disabled because it leaks memory like no tomorrow
force _fetchAPI : false //when Fetch API is enabled, it will force usage on every browser that supports it
} ;
var xhr = null , //array of currently active xhr requests
interval = null ; //timer used in tests
/ *
when set to true ( automatically ) the download test will use the fetch api instead of xhr .
fetch api is used if
- allow _fetchAPI is true AND
- ( we 're on chrome that supports fetch api AND enable_quirks is true) OR (we' re on any browser that supports fetch api AND force _fetchAPI is true )
* /
var useFetchAPI = false ;
/ *
listener for commands from main thread to this worker .
commands :
- status : returns the current status as a string of values spearated by a semicolon ( ; ) in this order : testStatus ; dlStatus ; ulStatus ; pingStatus ; clientIp ; jitterStatus
- abort : aborts the current test
- start : starts the test . optionally , settings can be passed as JSON .
example : start { "time_ul" : "10" , "time_dl" : "10" , "count_ping" : "50" }
* /
2016-10-09 07:13:36 +00:00
this . addEventListener ( 'message' , function ( e ) {
2016-10-23 17:47:55 +00:00
var params = e . data . split ( " " ) ;
2017-03-02 16:58:40 +00:00
if ( params [ 0 ] == "status" ) { //return status
postMessage ( testStatus + ";" + dlStatus + ";" + ulStatus + ";" + pingStatus + ";" + clientIp + ";" + jitterStatus ) ;
2016-10-23 17:47:55 +00:00
}
2017-03-02 16:58:40 +00:00
if ( params [ 0 ] == "start" && testStatus == 0 ) { //start new test
2016-11-28 20:09:12 +00:00
testStatus = 1 ;
try {
2017-03-02 16:58:40 +00:00
//parse settings, if present
2016-11-28 20:09:12 +00:00
var s = JSON . parse ( e . data . substring ( 5 ) ) ;
2017-03-02 16:58:40 +00:00
if ( typeof s . url _dl != "undefined" ) settings . url _dl = s . url _dl ; //download url
if ( typeof s . url _ul != "undefined" ) settings . url _ul = s . url _ul ; //upload url
if ( typeof s . url _ping != "undefined" ) settings . url _ping = s . url _ping ; //ping url
if ( typeof s . url _getIp != "undefined" ) settings . url _getIp = s . url _getIp ; //url to getIP.php
if ( typeof s . time _dl != "undefined" ) settings . time _dl = s . time _dl ; //duration of download test
if ( typeof s . time _ul != "undefined" ) settings . time _ul = s . time _ul ; //duration of upload test
if ( typeof s . enable _quirks != "undefined" ) settings . enable _quirks = s . enable _quirks ; //enable quirks or not
if ( typeof s . allow _fetchAPI != "undefined" ) settings . allow _fetchAPI = s . allow _fetchAPI ; //allows fetch api to be used if supported
//quirks for specific browsers. more may be added in future releases
if ( settings . enable _quirks ) {
var ua = navigator . userAgent ;
if ( /Firefox.(\d+\.\d+)/i . test ( ua ) ) {
//ff more precise with 1 upload stream
settings . xhr _ulMultistream = 1 ;
}
if ( /Edge.(\d+\.\d+)/i . test ( ua ) ) {
//edge more precise with 3 download streams
settings . xhr _dlMultistream = 3 ;
}
if ( ( /Safari.(\d+)/i . test ( ua ) ) && ! ( /Chrome.(\d+)/i . test ( ua ) ) ) {
//safari more precise with 10 upload streams and 5mb chunks for download test
settings . xhr _ulMultistream = 10 ;
settings . garbagePhp _chunkSize = 5 ;
}
if ( /Chrome.(\d+)/i . test ( ua ) && ( ! ! self . fetch ) ) {
//chrome can't handle large xhr very well, use fetch api if available and allowed
if ( settings . allow _fetchAPI ) useFetchAPI = true ;
//chrome more precise with 5 streams
settings . xhr _dlMultistream = 5 ;
}
}
if ( typeof s . count _ping != "undefined" ) settings . count _ping = s . count _ping ; //number of pings for ping test
if ( typeof s . xhr _dlMultistream != "undefined" ) settings . xhr _dlMultistream = s . xhr _dlMultistream ; //number of download streams
if ( typeof s . xhr _ulMultistream != "undefined" ) settings . xhr _ulMultistream = s . xhr _ulMultistream ; //number of upload streams
if ( typeof s . xhr _dlUseBlob != "undefined" ) settings . xhr _dlUseBlob = s . xhr _dlUseBlob ; //use blob for download test
if ( typeof s . garbagePhp _chunkSize != "undefined" ) settings . garbagePhp _chunkSize = s . garbagePhp _chunkSize ; //size of garbage.php chunks
if ( typeof s . force _fetchAPI != "undefined" ) settings . force _fetchAPI = s . force _fetchAPI ; //use fetch api on all browsers that support it if enabled
if ( settings . allow _fetchAPI && settings . force _fetchAPI && ( ! ! self . fetch ) ) useFetchAPI = true ;
2017-03-03 12:30:00 +00:00
} catch ( e ) { }
2017-03-02 16:58:40 +00:00
//run the tests
console . log ( settings ) ;
console . log ( "Fetch API: " + useFetchAPI ) ;
getIp ( function ( ) { dlTest ( function ( ) { testStatus = 2 ; pingTest ( function ( ) { testStatus = 3 ; ulTest ( function ( ) { testStatus = 4 ; } ) ; } ) ; } ) } ) ;
2016-10-23 17:47:55 +00:00
}
2017-03-02 16:58:40 +00:00
if ( params [ 0 ] == "abort" ) { //abort command
clearRequests ( ) ; //stop all xhr activity
if ( interval ) clearInterval ( interval ) ; //clear timer if present
testStatus = 5 ; dlStatus = "" ; ulStatus = "" ; pingStatus = "" ; jitterStatus = "" ; //set test as aborted
2016-10-23 17:47:55 +00:00
}
2016-10-09 07:13:36 +00:00
} ) ;
2017-03-02 16:58:40 +00:00
//stops all XHR activity, aggressively
function clearRequests ( ) {
if ( xhr ) {
for ( var i = 0 ; i < xhr . length ; i ++ ) {
if ( useFetchAPI ) try { xhr [ i ] . cancelRequested = true ; } catch ( e ) { }
try { xhr [ i ] . onprogress = null ; xhr [ i ] . onload = null ; xhr [ i ] . onerror = null ; } catch ( e ) { }
try { xhr [ i ] . upload . onprogress = null ; xhr [ i ] . upload . onload = null ; xhr [ i ] . upload . onerror = null ; } catch ( e ) { }
try { xhr [ i ] . abort ( ) ; } catch ( e ) { }
try { delete ( xhr [ i ] ) ; } catch ( e ) { }
}
xhr = null ;
}
}
//gets client's IP using url_getIp, then calls the done function
2017-02-26 11:15:51 +00:00
function getIp ( done ) {
xhr = new XMLHttpRequest ( ) ;
xhr . onload = function ( ) {
clientIp = xhr . responseText ;
done ( ) ;
}
xhr . onerror = function ( ) {
done ( ) ;
}
xhr . open ( "GET" , settings . url _getIp + "?r=" + Math . random ( ) , true ) ;
xhr . send ( ) ;
}
2017-03-02 16:58:40 +00:00
//download test, calls done function when it's over
var dlCalled = false ; //used to prevent multiple accidental calls to dlTest
2016-10-22 13:39:16 +00:00
function dlTest ( done ) {
2017-03-02 16:58:40 +00:00
if ( dlCalled ) return ; else dlCalled = true ; //dlTest already called?
var totLoaded = 0.0 , //total number of loaded bytes
startT = new Date ( ) . getTime ( ) , //timestamp when test was started
failed = false ; //set to true if a stream fails
xhr = [ ] ;
//function to create a download stream
var testStream = function ( i , delay ) {
setTimeout ( function ( ) { //delay creation of a stream slightly so that the new stream is completely detached from the one that created it
if ( testStatus != 1 ) return ; //delayed stream ended up starting after the end of the download test
if ( useFetchAPI ) {
xhr [ i ] = fetch ( settings . url _dl + "?r=" + Math . random ( ) + "&ckSize=" + settings . garbagePhp _chunkSize ) . then ( function ( response ) {
var reader = response . body . getReader ( ) ;
var consume = function ( ) {
return reader . read ( ) . then ( function ( result ) {
if ( result . done ) testStream ( i ) ; else {
totLoaded += result . value . length ;
if ( xhr [ i ] . canelRequested ) reader . cancel ( ) ;
}
return consume ( ) ;
} . bind ( this ) ) ;
} . bind ( this ) ;
return consume ( ) ;
} . bind ( this ) ) ;
} else {
var prevLoaded = 0 ; //number of bytes loaded last time onprogress was called
var x = new XMLHttpRequest ( ) ;
xhr [ i ] = x ;
xhr [ i ] . onprogress = function ( event ) {
if ( testStatus != 1 ) { try { x . abort ( ) ; } catch ( e ) { } } //just in case this XHR is still running after the download test
//progress event, add number of new loaded bytes to totLoaded
var loadDiff = event . loaded <= 0 ? 0 : ( event . loaded - prevLoaded ) ;
if ( isNaN ( loadDiff ) || ! isFinite ( loadDiff ) || loadDiff < 0 ) return ; //just in case
totLoaded += loadDiff ;
prevLoaded = event . loaded ;
} . bind ( this ) ;
xhr [ i ] . onload = function ( ) {
//the large file has been loaded entirely, start again
testStream ( i , 0 ) ;
} . bind ( this ) ;
xhr [ i ] . onerror = function ( ) {
//error, abort
failed = true ;
try { xhr [ i ] . abort ( ) ; } catch ( e ) { }
delete ( xhr [ i ] ) ;
} . bind ( this ) ;
//send xhr
2017-03-03 12:30:00 +00:00
try { if ( settings . xhr _dlUseBlob ) xhr [ i ] . responseType = 'blob' ; else xhr [ i ] . responseType = 'arraybuffer' ; } catch ( e ) { }
2017-03-02 16:58:40 +00:00
xhr [ i ] . open ( "GET" , settings . url _dl + "?r=" + Math . random ( ) + "&ckSize=" + settings . garbagePhp _chunkSize , true ) ; //random string to prevent caching
xhr [ i ] . send ( ) ;
}
} . bind ( this ) , 1 + delay ) ;
} . bind ( this ) ;
//open streams
for ( var i = 0 ; i < settings . xhr _dlMultistream ; i ++ ) {
testStream ( i , 100 * i ) ;
}
//every 200ms, update dlStatus
interval = setInterval ( function ( ) {
var t = new Date ( ) . getTime ( ) - startT ;
if ( t < 200 ) return ;
var speed = totLoaded / ( t / 1000.0 ) ;
dlStatus = ( ( speed * 8 ) / 925000.0 ) . toFixed ( 2 ) ; //925000 instead of 1048576 to account for overhead
if ( ( t / 1000.0 ) > settings . time _dl || failed ) { //test is over, stop streams and timer
if ( failed || isNaN ( dlStatus ) ) dlStatus = "Fail" ;
clearRequests ( ) ;
clearInterval ( interval ) ;
done ( ) ;
}
} . bind ( this ) , 200 ) ;
2016-10-09 07:13:36 +00:00
}
2017-03-02 16:58:40 +00:00
//upload test, calls done function whent it's over
//garbage data for upload test (1mb of random bytes repeated 20 times, for a total of 20mb)
var r = new ArrayBuffer ( 1048576 ) ;
try { r = new Float32Array ( r ) ; for ( var i = 0 ; i < r . length ; i ++ ) r [ i ] = Math . random ( ) ; } catch ( e ) { }
var req = [ ] ;
for ( var i = 0 ; i < 20 ; i ++ ) req . push ( r ) ;
req = new Blob ( req ) ;
var ulCalled = false ; //used to prevent multiple accidental calls to ulTest
2016-10-22 13:39:16 +00:00
function ulTest ( done ) {
2017-03-02 16:58:40 +00:00
if ( ulCalled ) return ; else ulCalled = true ; //ulTest already called?
var totLoaded = 0.0 , //total number of transmitted bytes
startT = new Date ( ) . getTime ( ) , //timestamp when test was started
failed = false ; //set to true if a stream fails
xhr = [ ] ;
//function to create an upload stream
var testStream = function ( i , delay ) {
setTimeout ( function ( ) { //delay creation of a stream slightly so that the new stream is completely detached from the one that created it
if ( testStatus != 3 ) return ; //delayed stream ended up starting after the end of the upload test
var prevLoaded = 0 ; //number of bytes transmitted last time onprogress was called
var x = new XMLHttpRequest ( ) ;
xhr [ i ] = x ;
xhr [ i ] . upload . onprogress = function ( event ) {
if ( testStatus != 3 ) { try { x . abort ( ) ; } catch ( e ) { } } //just in case this XHR is still running after the upload test
//progress event, add number of new loaded bytes to totLoaded
var loadDiff = event . loaded <= 0 ? 0 : ( event . loaded - prevLoaded ) ;
if ( isNaN ( loadDiff ) || ! isFinite ( loadDiff ) || loadDiff < 0 ) return ; //just in case
totLoaded += loadDiff ;
prevLoaded = event . loaded ;
} . bind ( this ) ;
xhr [ i ] . upload . onload = function ( ) {
//this stream sent all 20mb of garbage data, start again
testStream ( i , 0 ) ;
} . bind ( this ) ;
xhr [ i ] . upload . onerror = function ( ) {
//error, abort
failed = true ;
try { xhr [ i ] . abort ( ) ; } catch ( e ) { }
delete ( xhr [ i ] ) ;
} . bind ( this ) ;
//send xhr
xhr [ i ] . open ( "POST" , settings . url _ul + "?r=" + Math . random ( ) , true ) ; //random string to prevent caching
xhr [ i ] . setRequestHeader ( 'Content-Encoding' , 'identity' ) ; //disable compression (some browsers may refuse it, but data is incompressible anyway)
xhr [ i ] . send ( req ) ;
} . bind ( this ) , 1 ) ;
} . bind ( this ) ;
//open streams
for ( var i = 0 ; i < settings . xhr _ulMultistream ; i ++ ) {
testStream ( i , 100 * i ) ;
}
//every 200ms, update ulStatus
interval = setInterval ( function ( ) {
var t = new Date ( ) . getTime ( ) - startT ;
if ( t < 200 ) return ;
var speed = totLoaded / ( t / 1000.0 ) ;
ulStatus = ( ( speed * 8 ) / 925000.0 ) . toFixed ( 2 ) ; //925000 instead of 1048576 to account for overhead
if ( ( t / 1000.0 ) > settings . time _ul || failed ) { //test is over, stop streams and timer
if ( failed || isNaN ( ulStatus ) ) ulStatus = "Fail" ;
clearRequests ( ) ;
clearInterval ( interval ) ;
done ( ) ;
}
} . bind ( this ) , 200 ) ;
2016-10-09 07:13:36 +00:00
}
2017-03-02 16:58:40 +00:00
//ping+jitter test, function done is called when it's over
var ptCalled = false ; //used to prevent multiple accidental calls to pingTest
2016-10-22 13:39:16 +00:00
function pingTest ( done ) {
2017-03-02 16:58:40 +00:00
if ( ptCalled ) return ; else ptCalled = true ; //pingTest already called?
var prevT = null , //last time a pong was received
ping = 0.0 , //current ping value
jitter = 0.0 , //current jitter value
i = 0 , //counter of pongs received
prevInstspd = 0 ; //last ping time, used for jitter calculation
xhr = [ ] ;
//ping function
2016-10-22 13:39:16 +00:00
var doPing = function ( ) {
prevT = new Date ( ) . getTime ( ) ;
2017-03-02 16:58:40 +00:00
xhr [ 0 ] = new XMLHttpRequest ( ) ;
xhr [ 0 ] . onload = function ( ) {
//pong
2016-10-22 13:39:16 +00:00
if ( i == 0 ) {
2017-03-02 16:58:40 +00:00
prevT = new Date ( ) . getTime ( ) ; //first pong
2016-10-22 13:39:16 +00:00
} else {
2017-02-05 08:51:15 +00:00
var instspd = ( new Date ( ) . getTime ( ) - prevT ) / 2 ;
2017-03-02 16:58:40 +00:00
var instjitter = Math . abs ( instspd - prevInstspd ) ;
if ( i == 1 ) ping = instspd ; /*first ping, can't tell jiutter yet*/ else {
ping = ping * 0.9 + instspd * 0.1 ; //ping, weighted average
jitter = instjitter > jitter ? ( jitter * 0.2 + instjitter * 0.8 ) : ( jitter * 0.9 + instjitter * 0.1 ) ; //update jitter, weighted average. spikes in ping values are given more weight.
}
prevInstspd = instspd ;
2016-10-22 13:39:16 +00:00
}
pingStatus = ping . toFixed ( 2 ) ;
2017-03-02 16:58:40 +00:00
jitterStatus = jitter . toFixed ( 2 ) ;
2016-10-22 13:39:16 +00:00
i ++ ;
2017-03-02 16:58:40 +00:00
if ( i < settings . count _ping ) doPing ( ) ; else done ( ) ; //more pings to do?
2016-10-22 13:39:16 +00:00
} . bind ( this ) ;
2017-03-02 16:58:40 +00:00
xhr [ 0 ] . onerror = function ( ) {
//a ping failed, cancel test
2016-10-22 13:39:16 +00:00
pingStatus = "Fail" ;
2017-03-02 16:58:40 +00:00
jitterStatus = "Fail" ;
clearRequests ( ) ;
2016-10-22 13:39:16 +00:00
done ( ) ;
} . bind ( this ) ;
2017-03-02 16:58:40 +00:00
//sent xhr
xhr [ 0 ] . open ( "GET" , settings . url _ping + "?r=" + Math . random ( ) , true ) ; //random string to prevent caching
xhr [ 0 ] . send ( ) ;
2016-10-22 13:39:16 +00:00
} . bind ( this ) ;
2017-03-02 16:58:40 +00:00
doPing ( ) ; //start first ping
}