speedtest_worker.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. /*
  2. HTML5 Speedtest v4.1
  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. dlStatus="", //download speed in megabit/s with 2 decimal digits
  10. ulStatus="", //upload speed in megabit/s with 2 decimal digits
  11. pingStatus="", //ping in milliseconds with 2 decimal digits
  12. jitterStatus="", //jitter in milliseconds with 2 decimal digits
  13. clientIp=""; //client's IP address as reported by getIP.php
  14. //test settings. can be overridden by sending specific values with the start command
  15. var settings={
  16. time_ul:15, //duration of upload test in seconds
  17. time_dl:15, //duration of download test in seconds
  18. count_ping:35, //number of pings to perform in ping test
  19. url_dl:"garbage.php", //path to a large file or garbage.php, used for download test. must be relative to this js file
  20. url_ul:"empty.dat", //path to an empty file, used for upload test. must be relative to this js file
  21. url_ping:"empty.dat", //path to an empty file, used for ping test. must be relative to this js file
  22. url_getIp:"getIP.php", //path to getIP.php relative to this js file, or a similar thing that outputs the client's ip
  23. xhr_dlMultistream:10, //number of download streams to use (can be different if enable_quirks is active)
  24. xhr_ulMultistream:3, //number of upload streams to use (can be different if enable_quirks is active)
  25. 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)
  26. garbagePhp_chunkSize:20, //size of chunks sent by garbage.php (can be different if enable_quirks is active)
  27. 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
  28. allow_fetchAPI:false, //enables Fetch API. currently disabled because it leaks memory like no tomorrow
  29. force_fetchAPI:false //when Fetch API is enabled, it will force usage on every browser that supports it
  30. };
  31. var xhr=null, //array of currently active xhr requests
  32. interval=null; //timer used in tests
  33. /*
  34. when set to true (automatically) the download test will use the fetch api instead of xhr.
  35. fetch api is used if
  36. -allow_fetchAPI is true AND
  37. -(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)
  38. */
  39. var useFetchAPI=false;
  40. /*
  41. listener for commands from main thread to this worker.
  42. commands:
  43. -status: returns the current status as a string of values spearated by a semicolon (;) in this order: testStatus;dlStatus;ulStatus;pingStatus;clientIp;jitterStatus
  44. -abort: aborts the current test
  45. -start: starts the test. optionally, settings can be passed as JSON.
  46. example: start {"time_ul":"10", "time_dl":"10", "count_ping":"50"}
  47. */
  48. this.addEventListener('message', function(e){
  49. var params=e.data.split(" ");
  50. if(params[0]=="status"){ //return status
  51. postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus);
  52. }
  53. if(params[0]=="start"&&testStatus==0){ //start new test
  54. testStatus=1;
  55. try{
  56. //parse settings, if present
  57. var s=JSON.parse(e.data.substring(5));
  58. if(typeof s.url_dl != "undefined") settings.url_dl=s.url_dl; //download url
  59. if(typeof s.url_ul != "undefined") settings.url_ul=s.url_ul; //upload url
  60. if(typeof s.url_ping != "undefined") settings.url_ping=s.url_ping; //ping url
  61. if(typeof s.url_getIp != "undefined") settings.url_getIp=s.url_getIp; //url to getIP.php
  62. if(typeof s.time_dl != "undefined") settings.time_dl=s.time_dl; //duration of download test
  63. if(typeof s.time_ul != "undefined") settings.time_ul=s.time_ul; //duration of upload test
  64. if(typeof s.enable_quirks != "undefined") settings.enable_quirks=s.enable_quirks; //enable quirks or not
  65. if(typeof s.allow_fetchAPI != "undefined") settings.allow_fetchAPI=s.allow_fetchAPI; //allows fetch api to be used if supported
  66. //quirks for specific browsers. more may be added in future releases
  67. if(settings.enable_quirks){
  68. var ua=navigator.userAgent;
  69. if(/Firefox.(\d+\.\d+)/i.test(ua)){
  70. //ff more precise with 1 upload stream
  71. settings.xhr_ulMultistream=1;
  72. }
  73. if(/Edge.(\d+\.\d+)/i.test(ua)){
  74. //edge more precise with 3 download streams
  75. settings.xhr_dlMultistream=3;
  76. }
  77. if((/Safari.(\d+)/i.test(ua))&&!(/Chrome.(\d+)/i.test(ua))){
  78. //safari more precise with 10 upload streams and 5mb chunks for download test
  79. settings.xhr_ulMultistream=10;
  80. settings.garbagePhp_chunkSize=5;
  81. }
  82. if(/Chrome.(\d+)/i.test(ua)&&(!!self.fetch)){
  83. //chrome can't handle large xhr very well, use fetch api if available and allowed
  84. if(settings.allow_fetchAPI) useFetchAPI=true;
  85. //chrome more precise with 5 streams
  86. settings.xhr_dlMultistream=5;
  87. }
  88. }
  89. if(typeof s.count_ping != "undefined") settings.count_ping=s.count_ping; //number of pings for ping test
  90. if(typeof s.xhr_dlMultistream != "undefined") settings.xhr_dlMultistream=s.xhr_dlMultistream; //number of download streams
  91. if(typeof s.xhr_ulMultistream != "undefined") settings.xhr_ulMultistream=s.xhr_ulMultistream; //number of upload streams
  92. if(typeof s.xhr_dlUseBlob != "undefined") settings.xhr_dlUseBlob=s.xhr_dlUseBlob; //use blob for download test
  93. if(typeof s.garbagePhp_chunkSize != "undefined") settings.garbagePhp_chunkSize=s.garbagePhp_chunkSize; //size of garbage.php chunks
  94. if(typeof s.force_fetchAPI != "undefined") settings.force_fetchAPI=s.force_fetchAPI; //use fetch api on all browsers that support it if enabled
  95. if(settings.allow_fetchAPI&&settings.force_fetchAPI&&(!!self.fetch)) useFetchAPI=true;
  96. }catch(e){}
  97. //run the tests
  98. console.log(settings);
  99. console.log("Fetch API: "+useFetchAPI);
  100. getIp(function(){dlTest(function(){testStatus=2;pingTest(function(){testStatus=3;ulTest(function(){testStatus=4;});});})});
  101. }
  102. if(params[0]=="abort"){ //abort command
  103. clearRequests(); //stop all xhr activity
  104. if(interval)clearInterval(interval); //clear timer if present
  105. testStatus=5;dlStatus="";ulStatus="";pingStatus="";jitterStatus=""; //set test as aborted
  106. }
  107. });
  108. //stops all XHR activity, aggressively
  109. function clearRequests(){
  110. if(xhr){
  111. for(var i=0;i<xhr.length;i++){
  112. if(useFetchAPI)try{xhr[i].cancelRequested=true;}catch(e){}
  113. try{xhr[i].onprogress=null; xhr[i].onload=null; xhr[i].onerror=null;}catch(e){}
  114. try{xhr[i].upload.onprogress=null; xhr[i].upload.onload=null; xhr[i].upload.onerror=null;}catch(e){}
  115. try{xhr[i].abort();}catch(e){}
  116. try{delete(xhr[i]);}catch(e){}
  117. }
  118. xhr=null;
  119. }
  120. }
  121. //gets client's IP using url_getIp, then calls the done function
  122. function getIp(done){
  123. xhr=new XMLHttpRequest();
  124. xhr.onload=function(){
  125. clientIp=xhr.responseText;
  126. done();
  127. }
  128. xhr.onerror=function(){
  129. done();
  130. }
  131. xhr.open("GET",settings.url_getIp+"?r="+Math.random(),true);
  132. xhr.send();
  133. }
  134. //download test, calls done function when it's over
  135. var dlCalled=false; //used to prevent multiple accidental calls to dlTest
  136. function dlTest(done){
  137. if(dlCalled) return; else dlCalled=true; //dlTest already called?
  138. var totLoaded=0.0, //total number of loaded bytes
  139. startT=new Date().getTime(), //timestamp when test was started
  140. failed=false; //set to true if a stream fails
  141. xhr=[];
  142. //function to create a download stream. streams are slightly delayed so that they will not end at the same time
  143. var testStream=function(i,delay){
  144. setTimeout(function(){
  145. if(testStatus!=1) return; //delayed stream ended up starting after the end of the download test
  146. if(useFetchAPI){
  147. xhr[i]=fetch(settings.url_dl+"?r="+Math.random()+"&ckSize="+settings.garbagePhp_chunkSize).then(function(response) {
  148. var reader = response.body.getReader();
  149. var consume=function() {
  150. return reader.read().then(function(result){
  151. if(result.done) testStream(i); else{
  152. totLoaded+=result.value.length;
  153. if(xhr[i].canelRequested) reader.cancel();
  154. }
  155. return consume();
  156. }.bind(this));
  157. }.bind(this);
  158. return consume();
  159. }.bind(this));
  160. }else{
  161. var prevLoaded=0; //number of bytes loaded last time onprogress was called
  162. var x=new XMLHttpRequest();
  163. xhr[i]=x;
  164. xhr[i].onprogress=function(event){
  165. if(testStatus!=1){try{x.abort();}catch(e){}} //just in case this XHR is still running after the download test
  166. //progress event, add number of new loaded bytes to totLoaded
  167. var loadDiff=event.loaded<=0?0:(event.loaded-prevLoaded);
  168. if(isNaN(loadDiff)||!isFinite(loadDiff)||loadDiff<0) return; //just in case
  169. totLoaded+=loadDiff;
  170. prevLoaded=event.loaded;
  171. }.bind(this);
  172. xhr[i].onload=function(){
  173. //the large file has been loaded entirely, start again
  174. try{xhr[i].abort();}catch(e){} //reset the stream data to empty ram
  175. testStream(i,0);
  176. }.bind(this);
  177. xhr[i].onerror=function(){
  178. //error, abort
  179. failed=true;
  180. try{xhr[i].abort();}catch(e){}
  181. delete(xhr[i]);
  182. }.bind(this);
  183. //send xhr
  184. try{if(settings.xhr_dlUseBlob) xhr[i].responseType='blob'; else xhr[i].responseType='arraybuffer';}catch(e){}
  185. xhr[i].open("GET",settings.url_dl+"?r="+Math.random()+"&ckSize="+settings.garbagePhp_chunkSize,true); //random string to prevent caching
  186. xhr[i].send();
  187. }
  188. }.bind(this),1+delay);
  189. }.bind(this);
  190. //open streams
  191. for(var i=0;i<settings.xhr_dlMultistream;i++){
  192. testStream(i,100*i);
  193. }
  194. //every 200ms, update dlStatus
  195. interval=setInterval(function(){
  196. var t=new Date().getTime()-startT;
  197. if(t<200) return;
  198. var speed=totLoaded/(t/1000.0);
  199. dlStatus=((speed*8)/925000.0).toFixed(2); //925000 instead of 1048576 to account for overhead
  200. if((t/1000.0)>settings.time_dl||failed){ //test is over, stop streams and timer
  201. if(failed||isNaN(dlStatus)) dlStatus="Fail";
  202. clearRequests();
  203. clearInterval(interval);
  204. done();
  205. }
  206. }.bind(this),200);
  207. }
  208. //upload test, calls done function whent it's over
  209. //garbage data for upload test
  210. var r=new ArrayBuffer(1048576);
  211. try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random();}catch(e){}
  212. var req=[],reqsmall=[];
  213. for(var i=0;i<20;i++) req.push(r);
  214. req=new Blob(req);
  215. r=new ArrayBuffer(262144);
  216. try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random();}catch(e){}
  217. reqsmall.push(r);
  218. reqsmall=new Blob(reqsmall);
  219. var ulCalled=false; //used to prevent multiple accidental calls to ulTest
  220. function ulTest(done){
  221. if(ulCalled) return; else ulCalled=true; //ulTest already called?
  222. var totLoaded=0.0, //total number of transmitted bytes
  223. startT=new Date().getTime(), //timestamp when test was started
  224. failed=false; //set to true if a stream fails
  225. xhr=[];
  226. //function to create an upload stream. streams are slightly delayed so that they will not end at the same time
  227. var testStream=function(i,delay){
  228. setTimeout(function(){
  229. if(testStatus!=3) return; //delayed stream ended up starting after the end of the upload test
  230. var prevLoaded=0; //number of bytes transmitted last time onprogress was called
  231. var x=new XMLHttpRequest();
  232. xhr[i]=x;
  233. var ie11workaround;
  234. try{
  235. xhr[i].upload.onprogress;
  236. ie11workaround=false;
  237. }catch(e){
  238. ie11workaround=true;
  239. }
  240. if(ie11workaround){
  241. //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
  242. xhr[i].onload=function(){
  243. totLoaded+=262144;
  244. testStream(i,0);
  245. }
  246. xhr[i].onerror=function(){
  247. //error, abort
  248. failed=true;
  249. try{xhr[i].abort();}catch(e){}
  250. delete(xhr[i]);
  251. }
  252. xhr[i].open("POST",settings.url_ul+"?r="+Math.random(),true); //random string to prevent caching
  253. xhr[i].setRequestHeader('Content-Encoding','identity'); //disable compression (some browsers may refuse it, but data is incompressible anyway)
  254. xhr[i].send(reqsmall);
  255. }else{
  256. //REGULAR version, no workaround
  257. xhr[i].upload.onprogress=function(event){
  258. if(testStatus!=3){try{x.abort();}catch(e){}} //just in case this XHR is still running after the upload test
  259. //progress event, add number of new loaded bytes to totLoaded
  260. var loadDiff=event.loaded<=0?0:(event.loaded-prevLoaded);
  261. if(isNaN(loadDiff)||!isFinite(loadDiff)||loadDiff<0) return; //just in case
  262. totLoaded+=loadDiff;
  263. prevLoaded=event.loaded;
  264. }.bind(this);
  265. xhr[i].upload.onload=function(){
  266. //this stream sent all the garbage data, start again
  267. testStream(i,0);
  268. }.bind(this);
  269. xhr[i].upload.onerror=function(){
  270. //error, abort
  271. failed=true;
  272. try{xhr[i].abort();}catch(e){}
  273. delete(xhr[i]);
  274. }.bind(this);
  275. //send xhr
  276. xhr[i].open("POST",settings.url_ul+"?r="+Math.random(),true); //random string to prevent caching
  277. xhr[i].setRequestHeader('Content-Encoding','identity'); //disable compression (some browsers may refuse it, but data is incompressible anyway)
  278. xhr[i].send(req);
  279. }
  280. }.bind(this),1);
  281. }.bind(this);
  282. //open streams
  283. for(var i=0;i<settings.xhr_ulMultistream;i++){
  284. testStream(i,100*i);
  285. }
  286. //every 200ms, update ulStatus
  287. interval=setInterval(function(){
  288. var t=new Date().getTime()-startT;
  289. if(t<200) return;
  290. var speed=totLoaded/(t/1000.0);
  291. ulStatus=((speed*8)/925000.0).toFixed(2); //925000 instead of 1048576 to account for overhead
  292. if((t/1000.0)>settings.time_ul||failed){ //test is over, stop streams and timer
  293. if(failed||isNaN(ulStatus)) ulStatus="Fail";
  294. clearRequests();
  295. clearInterval(interval);
  296. done();
  297. }
  298. }.bind(this),200);
  299. }
  300. //ping+jitter test, function done is called when it's over
  301. var ptCalled=false; //used to prevent multiple accidental calls to pingTest
  302. function pingTest(done){
  303. if(ptCalled) return; else ptCalled=true; //pingTest already called?
  304. var prevT=null, //last time a pong was received
  305. ping=0.0, //current ping value
  306. jitter=0.0, //current jitter value
  307. i=0, //counter of pongs received
  308. prevInstspd=0; //last ping time, used for jitter calculation
  309. xhr=[];
  310. //ping function
  311. var doPing=function(){
  312. prevT=new Date().getTime();
  313. xhr[0]=new XMLHttpRequest();
  314. xhr[0].onload=function(){
  315. //pong
  316. if(i==0){
  317. prevT=new Date().getTime(); //first pong
  318. }else{
  319. var instspd=(new Date().getTime()-prevT)/2;
  320. var instjitter=Math.abs(instspd-prevInstspd);
  321. if(i==1)ping=instspd; /*first ping, can't tell jiutter yet*/ else{
  322. ping=ping*0.9+instspd*0.1; //ping, weighted average
  323. 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.
  324. }
  325. prevInstspd=instspd;
  326. }
  327. pingStatus=ping.toFixed(2);
  328. jitterStatus=jitter.toFixed(2);
  329. i++;
  330. if(i<settings.count_ping) doPing(); else done(); //more pings to do?
  331. }.bind(this);
  332. xhr[0].onerror=function(){
  333. //a ping failed, cancel test
  334. pingStatus="Fail";
  335. jitterStatus="Fail";
  336. clearRequests();
  337. done();
  338. }.bind(this);
  339. //sent xhr
  340. xhr[0].open("GET",settings.url_ping+"?r="+Math.random(),true); //random string to prevent caching
  341. xhr[0].send();
  342. }.bind(this);
  343. doPing(); //start first ping
  344. }