speedtest_worker.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /*
  2. HTML5 Speedtest v4.4
  3. by Federico Dossena
  4. https://github.com/adolfintel/speedtest/
  5. GNU LGPLv3 License
  6. */
  7. // data reported to main thread
  8. var testStatus = 0 // 0=not started, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort/error
  9. var dlStatus = '' // download speed in megabit/s with 2 decimal digits
  10. var ulStatus = '' // upload speed in megabit/s with 2 decimal digits
  11. var pingStatus = '' // ping in milliseconds with 2 decimal digits
  12. var jitterStatus = '' // jitter in milliseconds with 2 decimal digits
  13. var clientIp = '' // client's IP address as reported by getIP.php
  14. var log='' //telemetry log
  15. function tlog(s){log+=Date.now()+': '+s+'\n'}
  16. function twarn(s){log+=Date.now()+' WARN: '+s+'\n'; console.warn(s)}
  17. // test settings. can be overridden by sending specific values with the start command
  18. var settings = {
  19. test_order: "ID_U_P", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay
  20. time_ul: 15, // duration of upload test in seconds
  21. time_dl: 15, // duration of download test in seconds
  22. time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill)
  23. time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase)
  24. count_ping: 35, // number of pings to perform in ping test
  25. url_dl: 'garbage.php', // path to a large file or garbage.php, used for download test. must be relative to this js file
  26. url_ul: 'empty.php', // path to an empty file, used for upload test. must be relative to this js file
  27. url_ping: 'empty.php', // path to an empty file, used for ping test. must be relative to this js file
  28. url_getIp: 'getIP.php', // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
  29. xhr_dlMultistream: 10, // number of download streams to use (can be different if enable_quirks is active)
  30. xhr_ulMultistream: 3, // number of upload streams to use (can be different if enable_quirks is active)
  31. xhr_ignoreErrors: 1, // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors
  32. 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)
  33. garbagePhp_chunkSize: 20, // size of chunks sent by garbage.php (can be different if enable_quirks is active)
  34. 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
  35. 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)
  36. telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results+log)
  37. url_telemetry: 'telemetry.php' // path to the script that adds telemetry data to the database
  38. }
  39. var xhr = null // array of currently active xhr requests
  40. var interval = null // timer used in tests
  41. var delayTimer = null // another timer used in test
  42. var test_pointer = 0 //pointer to the next test to run inside settings.test_order
  43. /*
  44. this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator
  45. */
  46. function url_sep (url) { return url.match(/\?/) ? '&' : '?'; }
  47. /*
  48. listener for commands from main thread to this worker.
  49. commands:
  50. -status: returns the current status as a string of values spearated by a semicolon (;) in this order: testStatus;dlStatus;ulStatus;pingStatus;clientIp;jitterStatus
  51. -abort: aborts the current test
  52. -start: starts the test. optionally, settings can be passed as JSON.
  53. example: start {"time_ul":"10", "time_dl":"10", "count_ping":"50"}
  54. */
  55. this.addEventListener('message', function (e) {
  56. var params = e.data.split(' ')
  57. if (params[0] === 'status') { // return status
  58. postMessage(testStatus + ';' + dlStatus + ';' + ulStatus + ';' + pingStatus + ';' + clientIp + ';' + jitterStatus)
  59. }
  60. if (params[0] === 'start' && testStatus === 0) { // start new test
  61. testStatus = 1
  62. try {
  63. // parse settings, if present
  64. var s = {}
  65. try{
  66. var ss = e.data.substring(5)
  67. if (ss) s = JSON.parse(ss)
  68. }catch(e){ twarn('Error parsing custom settings JSON. Please check your syntax') }
  69. //copy custom settings
  70. for(var key in s){
  71. if(typeof settings[key] !== 'undefined') settings[key]=s[key]; else twarn("Unknown setting ignored: "+key);
  72. }
  73. // quirks for specific browsers. apply only if not overridden. more may be added in future releases
  74. if (settings.enable_quirks||(typeof s.enable_quirks !== 'undefined' && s.enable_quirks)) {
  75. var ua = navigator.userAgent
  76. if (/Firefox.(\d+\.\d+)/i.test(ua)) {
  77. if(typeof s.xhr_ulMultistream === 'undefined'){
  78. // ff more precise with 1 upload stream
  79. settings.xhr_ulMultistream = 1
  80. }
  81. if(typeof s.test_order === 'undefined'){
  82. // ff more precise if upload test is performed after ping because upload XHRs are not interrupted immediately
  83. settings.test_order= 'ID_P_U'
  84. }
  85. }
  86. if (/Edge.(\d+\.\d+)/i.test(ua)) {
  87. if(typeof s.xhr_dlMultistream === 'undefined'){
  88. // edge more precise with 3 download streams
  89. settings.xhr_dlMultistream = 3
  90. }
  91. if (/Edge\/15.(\d+)/i.test(ua) || /Edge\/16.(\d+)/i.test(ua)) {
  92. //Edge 15 introduced a bug that causes onprogress events to not get fired, so for Edge 15, we have to use the "small chunks" workaround that reduces accuracy
  93. settings.forceIE11Workaround = true
  94. }
  95. }
  96. if (/Chrome.(\d+)/i.test(ua) && (!!self.fetch)) {
  97. if(typeof s.xhr_dlMultistream === 'undefined'){
  98. // chrome more precise with 5 streams
  99. settings.xhr_dlMultistream = 5
  100. }
  101. }
  102. }
  103. //telemetry_level has to be parsed and not just copied
  104. if(typeof s.telemetry_level !== 'undefined') settings.telemetry_level = s.telemetry_level === 'basic' ? 1 : s.telemetry_level === 'full' ? 2 : 0; // telemetry level
  105. //transform test_order to uppercase, just in case
  106. settings.test_order=settings.test_order.toUpperCase();
  107. } catch (e) { twarn('Possible error in custom test settings. Some settings may not be applied. Exception: '+e) }
  108. // run the tests
  109. tlog(JSON.stringify(settings))
  110. test_pointer=0;
  111. var runNextTest=function(){
  112. if(test_pointer>=settings.test_order.length){testStatus=4; sendTelemetry(); return;}
  113. switch(settings.test_order.charAt(test_pointer)){
  114. case 'I':{test_pointer++; getIp(runNextTest);} break;
  115. case 'D':{test_pointer++; testStatus=1; dlTest(runNextTest);} break;
  116. case 'U':{test_pointer++; testStatus=3; ulTest(runNextTest);} break;
  117. case 'P':{test_pointer++; testStatus=2; pingTest(runNextTest);} break;
  118. case '_':{test_pointer++; delayTimer=setTimeout(runNextTest,1000);} break;
  119. default: test_pointer++;
  120. }
  121. }
  122. runNextTest()
  123. }
  124. if (params[0] === 'abort') { // abort command
  125. tlog('manually aborted')
  126. clearRequests() // stop all xhr activity
  127. runNextTest=null;
  128. if (interval) clearInterval(interval) // clear timer if present
  129. if (settings.telemetry_level > 1) sendTelemetry()
  130. testStatus = 5; dlStatus = ''; ulStatus = ''; pingStatus = ''; jitterStatus = '' // set test as aborted
  131. }
  132. })
  133. // stops all XHR activity, aggressively
  134. function clearRequests () {
  135. tlog('stopping pending XHRs')
  136. if (xhr) {
  137. for (var i = 0; i < xhr.length; i++) {
  138. try { xhr[i].onprogress = null; xhr[i].onload = null; xhr[i].onerror = null } catch (e) { }
  139. try { xhr[i].upload.onprogress = null; xhr[i].upload.onload = null; xhr[i].upload.onerror = null } catch (e) { }
  140. try { xhr[i].abort() } catch (e) { }
  141. try { delete (xhr[i]) } catch (e) { }
  142. }
  143. xhr = null
  144. }
  145. }
  146. // gets client's IP using url_getIp, then calls the done function
  147. var ipCalled = false // used to prevent multiple accidental calls to getIp
  148. function getIp (done) {
  149. tlog('getIp')
  150. if (ipCalled) return; else ipCalled = true // getIp already called?
  151. xhr = new XMLHttpRequest()
  152. xhr.onload = function () {
  153. tlog("IP: "+xhr.responseText)
  154. clientIp = xhr.responseText
  155. done()
  156. }
  157. xhr.onerror = function () {
  158. tlog('getIp failed')
  159. done()
  160. }
  161. xhr.open('GET', settings.url_getIp + url_sep(settings.url_getIp) + 'r=' + Math.random(), true)
  162. xhr.send()
  163. }
  164. // download test, calls done function when it's over
  165. var dlCalled = false // used to prevent multiple accidental calls to dlTest
  166. function dlTest (done) {
  167. tlog('dlTest')
  168. if (dlCalled) return; else dlCalled = true // dlTest already called?
  169. var totLoaded = 0.0, // total number of loaded bytes
  170. startT = new Date().getTime(), // timestamp when test was started
  171. graceTimeDone = false, //set to true after the grace time is past
  172. failed = false // set to true if a stream fails
  173. xhr = []
  174. // function to create a download stream. streams are slightly delayed so that they will not end at the same time
  175. var testStream = function (i, delay) {
  176. setTimeout(function () {
  177. if (testStatus !== 1) return // delayed stream ended up starting after the end of the download test
  178. tlog('dl test stream started '+i+' '+delay)
  179. var prevLoaded = 0 // number of bytes loaded last time onprogress was called
  180. var x = new XMLHttpRequest()
  181. xhr[i] = x
  182. xhr[i].onprogress = function (event) {
  183. tlog('dl stream progress event '+i+' '+event.loaded)
  184. if (testStatus !== 1) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the download test
  185. // progress event, add number of new loaded bytes to totLoaded
  186. var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
  187. if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return // just in case
  188. totLoaded += loadDiff
  189. prevLoaded = event.loaded
  190. }.bind(this)
  191. xhr[i].onload = function () {
  192. // the large file has been loaded entirely, start again
  193. tlog('dl stream finished '+i)
  194. try { xhr[i].abort() } catch (e) { } // reset the stream data to empty ram
  195. testStream(i, 0)
  196. }.bind(this)
  197. xhr[i].onerror = function () {
  198. // error
  199. tlog('dl stream failed '+i)
  200. if (settings.xhr_ignoreErrors === 0) failed=true //abort
  201. try { xhr[i].abort() } catch (e) { }
  202. delete (xhr[i])
  203. if (settings.xhr_ignoreErrors === 1) testStream(i, 100) //restart stream after 100ms
  204. }.bind(this)
  205. // send xhr
  206. try { if (settings.xhr_dlUseBlob) xhr[i].responseType = 'blob'; else xhr[i].responseType = 'arraybuffer' } catch (e) { }
  207. 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
  208. xhr[i].send()
  209. }.bind(this), 1 + delay)
  210. }.bind(this)
  211. // open streams
  212. for (var i = 0; i < settings.xhr_dlMultistream; i++) {
  213. testStream(i, 100 * i)
  214. }
  215. // every 200ms, update dlStatus
  216. interval = setInterval(function () {
  217. tlog('DL: '+dlStatus+(graceTimeDone?'':' (in grace time)'))
  218. var t = new Date().getTime() - startT
  219. if (t < 200) return
  220. if (!graceTimeDone){
  221. if (t > 1000 * settings.time_dlGraceTime){
  222. if (totLoaded > 0){ // if the connection is so slow that we didn't get a single chunk yet, do not reset
  223. startT = new Date().getTime()
  224. totLoaded = 0.0;
  225. }
  226. graceTimeDone = true;
  227. }
  228. }else{
  229. var speed = totLoaded / (t / 1000.0)
  230. 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
  231. if (((t / 1000.0) > settings.time_dl && dlStatus > 0) || failed) { // test is over, stop streams and timer
  232. if (failed || isNaN(dlStatus)) dlStatus = 'Fail'
  233. clearRequests()
  234. clearInterval(interval)
  235. tlog('dlTest finished '+dlStatus)
  236. done()
  237. }
  238. }
  239. }.bind(this), 200)
  240. }
  241. // upload test, calls done function whent it's over
  242. // garbage data for upload test
  243. var r = new ArrayBuffer(1048576)
  244. try { r = new Float32Array(r); for (var i = 0; i < r.length; i++)r[i] = Math.random() } catch (e) { }
  245. var req = []
  246. var reqsmall = []
  247. for (var i = 0; i < 20; i++) req.push(r)
  248. req = new Blob(req)
  249. r = new ArrayBuffer(262144)
  250. try { r = new Float32Array(r); for (var i = 0; i < r.length; i++)r[i] = Math.random() } catch (e) { }
  251. reqsmall.push(r)
  252. reqsmall = new Blob(reqsmall)
  253. var ulCalled = false // used to prevent multiple accidental calls to ulTest
  254. function ulTest (done) {
  255. tlog('ulTest')
  256. if (ulCalled) return; else ulCalled = true // ulTest already called?
  257. var totLoaded = 0.0, // total number of transmitted bytes
  258. startT = new Date().getTime(), // timestamp when test was started
  259. graceTimeDone = false, //set to true after the grace time is past
  260. failed = false // set to true if a stream fails
  261. xhr = []
  262. // function to create an upload stream. streams are slightly delayed so that they will not end at the same time
  263. var testStream = function (i, delay) {
  264. setTimeout(function () {
  265. if (testStatus !== 3) return // delayed stream ended up starting after the end of the upload test
  266. tlog('ul test stream started '+i+' '+delay)
  267. var prevLoaded = 0 // number of bytes transmitted last time onprogress was called
  268. var x = new XMLHttpRequest()
  269. xhr[i] = x
  270. var ie11workaround
  271. if (settings.forceIE11Workaround) ie11workaround = true; else {
  272. try {
  273. xhr[i].upload.onprogress
  274. ie11workaround = false
  275. } catch (e) {
  276. ie11workaround = true
  277. }
  278. }
  279. if (ie11workaround) {
  280. // 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
  281. xhr[i].onload = function () {
  282. tlog('ul stream progress event (ie11wa)')
  283. totLoaded += 262144
  284. testStream(i, 0)
  285. }
  286. xhr[i].onerror = function () {
  287. // error, abort
  288. tlog('ul stream failed (ie11wa)')
  289. if (settings.xhr_ignoreErrors === 0) failed = true //abort
  290. try { xhr[i].abort() } catch (e) { }
  291. delete (xhr[i])
  292. if (settings.xhr_ignoreErrors === 1) testStream(i,100); //restart stream after 100ms
  293. }
  294. xhr[i].open('POST', settings.url_ul + url_sep(settings.url_ul) + 'r=' + Math.random(), true) // random string to prevent caching
  295. xhr[i].setRequestHeader('Content-Encoding', 'identity') // disable compression (some browsers may refuse it, but data is incompressible anyway)
  296. xhr[i].send(reqsmall)
  297. } else {
  298. // REGULAR version, no workaround
  299. xhr[i].upload.onprogress = function (event) {
  300. tlog('ul stream progress event '+i+' '+event.loaded)
  301. if (testStatus !== 3) { try { x.abort() } catch (e) { } } // just in case this XHR is still running after the upload test
  302. // progress event, add number of new loaded bytes to totLoaded
  303. var loadDiff = event.loaded <= 0 ? 0 : (event.loaded - prevLoaded)
  304. if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return // just in case
  305. totLoaded += loadDiff
  306. prevLoaded = event.loaded
  307. }.bind(this)
  308. xhr[i].upload.onload = function () {
  309. // this stream sent all the garbage data, start again
  310. tlog('ul stream finished '+i)
  311. testStream(i, 0)
  312. }.bind(this)
  313. xhr[i].upload.onerror = function () {
  314. tlog('ul stream failed '+i)
  315. if (settings.xhr_ignoreErrors === 0) failed=true //abort
  316. try { xhr[i].abort() } catch (e) { }
  317. delete (xhr[i])
  318. if (settings.xhr_ignoreErrors === 1) testStream(i, 100) //restart stream after 100ms
  319. }.bind(this)
  320. // send xhr
  321. xhr[i].open('POST', settings.url_ul + url_sep(settings.url_ul) + 'r=' + Math.random(), true) // random string to prevent caching
  322. xhr[i].setRequestHeader('Content-Encoding', 'identity') // disable compression (some browsers may refuse it, but data is incompressible anyway)
  323. xhr[i].send(req)
  324. }
  325. }.bind(this), 1)
  326. }.bind(this)
  327. // open streams
  328. for (var i = 0; i < settings.xhr_ulMultistream; i++) {
  329. testStream(i, 100 * i)
  330. }
  331. // every 200ms, update ulStatus
  332. interval = setInterval(function () {
  333. tlog('UL: '+ulStatus+(graceTimeDone?'':' (in grace time)'))
  334. var t = new Date().getTime() - startT
  335. if (t < 200) return
  336. if (!graceTimeDone){
  337. if (t > 1000 * settings.time_ulGraceTime){
  338. if (totLoaded > 0){ // if the connection is so slow that we didn't get a single chunk yet, do not reset
  339. startT = new Date().getTime()
  340. totLoaded = 0.0;
  341. }
  342. graceTimeDone = true;
  343. }
  344. }else{
  345. var speed = totLoaded / (t / 1000.0)
  346. 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
  347. if (((t / 1000.0) > settings.time_ul && ulStatus > 0) || failed) { // test is over, stop streams and timer
  348. if (failed || isNaN(ulStatus)) ulStatus = 'Fail'
  349. clearRequests()
  350. clearInterval(interval)
  351. tlog('ulTest finished '+ulStatus)
  352. done()
  353. }
  354. }
  355. }.bind(this), 200)
  356. }
  357. // ping+jitter test, function done is called when it's over
  358. var ptCalled = false // used to prevent multiple accidental calls to pingTest
  359. function pingTest (done) {
  360. tlog('pingTest')
  361. if (ptCalled) return; else ptCalled = true // pingTest already called?
  362. var prevT = null // last time a pong was received
  363. var ping = 0.0 // current ping value
  364. var jitter = 0.0 // current jitter value
  365. var i = 0 // counter of pongs received
  366. var prevInstspd = 0 // last ping time, used for jitter calculation
  367. xhr = []
  368. // ping function
  369. var doPing = function () {
  370. tlog('ping')
  371. prevT = new Date().getTime()
  372. xhr[0] = new XMLHttpRequest()
  373. xhr[0].onload = function () {
  374. // pong
  375. tlog('pong')
  376. if (i === 0) {
  377. prevT = new Date().getTime() // first pong
  378. } else {
  379. var instspd = (new Date().getTime() - prevT)
  380. var instjitter = Math.abs(instspd - prevInstspd)
  381. if (i === 1) ping = instspd; /* first ping, can't tell jitter yet*/ else {
  382. ping = ping * 0.9 + instspd * 0.1 // ping, weighted average
  383. 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.
  384. }
  385. prevInstspd = instspd
  386. }
  387. pingStatus = ping.toFixed(2)
  388. jitterStatus = jitter.toFixed(2)
  389. i++
  390. tlog('PING: '+pingStatus+' JITTER: '+jitterStatus)
  391. if (i < settings.count_ping) doPing(); else done() // more pings to do?
  392. }.bind(this)
  393. xhr[0].onerror = function () {
  394. // a ping failed, cancel test
  395. tlog('ping failed')
  396. if (settings.xhr_ignoreErrors === 0) { //abort
  397. pingStatus = 'Fail'
  398. jitterStatus = 'Fail'
  399. clearRequests()
  400. done()
  401. }
  402. if (settings.xhr_ignoreErrors === 1) doPing() //retry ping
  403. if (settings.xhr_ignoreErrors === 2){ //ignore failed ping
  404. i++
  405. if (i < settings.count_ping) doPing(); else done() // more pings to do?
  406. }
  407. }.bind(this)
  408. // sent xhr
  409. xhr[0].open('GET', settings.url_ping + url_sep(settings.url_ping) + 'r=' + Math.random(), true) // random string to prevent caching
  410. xhr[0].send()
  411. }.bind(this)
  412. doPing() // start first ping
  413. }
  414. // telemetry
  415. function sendTelemetry(){
  416. if (settings.telemetry_level < 1) return
  417. xhr = new XMLHttpRequest()
  418. xhr.onload = function () { console.log('TELEMETRY OL '+xhr.responseText) }
  419. xhr.onerror = function () { console.log('TELEMETRY ERROR '+xhr) }
  420. xhr.open('POST', settings.url_telemetry+"?r="+Math.random(), true);
  421. try{
  422. var fd = new FormData()
  423. fd.append('dl', dlStatus)
  424. fd.append('ul', ulStatus)
  425. fd.append('ping', pingStatus)
  426. fd.append('jitter', jitterStatus)
  427. fd.append('log', settings.telemetry_level>1?log:"")
  428. xhr.send(fd)
  429. }catch(ex){
  430. var postData = 'dl='+encodeURIComponent(dlStatus)+'&ul='+encodeURIComponent(ulStatus)+'&ping='+encodeURIComponent(pingStatus)+'&jitter='+encodeURIComponent(jitterStatus)+'&log='+encodeURIComponent(settings.telemetry_level>1?log:'')
  431. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
  432. xhr.send(postData)
  433. }
  434. }