2017-03-02 16:58:40 +00:00
/ *
2017-06-22 06:33:39 +00:00
HTML5 Speedtest v4 . 2.7
2017-03-02 16:58:40 +00:00
by Federico Dossena
https : //github.com/adolfintel/speedtest/
GNU LGPLv3 License
* /
2017-05-15 10:02:32 +00:00
// 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
var dlStatus = '' // download speed in megabit/s with 2 decimal digits
var ulStatus = '' // upload speed in megabit/s with 2 decimal digits
var pingStatus = '' // ping in milliseconds with 2 decimal digits
var jitterStatus = '' // jitter in milliseconds with 2 decimal digits
var clientIp = '' // client's IP address as reported by getIP.php
2017-03-02 16:58:40 +00:00
2017-05-15 10:02:32 +00:00
// 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
2017-06-16 04:59:11 +00:00
time _ulGraceTime : 3 , //time to wait in seconds before actually measuring ul speed (wait for buffers to fill)
time _dlGraceTime : 1.5 , //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase)
2017-05-15 10:02:32 +00:00
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
2017-05-15 15:32:46 +00:00
url _ul : 'empty.php' , // path to an empty file, used for upload test. must be relative to this js file
url _ping : 'empty.php' , // path to an empty file, used for ping test. must be relative to this js file
2017-05-15 10:02:32 +00:00
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)
2017-06-13 12:20:05 +00:00
xhr _ignoreErrors : 1 , // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors
2017-05-15 10:02:32 +00:00
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
2017-06-15 05:02:35 +00:00
overheadCompensationFactor : 1048576 / 925000 //compensation for HTTP+TCP+IP+ETH overhead. 925000 is how much data is actually carried over 1048576 (1mb) bytes downloaded/uploaded. This default value assumes HTTP+TCP+IPv4+ETH with typical MTUs over the Internet. You may want to change this if you're going through your local network with a different MTU or if you're going over IPv6 (see doc.md for some other values)
2017-05-15 10:02:32 +00:00
}
var xhr = null // array of currently active xhr requests
var interval = null // timer used in tests
2017-03-02 16:58:40 +00:00
/ *
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 )
* /
2017-05-15 10:02:32 +00:00
var useFetchAPI = false
2017-06-16 04:59:11 +00:00
/ *
this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator
* /
function url _sep ( url ) { return url . match ( /\?/ ) ? '&' : '?' ; }
2017-03-02 16:58:40 +00:00
/ *
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
2017-05-15 10:02:32 +00:00
- start : starts the test . optionally , settings can be passed as JSON .
2017-03-02 16:58:40 +00:00
example : start { "time_ul" : "10" , "time_dl" : "10" , "count_ping" : "50" }
* /
2017-05-15 10:02:32 +00:00
this . addEventListener ( 'message' , function ( e ) {
var params = e . data . split ( ' ' )
if ( params [ 0 ] === 'status' ) { // return status
postMessage ( testStatus + ';' + dlStatus + ';' + ulStatus + ';' + pingStatus + ';' + clientIp + ';' + jitterStatus )
}
if ( params [ 0 ] === 'start' && testStatus === 0 ) { // start new test
testStatus = 1
try {
// parse settings, if present
2017-06-19 15:19:58 +00:00
var s = { }
try {
var ss = e . data . substring ( 5 ) ;
if ( ss ) s = JSON . parse ( ss ) ;
} catch ( e ) { console . warn ( "Error parsing custom settings JSON. Please check your syntax" ) ; }
2017-05-15 10:02:32 +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
// 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 ( /Chrome.(\d+)/i . test ( ua ) && ( ! ! self . fetch ) ) {
// 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
2017-06-15 05:02:35 +00:00
if ( typeof s . xhr _ignoreErrors !== 'undefined' ) settings . xhr _ignoreErrors = s . xhr _ignoreErrors // what to do in case of errors during the test
2017-05-15 10:02:32 +00:00
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
2017-06-16 04:59:11 +00:00
if ( typeof s . time _dlGraceTime !== 'undefined' ) settings . time _dlGraceTime = s . time _dlGraceTime // dl test grace time before measuring
if ( typeof s . time _ulGraceTime !== 'undefined' ) settings . time _ulGraceTime = s . time _ulGraceTime // ul test grace time before measuring
2017-06-15 05:02:35 +00:00
if ( typeof s . overheadCompensationFactor !== 'undefined' ) settings . overheadCompensationFactor = s . overheadCompensationFactor //custom overhead compensation factor (default assumes HTTP+TCP+IP+ETH with typical MTUs)
2017-06-17 12:45:03 +00:00
} catch ( e ) { console . warn ( "Possible error in custom test settings. Some settings may not be applied. Exception: " + e ) }
2017-05-15 10:02:32 +00:00
// run the tests
console . log ( settings )
getIp ( function ( ) { dlTest ( function ( ) { testStatus = 2 ; pingTest ( function ( ) { testStatus = 3 ; ulTest ( function ( ) { testStatus = 4 } ) } ) } ) } )
}
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
}
} )
// 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
}
2017-03-02 16:58:40 +00:00
}
2017-05-15 10:02:32 +00:00
// gets client's IP using url_getIp, then calls the done function
function getIp ( done ) {
xhr = new XMLHttpRequest ( )
xhr . onload = function ( ) {
clientIp = xhr . responseText
done ( )
}
xhr . onerror = function ( ) {
done ( )
}
2017-06-16 04:59:11 +00:00
xhr . open ( 'GET' , settings . url _getIp + url _sep ( settings . url _getIp ) + 'r=' + Math . random ( ) , true )
2017-05-15 10:02:32 +00:00
xhr . send ( )
2017-02-26 11:15:51 +00:00
}
2017-05-15 10:02:32 +00:00
// download test, calls done function when it's over
var dlCalled = false // used to prevent multiple accidental calls to dlTest
function dlTest ( done ) {
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
2017-06-16 04:59:11 +00:00
graceTimeDone = false , //set to true after the grace time is past
2017-05-15 10:02:32 +00:00
failed = false // set to true if a stream fails
xhr = [ ]
// function to create a download stream. streams are slightly delayed so that they will not end at the same time
var testStream = function ( i , delay ) {
setTimeout ( function ( ) {
if ( testStatus !== 1 ) return // delayed stream ended up starting after the end of the download test
2017-06-22 06:33:39 +00:00
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
try { xhr [ i ] . abort ( ) } catch ( e ) { } // reset the stream data to empty ram
testStream ( i , 0 )
} . bind ( this )
xhr [ i ] . onerror = function ( ) {
// error
if ( settings . xhr _ignoreErrors === 0 ) failed = true //abort
try { xhr [ i ] . abort ( ) } catch ( e ) { }
delete ( xhr [ i ] )
if ( settings . xhr _ignoreErrors === 1 ) testStream ( i , 100 ) //restart stream after 100ms
} . bind ( this )
// send xhr
try { if ( settings . xhr _dlUseBlob ) xhr [ i ] . responseType = 'blob' ; else xhr [ i ] . responseType = 'arraybuffer' } catch ( e ) { }
xhr [ i ] . open ( 'GET' , settings . url _dl + url _sep ( settings . url _dl ) + 'r=' + Math . random ( ) + '&ckSize=' + settings . garbagePhp _chunkSize , true ) // random string to prevent caching
xhr [ i ] . send ( )
2017-05-15 10:02:32 +00:00
} . 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
2017-06-16 04:59:11 +00:00
if ( ! graceTimeDone ) {
if ( t > 1000 * settings . time _dlGraceTime ) {
if ( totLoaded > 0 ) { // if the connection is so slow that we didn't get a single chunk yet, do not reset
startT = new Date ( ) . getTime ( )
totLoaded = 0.0 ;
}
graceTimeDone = true ;
}
} else {
var speed = totLoaded / ( t / 1000.0 )
dlStatus = ( ( speed * 8 * settings . overheadCompensationFactor ) / 1048576 ) . toFixed ( 2 ) // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 to go to megabits/s
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 ( )
}
2017-05-15 10:02:32 +00:00
}
} . bind ( this ) , 200 )
2016-10-09 07:13:36 +00:00
}
2017-05-15 10:02:32 +00:00
// upload test, calls done function whent it's over
// garbage data for upload test
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 = [ ]
var reqsmall = [ ]
for ( var i = 0 ; i < 20 ; i ++ ) req . push ( r )
req = new Blob ( req )
r = new ArrayBuffer ( 262144 )
try { r = new Float32Array ( r ) ; for ( var i = 0 ; i < r . length ; i ++ ) r [ i ] = Math . random ( ) } catch ( e ) { }
reqsmall . push ( r )
reqsmall = new Blob ( reqsmall )
var ulCalled = false // used to prevent multiple accidental calls to ulTest
function ulTest ( done ) {
if ( ulCalled ) return ; else ulCalled = true // ulTest already called?
2017-06-16 04:59:11 +00:00
var totLoaded = 0.0 , // total number of transmitted bytes
startT = new Date ( ) . getTime ( ) , // timestamp when test was started
graceTimeDone = false , //set to true after the grace time is past
failed = false // set to true if a stream fails
2017-05-15 10:02:32 +00:00
xhr = [ ]
// function to create an upload stream. streams are slightly delayed so that they will not end at the same time
var testStream = function ( i , delay ) {
setTimeout ( function ( ) {
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
var ie11workaround
try {
xhr [ i ] . upload . onprogress
ie11workaround = false
} catch ( e ) {
ie11workaround = true
}
if ( ie11workaround ) {
// IE11 workarond: xhr.upload does not work properly, therefore we send a bunch of small 256k requests and use the onload event as progress. This is not precise, especially on fast connections
xhr [ i ] . onload = function ( ) {
totLoaded += 262144
testStream ( i , 0 )
}
xhr [ i ] . onerror = function ( ) {
// error, abort
2017-06-13 12:20:05 +00:00
if ( settings . xhr _ignoreErrors === 0 ) failed = true //abort
2017-05-15 10:02:32 +00:00
try { xhr [ i ] . abort ( ) } catch ( e ) { }
delete ( xhr [ i ] )
2017-06-15 05:25:26 +00:00
if ( settings . xhr _ignoreErrors === 1 ) testStream ( i , 100 ) ; //restart stream after 100ms
2017-05-15 10:02:32 +00:00
}
2017-06-16 04:59:11 +00:00
xhr [ i ] . open ( 'POST' , settings . url _ul + url _sep ( settings . url _ul ) + 'r=' + Math . random ( ) , true ) // random string to prevent caching
2017-05-15 10:02:32 +00:00
xhr [ i ] . setRequestHeader ( 'Content-Encoding' , 'identity' ) // disable compression (some browsers may refuse it, but data is incompressible anyway)
xhr [ i ] . send ( reqsmall )
} else {
// REGULAR version, no workaround
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 the garbage data, start again
testStream ( i , 0 )
} . bind ( this )
xhr [ i ] . upload . onerror = function ( ) {
2017-06-13 12:20:05 +00:00
if ( settings . xhr _ignoreErrors === 0 ) failed = true //abort
2017-05-15 10:02:32 +00:00
try { xhr [ i ] . abort ( ) } catch ( e ) { }
delete ( xhr [ i ] )
2017-06-13 12:20:05 +00:00
if ( settings . xhr _ignoreErrors === 1 ) testStream ( i , 100 ) //restart stream after 100ms
2017-05-15 10:02:32 +00:00
} . bind ( this )
// send xhr
2017-06-16 04:59:11 +00:00
xhr [ i ] . open ( 'POST' , settings . url _ul + url _sep ( settings . url _ul ) + 'r=' + Math . random ( ) , true ) // random string to prevent caching
2017-05-15 10:02:32 +00:00
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
2017-06-16 04:59:11 +00:00
if ( ! graceTimeDone ) {
if ( t > 1000 * settings . time _ulGraceTime ) {
if ( totLoaded > 0 ) { // if the connection is so slow that we didn't get a single chunk yet, do not reset
startT = new Date ( ) . getTime ( )
totLoaded = 0.0 ;
}
graceTimeDone = true ;
}
} else {
var speed = totLoaded / ( t / 1000.0 )
ulStatus = ( ( speed * 8 * settings . overheadCompensationFactor ) / 1048576 ) . toFixed ( 2 ) // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 to go to megabits/s
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 ( )
}
2017-05-15 10:02:32 +00:00
}
} . bind ( this ) , 200 )
2016-10-09 07:13:36 +00:00
}
2017-05-15 10:02:32 +00:00
// ping+jitter test, function done is called when it's over
var ptCalled = false // used to prevent multiple accidental calls to pingTest
function pingTest ( done ) {
if ( ptCalled ) return ; else ptCalled = true // pingTest already called?
var prevT = null // last time a pong was received
var ping = 0.0 // current ping value
var jitter = 0.0 // current jitter value
var i = 0 // counter of pongs received
var prevInstspd = 0 // last ping time, used for jitter calculation
xhr = [ ]
// ping function
var doPing = function ( ) {
prevT = new Date ( ) . getTime ( )
xhr [ 0 ] = new XMLHttpRequest ( )
xhr [ 0 ] . onload = function ( ) {
// pong
if ( i === 0 ) {
prevT = new Date ( ) . getTime ( ) // first pong
} else {
2017-06-14 10:33:11 +00:00
var instspd = ( new Date ( ) . getTime ( ) - prevT )
2017-05-15 10:02:32 +00:00
var instjitter = Math . abs ( instspd - prevInstspd )
2017-06-16 04:59:11 +00:00
if ( i === 1 ) ping = instspd ; /* first ping, can't tell jitter yet*/ else {
2017-05-15 10:02:32 +00:00
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
}
pingStatus = ping . toFixed ( 2 )
jitterStatus = jitter . toFixed ( 2 )
i ++
if ( i < settings . count _ping ) doPing ( ) ; else done ( ) // more pings to do?
} . bind ( this )
xhr [ 0 ] . onerror = function ( ) {
// a ping failed, cancel test
2017-06-13 12:20:05 +00:00
if ( settings . xhr _ignoreErrors === 0 ) { //abort
pingStatus = 'Fail'
jitterStatus = 'Fail'
clearRequests ( )
done ( )
}
if ( settings . xhr _ignoreErrors === 1 ) doPing ( ) //retry ping
2017-06-16 04:59:11 +00:00
if ( settings . xhr _ignoreErrors === 2 ) { //ignore failed ping
2017-06-13 12:20:05 +00:00
i ++
if ( i < settings . count _ping ) doPing ( ) ; else done ( ) // more pings to do?
}
2017-05-15 10:02:32 +00:00
} . bind ( this )
// sent xhr
2017-06-16 04:59:11 +00:00
xhr [ 0 ] . open ( 'GET' , settings . url _ping + url _sep ( settings . url _ping ) + 'r=' + Math . random ( ) , true ) // random string to prevent caching
2017-05-15 10:02:32 +00:00
xhr [ 0 ] . send ( )
} . bind ( this )
doPing ( ) // start first ping
2017-03-02 16:58:40 +00:00
}