diff --git a/doc.md b/doc.md
index 92bd669a445d83c59ea3b0e89f9c34e44675397c..5b67e68a3c35bcb8ecba253df2f4554f08d8868b 100644
--- a/doc.md
+++ b/doc.md
@@ -1,7 +1,7 @@
 # HTML5 Speedtest
 
 > by Federico Dossena  
-> Version 4.5.2, February 9, 2018
+> Version 4.5.3, February 23, 2018
 > [https://github.com/adolfintel/speedtest/](https://github.com/adolfintel/speedtest/)
 
 
@@ -217,6 +217,11 @@ w.postMessage('start '+JSON.stringify(params))
     * __Important:__ On Firefox, it is better to run the upload test last
 * __getIp_ispInfo__: if true, the server will try to get ISP info and pass it along with the IP address. This will add `isp=true` to the request to `url_getIp`. getIP.php accomplishes this using ipinfo.io
     * Default: `true`
+* __getIp_ispInfo_distance__: if true, the server will try to get an estimate of the distance from the client to the speedtest server. This will add a `distance` argument to the request to `url_getIp`. `__getIp_ispInfo__` must be enabled in order for this to work. getIP.php accomplishes this using ipinfo.io
+    * `km`: estimate distance in kilometers
+    * `mi`: estimate distance in miles
+    * not set: do not measure distance
+    * Default: `km`
 * __enable_quirks__: enables browser-specific optimizations. These optimizations override some of the default settings. They do not override settings that are explicitly set.
     * Default: `true`
 * __garbagePhp_chunkSize__: size of chunks sent by garbage.php in megabytes
diff --git a/getIP.php b/getIP.php
index 3fc781ffe5a445a351a5db0fa7b19aced7f4f842..97bfe53633db6804d067cf1d4eb67439691bdcf4 100644
--- a/getIP.php
+++ b/getIP.php
@@ -11,12 +11,58 @@
         $ip=$_SERVER['REMOTE_ADDR'];
     }
     $ip=preg_replace("/^::ffff:/", "", $ip);
-    $isp="";
+	/**
+	 * Optimized algorithm from http://www.codexworld.com
+	 *
+	 * @param float $latitudeFrom
+	 * @param float $longitudeFrom
+	 * @param float $latitudeTo
+	 * @param float $longitudeTo
+	 *
+	 * @return float [km]
+	 */
+	function distance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo){
+		$rad = M_PI / 180;
+		$theta = $longitudeFrom - $longitudeTo;
+		$dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) +  cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad);
+		return acos($dist) / $rad * 60 *  1.853;
+	}
     if(isset($_GET["isp"])){
-        $json = file_get_contents("https://ipinfo.io/".$ip."/json");
-        $details = json_decode($json,true);
-        if(array_key_exists("org",$details)) $isp.=$details["org"]; else $isp.="Unknown ISP";
-        if(array_key_exists("country",$details)) $isp.=" (".$details["country"].")";
+		$isp="";
+        try{
+            $json = file_get_contents("https://ipinfo.io/".$ip."/json");
+            $details = json_decode($json,true);
+            if(array_key_exists("org",$details)) $isp.=$details["org"]; else $isp.="Unknown ISP";
+            if(array_key_exists("country",$details)) $isp.=", ".$details["country"];
+            $clientLoc=NULL; $serverLoc=NULL;
+            if(array_key_exists("loc",$details)) $clientLoc=$details["loc"];
+            if(isset($_GET["distance"])){
+                if($clientLoc){
+                    $json = file_get_contents("https://ipinfo.io/json");
+                    $details = json_decode($json,true);
+                    if(array_key_exists("loc",$details)) $serverLoc=$details["loc"];
+                    if($serverLoc){
+                        try{
+                            $clientLoc=explode(",",$clientLoc);
+                            $serverLoc=explode(",",$serverLoc);
+                            $dist=distance($clientLoc[0],$clientLoc[1],$serverLoc[0],$serverLoc[1]);
+                            if($_GET["distance"]=="mi"){
+                                $dist/=1.609344;
+                                $dist=round($dist,-1);
+                                if($dist<15) $dist="<15";
+                                $isp.=" (".$dist." mi)";
+                            }else if($_GET["distance"]=="km"){
+                                $dist=round($dist,-1);
+                                if($dist<20) $dist="<20";
+                                $isp.=" (".$dist." km)";
+                            }
+                        }catch(Exception $e){}
+                    }
+                }
+            }
+        }catch(Exception $ex){
+            $isp="Unknown ISP";
+        }
         echo $ip." - ".$isp;
     } else echo $ip;
 ?>
diff --git a/speedtest_worker.js b/speedtest_worker.js
index 0565022031d86787857645974e8ba9ed66c6770d..1d61a27109b5eab3184653f9219f6c83caadf220 100644
--- a/speedtest_worker.js
+++ b/speedtest_worker.js
@@ -1,5 +1,5 @@
 /*
-	HTML5 Speedtest v4.5.2
+	HTML5 Speedtest v4.5.3
 	by Federico Dossena
 	https://github.com/adolfintel/speedtest/
 	GNU LGPLv3 License
@@ -33,6 +33,7 @@ var settings = {
   url_ping: 'empty.php', // 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
   getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address
+  getIp_ispInfo_distance: 'km', //km or mi=estimate distance from server in km/mi; set to false to disable distance estimation. getIp_ispInfo must be enabled in order for this to work
   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_multistreamDelay: 300, //how much concurrent requests should be delayed
@@ -168,7 +169,7 @@ function getIp (done) {
 	tlog('getIp failed')
     done()
   }
-  xhr.open('GET', settings.url_getIp + url_sep(settings.url_getIp) + (settings.getIp_ispInfo?"isp=true":"") + 'r=' + Math.random(), true)
+  xhr.open('GET', settings.url_getIp + url_sep(settings.url_getIp) + (settings.getIp_ispInfo?("isp=true"+(settings.getIp_ispInfo_distance?("&distance="+settings.getIp_ispInfo_distance+"&"):"&")):"&") + 'r=' + Math.random(), true)
   xhr.send()
 }
 // download test, calls done function when it's over
diff --git a/speedtest_worker.min.js b/speedtest_worker.min.js
index b58031a5637feb495e1d5c5ac2ac98f5a53bd72b..74e4608265b6c0f6bafd4017819659a3c600c948 100644
--- a/speedtest_worker.min.js
+++ b/speedtest_worker.min.js
@@ -1 +1 @@
-function tlog(s){log+=Date.now()+": "+s+"\n"}function twarn(s){log+=Date.now()+" WARN: "+s+"\n",console.warn(s)}function url_sep(url){return url.match(/\?/)?"&":"?"}function clearRequests(){if(tlog("stopping pending XHRs"),xhr){for(var i=0;i<xhr.length;i++){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}}function getIp(done){tlog("getIp"),ipCalled||(ipCalled=!0,xhr=new XMLHttpRequest,xhr.onload=function(){tlog("IP: "+xhr.responseText),clientIp=xhr.responseText,done()},xhr.onerror=function(){tlog("getIp failed"),done()},xhr.open("GET",settings.url_getIp+url_sep(settings.url_getIp)+(settings.getIp_ispInfo?"isp=true":"")+"r="+Math.random(),!0),xhr.send())}function dlTest(done){if(tlog("dlTest"),!dlCalled){dlCalled=!0;var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(1===testStatus){tlog("dl test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x,xhr[i].onprogress=function(event){if(tlog("dl stream progress event "+i+" "+event.loaded),1!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].onload=function(){tlog("dl stream finished "+i);try{xhr[i].abort()}catch(e){}testStream(i,0)}.bind(this),xhr[i].onerror=function(){tlog("dl stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)}.bind(this);try{settings.xhr_dlUseBlob?xhr[i].responseType="blob":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,!0),xhr[i].send()}}.bind(this),1+delay)}.bind(this),i=0;i<settings.xhr_dlMultistream;i++)testStream(i,settings.xhr_multistreamDelay*i);interval=setInterval(function(){tlog("DL: "+dlStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(graceTimeDone&&(dlProgress=t/(1e3*settings.time_dl)),!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);dlStatus=(8*speed*settings.overheadCompensationFactor/(settings.useMebibits?1048576:1e6)).toFixed(2),(t/1e3>settings.time_dl&&dlStatus>0||failed)&&((failed||isNaN(dlStatus))&&(dlStatus="Fail"),clearRequests(),clearInterval(interval),dlProgress=1,tlog("dlTest finished "+dlStatus),done())}else t>1e3*settings.time_dlGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function ulTest(done){if(tlog("ulTest"),!ulCalled){ulCalled=!0;var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(3===testStatus){tlog("ul test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x;var ie11workaround;if(settings.forceIE11Workaround)ie11workaround=!0;else try{xhr[i].upload.onprogress,ie11workaround=!1}catch(e){ie11workaround=!0}ie11workaround?(xhr[i].onload=function(){tlog("ul stream progress event (ie11wa)"),totLoaded+=reqsmall.size,testStream(i,0)},xhr[i].onerror=function(){tlog("ul stream failed (ie11wa)"),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)},xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(reqsmall)):(xhr[i].upload.onprogress=function(event){if(tlog("ul stream progress event "+i+" "+event.loaded),3!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].upload.onload=function(){tlog("ul stream finished "+i),testStream(i,0)}.bind(this),xhr[i].upload.onerror=function(){tlog("ul stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)}.bind(this),xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(req))}}.bind(this),1)}.bind(this),i=0;i<settings.xhr_ulMultistream;i++)testStream(i,settings.xhr_multistreamDelay*i);interval=setInterval(function(){tlog("UL: "+ulStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(graceTimeDone&&(ulProgress=t/(1e3*settings.time_ul)),!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);ulStatus=(8*speed*settings.overheadCompensationFactor/(settings.useMebibits?1048576:1e6)).toFixed(2),(t/1e3>settings.time_ul&&ulStatus>0||failed)&&((failed||isNaN(ulStatus))&&(ulStatus="Fail"),clearRequests(),clearInterval(interval),ulProgress=1,tlog("ulTest finished "+ulStatus),done())}else t>1e3*settings.time_ulGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function pingTest(done){if(tlog("pingTest"),!ptCalled){ptCalled=!0;var prevT=null,ping=0,jitter=0,i=0,prevInstspd=0;xhr=[];var doPing=function(){tlog("ping"),pingProgress=i/settings.count_ping,prevT=(new Date).getTime(),xhr[0]=new XMLHttpRequest,xhr[0].onload=function(){if(tlog("pong"),0===i)prevT=(new Date).getTime();else{var instspd=(new Date).getTime()-prevT;if(settings.ping_allowPerformanceApi)try{var p=performance.getEntries();p=p[p.length-1];var d=p.responseStart-p.requestStart;0>=d&&(d=p.duration),d>0&&instspd>d&&(instspd=d)}catch(e){tlog("Performance API not supported, using estimate")}var instjitter=Math.abs(instspd-prevInstspd);1===i?ping=instspd:(ping=.9*ping+.1*instspd,jitter=instjitter>jitter?.2*jitter+.8*instjitter:.9*jitter+.1*instjitter),prevInstspd=instspd}pingStatus=ping.toFixed(2),jitterStatus=jitter.toFixed(2),i++,tlog("PING: "+pingStatus+" JITTER: "+jitterStatus),i<settings.count_ping?doPing():(pingProgress=1,done())}.bind(this),xhr[0].onerror=function(){tlog("ping failed"),0===settings.xhr_ignoreErrors&&(pingStatus="Fail",jitterStatus="Fail",clearRequests(),done()),1===settings.xhr_ignoreErrors&&doPing(),2===settings.xhr_ignoreErrors&&(i++,i<settings.count_ping?doPing():done())}.bind(this),xhr[0].open("GET",settings.url_ping+url_sep(settings.url_ping)+"r="+Math.random(),!0),xhr[0].send()}.bind(this);doPing()}}function sendTelemetry(){if(!(settings.telemetry_level<1)){xhr=new XMLHttpRequest,xhr.onload=function(){console.log("TELEMETRY OL "+xhr.responseText)},xhr.onerror=function(){console.log("TELEMETRY ERROR "+xhr)},xhr.open("POST",settings.url_telemetry+"?r="+Math.random(),!0);try{var fd=new FormData;fd.append("dl",dlStatus),fd.append("ul",ulStatus),fd.append("ping",pingStatus),fd.append("jitter",jitterStatus),fd.append("log",settings.telemetry_level>1?log:""),xhr.send(fd)}catch(ex){var postData="dl="+encodeURIComponent(dlStatus)+"&ul="+encodeURIComponent(ulStatus)+"&ping="+encodeURIComponent(pingStatus)+"&jitter="+encodeURIComponent(jitterStatus)+"&log="+encodeURIComponent(settings.telemetry_level>1?log:"");xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.send(postData)}}}var testStatus=-1,dlStatus="",ulStatus="",pingStatus="",jitterStatus="",clientIp="",dlProgress=0,ulProgress=0,pingProgress=0,log="",settings={test_order:"IP_D_U",time_ul:15,time_dl:15,time_ulGraceTime:3,time_dlGraceTime:1.5,count_ping:35,url_dl:"garbage.php",url_ul:"empty.php",url_ping:"empty.php",url_getIp:"getIP.php",getIp_ispInfo:!0,xhr_dlMultistream:10,xhr_ulMultistream:3,xhr_multistreamDelay:300,xhr_ignoreErrors:1,xhr_dlUseBlob:!1,garbagePhp_chunkSize:20,enable_quirks:!0,ping_allowPerformanceApi:!0,overheadCompensationFactor:1.06,useMebibits:!1,telemetry_level:0,url_telemetry:"telemetry.php"},xhr=null,interval=null,test_pointer=0;this.addEventListener("message",function(e){var params=e.data.split(" ");if("status"===params[0]&&postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus+";"+dlProgress+";"+ulProgress+";"+pingProgress),"start"===params[0]&&-1===testStatus){testStatus=0;try{var s={};try{var ss=e.data.substring(5);ss&&(s=JSON.parse(ss))}catch(e){twarn("Error parsing custom settings JSON. Please check your syntax")}for(var key in s)"undefined"!=typeof settings[key]?settings[key]=s[key]:twarn("Unknown setting ignored: "+key);if(settings.enable_quirks||"undefined"!=typeof s.enable_quirks&&s.enable_quirks){var ua=navigator.userAgent;/Firefox.(\d+\.\d+)/i.test(ua)&&"undefined"==typeof s.xhr_ulMultistream&&(settings.xhr_ulMultistream=1),/Edge.(\d+\.\d+)/i.test(ua)&&"undefined"==typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=3),/Chrome.(\d+)/i.test(ua)&&self.fetch&&"undefined"==typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=5)}/Edge.(\d+\.\d+)/i.test(ua)&&(settings.forceIE11Workaround=!0),"undefined"!=typeof s.telemetry_level&&(settings.telemetry_level="basic"===s.telemetry_level?1:"full"===s.telemetry_level?2:0),settings.test_order=settings.test_order.toUpperCase()}catch(e){twarn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e)}tlog(JSON.stringify(settings)),test_pointer=0;var iRun=!1,dRun=!1,uRun=!1,pRun=!1,runNextTest=function(){if(5!=testStatus){if(test_pointer>=settings.test_order.length)return testStatus=4,void sendTelemetry();switch(settings.test_order.charAt(test_pointer)){case"I":if(test_pointer++,iRun)return void runNextTest();iRun=!0,getIp(runNextTest);break;case"D":if(test_pointer++,dRun)return void runNextTest();dRun=!0,testStatus=1,dlTest(runNextTest);break;case"U":if(test_pointer++,uRun)return void runNextTest();uRun=!0,testStatus=3,ulTest(runNextTest);break;case"P":if(test_pointer++,pRun)return void runNextTest();pRun=!0,testStatus=2,pingTest(runNextTest);break;case"_":test_pointer++,setTimeout(runNextTest,1e3);break;default:test_pointer++}}};runNextTest()}"abort"===params[0]&&(tlog("manually aborted"),clearRequests(),runNextTest=null,interval&&clearInterval(interval),settings.telemetry_level>1&&sendTelemetry(),testStatus=5,dlStatus="",ulStatus="",pingStatus="",jitterStatus="")});var ipCalled=!1,dlCalled=!1,r=new ArrayBuffer(1048576);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}for(var req=[],reqsmall=[],i=0;20>i;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=!1,ptCalled=!1;
+function tlog(s){log+=Date.now()+": "+s+"\n"}function twarn(s){log+=Date.now()+" WARN: "+s+"\n",console.warn(s)}function url_sep(url){return url.match(/\?/)?"&":"?"}function clearRequests(){if(tlog("stopping pending XHRs"),xhr){for(var i=0;i<xhr.length;i++){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}}function getIp(done){tlog("getIp"),ipCalled||(ipCalled=!0,xhr=new XMLHttpRequest,xhr.onload=function(){tlog("IP: "+xhr.responseText),clientIp=xhr.responseText,done()},xhr.onerror=function(){tlog("getIp failed"),done()},xhr.open("GET",settings.url_getIp+url_sep(settings.url_getIp)+(settings.getIp_ispInfo?"isp=true"+(settings.getIp_ispInfo_distance?"&distance="+settings.getIp_ispInfo_distance+"&":"&"):"&")+"r="+Math.random(),!0),xhr.send())}function dlTest(done){if(tlog("dlTest"),!dlCalled){dlCalled=!0;var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(1===testStatus){tlog("dl test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x,xhr[i].onprogress=function(event){if(tlog("dl stream progress event "+i+" "+event.loaded),1!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].onload=function(){tlog("dl stream finished "+i);try{xhr[i].abort()}catch(e){}testStream(i,0)}.bind(this),xhr[i].onerror=function(){tlog("dl stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)}.bind(this);try{settings.xhr_dlUseBlob?xhr[i].responseType="blob":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,!0),xhr[i].send()}}.bind(this),1+delay)}.bind(this),i=0;i<settings.xhr_dlMultistream;i++)testStream(i,settings.xhr_multistreamDelay*i);interval=setInterval(function(){tlog("DL: "+dlStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(graceTimeDone&&(dlProgress=t/(1e3*settings.time_dl)),!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);dlStatus=(8*speed*settings.overheadCompensationFactor/(settings.useMebibits?1048576:1e6)).toFixed(2),(t/1e3>settings.time_dl&&dlStatus>0||failed)&&((failed||isNaN(dlStatus))&&(dlStatus="Fail"),clearRequests(),clearInterval(interval),dlProgress=1,tlog("dlTest finished "+dlStatus),done())}else t>1e3*settings.time_dlGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function ulTest(done){if(tlog("ulTest"),!ulCalled){ulCalled=!0;var totLoaded=0,startT=(new Date).getTime(),graceTimeDone=!1,failed=!1;xhr=[];for(var testStream=function(i,delay){setTimeout(function(){if(3===testStatus){tlog("ul test stream started "+i+" "+delay);var prevLoaded=0,x=new XMLHttpRequest;xhr[i]=x;var ie11workaround;if(settings.forceIE11Workaround)ie11workaround=!0;else try{xhr[i].upload.onprogress,ie11workaround=!1}catch(e){ie11workaround=!0}ie11workaround?(xhr[i].onload=function(){tlog("ul stream progress event (ie11wa)"),totLoaded+=reqsmall.size,testStream(i,0)},xhr[i].onerror=function(){tlog("ul stream failed (ie11wa)"),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)},xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(reqsmall)):(xhr[i].upload.onprogress=function(event){if(tlog("ul stream progress event "+i+" "+event.loaded),3!==testStatus)try{x.abort()}catch(e){}var loadDiff=event.loaded<=0?0:event.loaded-prevLoaded;isNaN(loadDiff)||!isFinite(loadDiff)||0>loadDiff||(totLoaded+=loadDiff,prevLoaded=event.loaded)}.bind(this),xhr[i].upload.onload=function(){tlog("ul stream finished "+i),testStream(i,0)}.bind(this),xhr[i].upload.onerror=function(){tlog("ul stream failed "+i),0===settings.xhr_ignoreErrors&&(failed=!0);try{xhr[i].abort()}catch(e){}delete xhr[i],1===settings.xhr_ignoreErrors&&testStream(i,0)}.bind(this),xhr[i].open("POST",settings.url_ul+url_sep(settings.url_ul)+"r="+Math.random(),!0),xhr[i].setRequestHeader("Content-Encoding","identity"),xhr[i].send(req))}}.bind(this),1)}.bind(this),i=0;i<settings.xhr_ulMultistream;i++)testStream(i,settings.xhr_multistreamDelay*i);interval=setInterval(function(){tlog("UL: "+ulStatus+(graceTimeDone?"":" (in grace time)"));var t=(new Date).getTime()-startT;if(graceTimeDone&&(ulProgress=t/(1e3*settings.time_ul)),!(200>t))if(graceTimeDone){var speed=totLoaded/(t/1e3);ulStatus=(8*speed*settings.overheadCompensationFactor/(settings.useMebibits?1048576:1e6)).toFixed(2),(t/1e3>settings.time_ul&&ulStatus>0||failed)&&((failed||isNaN(ulStatus))&&(ulStatus="Fail"),clearRequests(),clearInterval(interval),ulProgress=1,tlog("ulTest finished "+ulStatus),done())}else t>1e3*settings.time_ulGraceTime&&(totLoaded>0&&(startT=(new Date).getTime(),totLoaded=0),graceTimeDone=!0)}.bind(this),200)}}function pingTest(done){if(tlog("pingTest"),!ptCalled){ptCalled=!0;var prevT=null,ping=0,jitter=0,i=0,prevInstspd=0;xhr=[];var doPing=function(){tlog("ping"),pingProgress=i/settings.count_ping,prevT=(new Date).getTime(),xhr[0]=new XMLHttpRequest,xhr[0].onload=function(){if(tlog("pong"),0===i)prevT=(new Date).getTime();else{var instspd=(new Date).getTime()-prevT;if(settings.ping_allowPerformanceApi)try{var p=performance.getEntries();p=p[p.length-1];var d=p.responseStart-p.requestStart;0>=d&&(d=p.duration),d>0&&instspd>d&&(instspd=d)}catch(e){tlog("Performance API not supported, using estimate")}var instjitter=Math.abs(instspd-prevInstspd);1===i?ping=instspd:(ping=.9*ping+.1*instspd,jitter=instjitter>jitter?.2*jitter+.8*instjitter:.9*jitter+.1*instjitter),prevInstspd=instspd}pingStatus=ping.toFixed(2),jitterStatus=jitter.toFixed(2),i++,tlog("PING: "+pingStatus+" JITTER: "+jitterStatus),i<settings.count_ping?doPing():(pingProgress=1,done())}.bind(this),xhr[0].onerror=function(){tlog("ping failed"),0===settings.xhr_ignoreErrors&&(pingStatus="Fail",jitterStatus="Fail",clearRequests(),done()),1===settings.xhr_ignoreErrors&&doPing(),2===settings.xhr_ignoreErrors&&(i++,i<settings.count_ping?doPing():done())}.bind(this),xhr[0].open("GET",settings.url_ping+url_sep(settings.url_ping)+"r="+Math.random(),!0),xhr[0].send()}.bind(this);doPing()}}function sendTelemetry(){if(!(settings.telemetry_level<1)){xhr=new XMLHttpRequest,xhr.onload=function(){console.log("TELEMETRY OL "+xhr.responseText)},xhr.onerror=function(){console.log("TELEMETRY ERROR "+xhr)},xhr.open("POST",settings.url_telemetry+"?r="+Math.random(),!0);try{var fd=new FormData;fd.append("dl",dlStatus),fd.append("ul",ulStatus),fd.append("ping",pingStatus),fd.append("jitter",jitterStatus),fd.append("log",settings.telemetry_level>1?log:""),xhr.send(fd)}catch(ex){var postData="dl="+encodeURIComponent(dlStatus)+"&ul="+encodeURIComponent(ulStatus)+"&ping="+encodeURIComponent(pingStatus)+"&jitter="+encodeURIComponent(jitterStatus)+"&log="+encodeURIComponent(settings.telemetry_level>1?log:"");xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),xhr.send(postData)}}}var testStatus=-1,dlStatus="",ulStatus="",pingStatus="",jitterStatus="",clientIp="",dlProgress=0,ulProgress=0,pingProgress=0,log="",settings={test_order:"IP_D_U",time_ul:15,time_dl:15,time_ulGraceTime:3,time_dlGraceTime:1.5,count_ping:35,url_dl:"garbage.php",url_ul:"empty.php",url_ping:"empty.php",url_getIp:"getIP.php",getIp_ispInfo:!0,getIp_ispInfo_distance:"km",xhr_dlMultistream:10,xhr_ulMultistream:3,xhr_multistreamDelay:300,xhr_ignoreErrors:1,xhr_dlUseBlob:!1,garbagePhp_chunkSize:20,enable_quirks:!0,ping_allowPerformanceApi:!0,overheadCompensationFactor:1.06,useMebibits:!1,telemetry_level:0,url_telemetry:"telemetry.php"},xhr=null,interval=null,test_pointer=0;this.addEventListener("message",function(e){var params=e.data.split(" ");if("status"===params[0]&&postMessage(testStatus+";"+dlStatus+";"+ulStatus+";"+pingStatus+";"+clientIp+";"+jitterStatus+";"+dlProgress+";"+ulProgress+";"+pingProgress),"start"===params[0]&&-1===testStatus){testStatus=0;try{var s={};try{var ss=e.data.substring(5);ss&&(s=JSON.parse(ss))}catch(e){twarn("Error parsing custom settings JSON. Please check your syntax")}for(var key in s)"undefined"!=typeof settings[key]?settings[key]=s[key]:twarn("Unknown setting ignored: "+key);if(settings.enable_quirks||"undefined"!=typeof s.enable_quirks&&s.enable_quirks){var ua=navigator.userAgent;/Firefox.(\d+\.\d+)/i.test(ua)&&"undefined"==typeof s.xhr_ulMultistream&&(settings.xhr_ulMultistream=1),/Edge.(\d+\.\d+)/i.test(ua)&&"undefined"==typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=3),/Chrome.(\d+)/i.test(ua)&&self.fetch&&"undefined"==typeof s.xhr_dlMultistream&&(settings.xhr_dlMultistream=5)}/Edge.(\d+\.\d+)/i.test(ua)&&(settings.forceIE11Workaround=!0),"undefined"!=typeof s.telemetry_level&&(settings.telemetry_level="basic"===s.telemetry_level?1:"full"===s.telemetry_level?2:0),settings.test_order=settings.test_order.toUpperCase()}catch(e){twarn("Possible error in custom test settings. Some settings may not be applied. Exception: "+e)}tlog(JSON.stringify(settings)),test_pointer=0;var iRun=!1,dRun=!1,uRun=!1,pRun=!1,runNextTest=function(){if(5!=testStatus){if(test_pointer>=settings.test_order.length)return testStatus=4,void sendTelemetry();switch(settings.test_order.charAt(test_pointer)){case"I":if(test_pointer++,iRun)return void runNextTest();iRun=!0,getIp(runNextTest);break;case"D":if(test_pointer++,dRun)return void runNextTest();dRun=!0,testStatus=1,dlTest(runNextTest);break;case"U":if(test_pointer++,uRun)return void runNextTest();uRun=!0,testStatus=3,ulTest(runNextTest);break;case"P":if(test_pointer++,pRun)return void runNextTest();pRun=!0,testStatus=2,pingTest(runNextTest);break;case"_":test_pointer++,setTimeout(runNextTest,1e3);break;default:test_pointer++}}};runNextTest()}"abort"===params[0]&&(tlog("manually aborted"),clearRequests(),runNextTest=null,interval&&clearInterval(interval),settings.telemetry_level>1&&sendTelemetry(),testStatus=5,dlStatus="",ulStatus="",pingStatus="",jitterStatus="")});var ipCalled=!1,dlCalled=!1,r=new ArrayBuffer(1048576);try{r=new Float32Array(r);for(var i=0;i<r.length;i++)r[i]=Math.random()}catch(e){}for(var req=[],reqsmall=[],i=0;20>i;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=!1,ptCalled=!1;