Explorar el Código

Implemented multistream; Implemented jitter test; Implemented Fetch API (default disabled); Added more settings; Improved code quality; Improved accuracy; Added browser-specific optimizations; Added minified version; Expanded documentation; Some bug fixes

dosse91 hace 8 años
padre
commit
fce6a67be0
Se han modificado 10 ficheros con 342 adiciones y 121 borrados
  1. 2 0
      .gitignore
  2. 4 5
      README.md
  3. BIN
      doc.pdf
  4. 2 2
      example1.html
  5. 10 4
      example2.html
  6. 20 7
      example3.html
  7. 23 4
      example4.html
  8. 1 1
      garbage.php
  9. 280 98
      speedtest_worker.js
  10. 0 0
      speedtest_worker.min.js

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+doc.odt
+cktest.html

+ 4 - 5
README.md

@@ -11,14 +11,13 @@ This is a very lightweight Speedtest implemented in Javascript, using XMLHttpReq
 Only modern browsers are supported (Edge 12+)
 
 ## Requirements
- - A reasonably fast web server
- - Some way to generate garbage data using either the included PHP script, a [big file of random data](http://downloads.fdossena.com/geth.php?r=speedtest-bigfile), or a symlink to /dev/urandom
- - Your server must accept large POST requests (up to 10 Megabytes), otherwise the upload test will fail
- - Client side, there must not be any type of buffering (such as a proxy), or you may get incorrect results
+ - A reasonably fast web server. PHP is optional but recommended (see doc.pdf for details)
+ - Some way to generate garbage data (PHP script included, see doc.pdf for other solutions)
+ - Your server must accept large POST requests (up to 20 Megabytes), otherwise the upload test will fail
  - It's also better if your server does not use compression, but it's not mandatory
 
 ## How to use
-See the examples, it's really simple.
+See the examples or doc.pdf
 
 ## License
 Copyright (C) 2016-2017 Federico Dossena

BIN
doc.pdf


+ 2 - 2
example1.html

@@ -14,13 +14,13 @@
 <h4>Latency</h4>
 <div id="ping"></div>
 <script type="text/javascript">
-	var w=new Worker("speedtest_worker.js"); //create new worker
+	var w=new Worker("speedtest_worker.min.js"); //create new worker
 	setInterval(function(){w.postMessage("status");}.bind(this),100); //ask for status every 100ms
 	w.onmessage=function(event){ //when status is received, split the string and put the values in the appropriate fields
 		var data=event.data.split(";"); //string format: status;download;upload;ping (speeds are in mbit/s) (status: 0=not started, 1=downloading, 2=uploading, 3=ping, 4=done, 5=aborted)
 		document.getElementById("download").innerHTML=data[1]+" Mbit/s";
 		document.getElementById("upload").innerHTML=data[2]+" Mbit/s";
-		document.getElementById("ping").innerHTML=data[3]+" ms";
+		document.getElementById("ping").innerHTML=data[3]+" ms, "+data[5]+" ms jitter";
 		document.getElementById("ip").innerHTML=data[4];
 	}
 	w.postMessage("start"); //start the speedtest (default params. keep garbage.php and empty.dat in the same directory as the js file)

+ 10 - 4
example2.html

@@ -11,7 +11,7 @@
             }
             div.test{
                 display:inline-block;
-                width:30vw;
+                margin:1em;
                 text-align:center;
             }
             div.testName,div.meterUnit{
@@ -48,22 +48,28 @@
             <div class="testName">Latency</div>
             <div class="meter">&nbsp;<span id="ping"></span>&nbsp;</div>
             <div class="meterUnit">ms</div>
+        </div>
+		<div class="test">
+            <div class="testName">Jitter</div>
+            <div class="meter">&nbsp;<span id="jitter"></span>&nbsp;</div>
+            <div class="meterUnit">ms</div>
         </div>
 		<div id="ip"></div>
         <script type="text/javascript">
-            var w=new Worker("speedtest_worker.js");
+            var w=new Worker("speedtest_worker.min.js");
             var interval=setInterval(function(){w.postMessage("status");}.bind(this),100);
             w.onmessage=function(event){
                 var data=event.data.split(";");
                 var status=Number(data[0]);
-                var dl=document.getElementById("download"),ul=document.getElementById("upload"),ping=document.getElementById("ping"),ip=document.getElementById("ip");
-                dl.className=status==1?"flash":"";ul.className=status==2?"flash":"";ping.className=status==3?"flash":"";
+                var dl=document.getElementById("download"),ul=document.getElementById("upload"),ping=document.getElementById("ping"),ip=document.getElementById("ip"),jitter=document.getElementById("jitter");
+                dl.className=status==1?"flash":"";ping.className=status==2?"flash":"";jitter.className=ul.className=status==3?"flash":"";
                 if(status>=4){
                     clearInterval(interval);
                 }
                 dl.innerHTML=data[1];
                 ul.innerHTML=data[2];
                 ping.innerHTML=data[3];
+				jitter.innerHTML=data[5];
 				ip.innerHTML="Your IP: "+data[4];
             }.bind(this);
             w.postMessage('start {"url_dl":"garbage.php","url_ul":"empty.dat","url_ping":"empty.dat","time_dl":"10","time_ul":"15","count_ping":"30"}'); //start with custom parameters. paths are relative to js file. you can omit parameters that you don't want to change

+ 20 - 7
example3.html

@@ -11,16 +11,18 @@
             }
             div.test{
                 display:inline-block;
-                width:30vw;
+                margin:1em;
+				font-size:2vw;
+				min-width:20vw;
                 text-align:center;
             }
             div.testName,div.meterUnit{
-                font-size:3vw;
+                font-size:1em;
             }
             div.meter{
-                font-size:6vw;
+                font-size:1.5em;
                 line-height:1.5em;
-                height:1.5em !important;
+                height:2em !important;
             }
             .flash{
                 animation:flash 0.6s linear infinite;
@@ -41,6 +43,11 @@
 				margin:0.8em 0;
 				font-size:1.2em;
 			}
+			@media all and (max-width: 50em){
+				div.test{
+					font-size:2em;
+				}
+			}
         </style>
         <script type="text/javascript">
             var w=null;
@@ -48,13 +55,13 @@
                 document.getElementById("startBtn").style.display="none";
                 document.getElementById("testArea").style.display="";
                 document.getElementById("abortBtn").style.display="";
-                w=new Worker("speedtest_worker.js");
+                w=new Worker("speedtest_worker.min.js");
                 var interval=setInterval(function(){w.postMessage("status");}.bind(this),100);
                 w.onmessage=function(event){
                     var data=event.data.split(";");
                     var status=Number(data[0]);
-                    var dl=document.getElementById("download"),ul=document.getElementById("upload"),ping=document.getElementById("ping"),ip=document.getElementById("ip");
-                    dl.className=status==1?"flash":"";ul.className=status==2?"flash":"";ping.className=status==3?"flash":"";
+                    var dl=document.getElementById("download"),ul=document.getElementById("upload"),ping=document.getElementById("ping"),ip=document.getElementById("ip"),jitter=document.getElementById("jitter");
+					dl.className=status==1?"flash":"";ping.className=status==2?"flash":"";jitter.className=ul.className=status==3?"flash":"";
                     if(status>=4){
                         clearInterval(interval);
                         document.getElementById("abortBtn").style.display="none";
@@ -67,6 +74,7 @@
                     dl.innerHTML=data[1];
                     ul.innerHTML=data[2];
                     ping.innerHTML=data[3];
+					jitter.innerHTML=data[5];
 					ip.innerHTML=data[4];
                 }.bind(this);
                 w.postMessage("start");
@@ -94,6 +102,11 @@
                 <div class="testName">Latency</div>
                 <div class="meter">&nbsp;<span id="ping"></span>&nbsp;</div>
                 <div class="meterUnit">ms</div>
+            </div>
+			<div class="test">
+                <div class="testName">Jitter</div>
+                <div class="meter">&nbsp;<span id="jitter"></span>&nbsp;</div>
+                <div class="meterUnit">ms</div>
             </div>
             <br/>
             <a href="javascript:abortTest()" id="abortBtn">Abort</a>

+ 23 - 4
example4.html

@@ -65,7 +65,7 @@
             var w=null;
             var ggdl,ggul,ggping;
             function runTest(){
-                w=new Worker("speedtest_worker.js");
+                w=new Worker("speedtest_worker.min.js");
                 var interval=setInterval(function(){w.postMessage("status");}.bind(this),100);
                 document.getElementById("abortBtn").style.display="";
                 document.getElementById("startBtn").style.display="none";
@@ -82,8 +82,9 @@
                     updateGauge(ggul,   data[2]);
                     updateGauge(ggping, data[3]);
 					document.getElementById("ip").innerHTML="Your IP: "+data[4];
+					updateGauge(ggjitter, data[5]);
                 }.bind(this);
-                w.postMessage('start {"time_ul":"10", "time_dl":"10", "count_ping":"20", "url_dl":"garbage.php","url_ul":"empty.dat","url_ping":"empty.dat","url_getIp":"getIP.php"}');
+                w.postMessage('start {"time_ul":"10", "time_dl":"10", "count_ping":"50", "url_dl":"garbage.php","url_ul":"empty.dat","url_ping":"empty.dat","url_getIp":"getIP.php"}');
             }
             function abortTest(){
                 if(w)w.postMessage("abort");
@@ -138,7 +139,25 @@
                     refreshAnimationTime: 300, 
                     value: 0,
                     min: 0,
-                    max: 10,
+                    max: 100,
+                    decimals : 2,
+                    formatNumber: true,                    
+                    humanFriendly : false,  
+                    levelColors: [
+                        "#999999",
+                        "#993333"
+                    ]
+                });
+				ggjitter = new JustGage({
+                    id: 'ggjitter',
+                    title: "Jitter",
+                    label: "ms",
+                    titleFontFamily : "Open Sans",
+                    valueFontFamily : "Open Sans",
+                    refreshAnimationTime: 300, 
+                    value: 0,
+                    min: 0,
+                    max: 100,
                     decimals : 2,
                     formatNumber: true,                    
                     humanFriendly : false,  
@@ -146,7 +165,6 @@
                         "#999999",
                         "#993333"
                     ]
-
                 });
             });
 
@@ -166,6 +184,7 @@
             <div class="meter" id="ggdl"></div>
             <div class="meter" id="ggul"></div>
             <div class="meter" id="ggping"></div>
+			<div class="meter" id="ggjitter"></div>
         </div>
 		<div id="ip"></div>
         <div>

+ 1 - 1
garbage.php

@@ -17,7 +17,7 @@ header("Pragma: no-cache");
 // Generate data
 $data=openssl_random_pseudo_bytes(1048576);
 // Deliver chunks of 1048576 bytes
-for($i=0;$i<100;$i++){
+for($i=0;$i<intval($_GET["ckSize"]);$i++){
     echo $data;
     flush();
 }

+ 280 - 98
speedtest_worker.js

@@ -1,30 +1,129 @@
-var testStatus=0,dlStatus="",ulStatus="",pingStatus="",clientIp="";
-var settings={time_ul:15,time_dl:15,count_ping:35,url_dl:"garbage.php",url_ul:"empty.dat",url_ping:"empty.dat",url_getIp:"getIP.php"};
-var xhr=null;
+/*
+	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"}
+*/
 this.addEventListener('message', function(e){
 	var params=e.data.split(" ");
-	if(params[0]=="status"){
-		postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp);
+	if(params[0]=="status"){ //return status
+		postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus);
 	}
-	if(params[0]=="start"&&testStatus==0){
+	if(params[0]=="start"&&testStatus==0){ //start new test
 		testStatus=1;
 		try{
+			//parse settings, if present
 			var s=JSON.parse(e.data.substring(5));
-			if(typeof s.url_dl != "undefined") settings.url_dl=s.url_dl;
-			if(typeof s.url_ul != "undefined") settings.url_ul=s.url_ul;
-			if(typeof s.url_ping != "undefined") settings.url_ping=s.url_ping;
-			if(typeof s.url_getIp != "undefined") settings.url_getIp=s.url_getIp;
-			if(typeof s.time_dl != "undefined") settings.time_dl=s.time_dl;
-			if(typeof s.time_ul != "undefined") settings.time_ul=s.time_ul;
-			if(typeof s.count_ping != "undefined") settings.count_ping=s.count_ping;
-		}catch(e){}
-		getIp(function(){dlTest(function(){testStatus=2;ulTest(function(){testStatus=3;pingTest(function(){testStatus=4;});});})});
+			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;
+		}catch(e){console.log(e)}
+		//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;});});})});
 	}
-	if(params[0]=="abort"){
-		try{if(xhr)xhr.abort();}catch(e){}
-		testStatus=5;dlStatus="";ulStatus="";pingStatus="";
+	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;
+	}
+}
+//gets client's IP using url_getIp, then calls the done function
 function getIp(done){
 	xhr=new XMLHttpRequest();
 	xhr.onload=function(){
@@ -37,101 +136,184 @@ function getIp(done){
 	xhr.open("GET",settings.url_getIp+"?r="+Math.random(),true);
 	xhr.send();
 }
-var dlCalled=false;
+//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;
-    var firstTick=true,startT=new Date().getTime(), prevT=new Date().getTime(),prevLoaded=0,speed=0.0;
-    xhr=new XMLHttpRequest();
-    xhr.onprogress=function(event){
-        var instspd=event.loaded<=0?speed:((event.loaded-prevLoaded)/((new Date().getTime()-prevT)/1000.0))*1.25;
-        if(isNaN(instspd)||!isFinite(instspd)||instspd<0) return;
-        if(firstTick){
-            speed=instspd;
-            firstTick=false;
-        }else{
-            speed=speed*0.9+instspd*0.1;
-        }
-        prevLoaded=event.loaded;
-        prevT=new Date().getTime();
-        dlStatus=((speed*8)/1048576.0).toFixed(2);
-        if(((prevT-startT)/1000.0)>settings.time_dl){xhr.onprogress=null; xhr.onload=null; xhr.onerror=null; try{xhr.abort();}catch(e){} xhr=null; done();}
-    }.bind(this);
-    xhr.onload=function(){
-		prevT=new Date().getTime(); prevLoaded=0; fistTick=true;
-        xhr.open("GET",settings.url_dl+"?r="+Math.random(),true);
-		xhr.send();
-    }.bind(this);
-    xhr.onerror=function(){
-        dlStatus="Fail";
-		try{xhr.abort();}catch(e){}
-        xhr=null;
-        done();
-    }.bind(this);
-    xhr.open("GET",settings.url_dl+"?r="+Math.random(),true);
-    xhr.send();
+	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
+				if(settings.xhr_dlUseBlob) xhr[i].responseType='blob'; else xhr[i].responseType='arraybuffer';
+				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);
 }
-var ulCalled=false;
+//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
 function ulTest(done){
-	if(ulCalled) return; else ulCalled=true;
-    var firstTick=true,startT=new Date().getTime(), prevT=new Date().getTime(),prevLoaded=0,speed=0.0;
-    xhr=new XMLHttpRequest();
-    xhr.upload.onprogress=function(event){
-        var instspd=event.loaded<=0?speed:((event.loaded-prevLoaded)/((new Date().getTime()-prevT)/1000.0))*1.25;
-        if(isNaN(instspd)||!isFinite(instspd)||instspd<0) return;
-        if(firstTick){
-            firstTick=false;
-        }else{
-            speed=instspd<speed?(speed*0.4+instspd*0.6):(speed*0.8+instspd*0.2);
-        }
-        prevLoaded=event.loaded;
-        prevT=new Date().getTime();
-        ulStatus=((speed*8)/1048576.0).toFixed(2);
-        if(((prevT-startT)/1000.0)>settings.time_ul){xhr.upload.onprogress=null; xhr.upload.onload=null; xhr.upload.onerror=null; try{xhr.abort();}catch(e){} xhr=null; done();}
-    }.bind(this);
-    xhr.upload.onload=function(){
-		prevT=new Date().getTime(); prevLoaded=0; fistTick=true;
-        xhr.open("POST",settings.url_ul+"?r="+Math.random(),true);
-		xhr.send(r);
-    }.bind(this);
-    xhr.upload.onerror=function(){
-        ulStatus="Fail";
-		try{xhr.abort();}catch(e){}
-		xhr=null;
-        done();
-    }.bind(this);
-    xhr.open("POST",settings.url_ul+"?r="+Math.random(),true);
-	xhr.setRequestHeader('Content-Encoding','identity');
-	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);
-    xhr.send(req);
+	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);
 }
-var ptCalled=false;
+//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;
-    var prevT=null,ping=0.0,i=0;
+	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
     var doPing=function(){
         prevT=new Date().getTime();
-        xhr=new XMLHttpRequest();
-        xhr.onload=function(){
+        xhr[0]=new XMLHttpRequest();
+        xhr[0].onload=function(){
+			//pong
             if(i==0){
-                prevT=new Date().getTime();
+                prevT=new Date().getTime(); //first pong
             }else{
                 var instspd=(new Date().getTime()-prevT)/2;
-                if(i==1)ping=instspd; else ping=ping*0.9+instspd*0.1;
+				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;
             }
             pingStatus=ping.toFixed(2);
+			jitterStatus=jitter.toFixed(2);
             i++;
-            if(i<settings.count_ping) doPing(); else done();
+            if(i<settings.count_ping) doPing(); else done(); //more pings to do?
         }.bind(this);
-        xhr.onerror=function(){
+        xhr[0].onerror=function(){
+			//a ping failed, cancel test
             pingStatus="Fail";
+			jitterStatus="Fail";
+			clearRequests();
             done();
         }.bind(this);
-        xhr.open("GET",settings.url_ping+"?r="+Math.random(),true);
-        xhr.send();
+		//sent xhr
+        xhr[0].open("GET",settings.url_ping+"?r="+Math.random(),true); //random string to prevent caching
+        xhr[0].send();
     }.bind(this);
-    doPing();
-}
+    doPing(); //start first ping
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
speedtest_worker.min.js


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio