├── favicon.ico ├── preview.png ├── backend ├── getIP_ipInfo_apikey.php ├── empty.php ├── garbage.php └── getIP.php ├── entrypoint.sh ├── README.md ├── Dockerfile ├── index.html ├── single-nogitalk.html ├── single.html ├── mult-pretty.html ├── mult.html ├── speedtest.js └── speedtest_worker.js /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jialezi/html5-speedtest/master/favicon.ico -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jialezi/html5-speedtest/master/preview.png -------------------------------------------------------------------------------- /backend/getIP_ipInfo_apikey.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "Start..." 3 | sed -i "s/Server1/$node/" /var/www/localhost/htdocs/index.html 4 | httpd -D FOREGROUND -------------------------------------------------------------------------------- /backend/empty.php: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # html5-speedtest 2 | 3 | #### 使用 4 | 5 | 1.按教程申请gitalk应用(https://github.com/gitalk/gitalk) 6 | 7 | 2.修改single.html或mult.html或mult-pretty.html里面对应的gitalk参数 8 | 9 | 10 | 11 | >注: 12 | >index.html和single-nogitalk.html为不带gitalk的原始测速页面 13 | >single.html为带gitalk的单服务器测速页面 14 | >mult.html和mult-pretty.html为带gitalk的多服务器测速页面(需要自行修改添加 LIST OF TEST SERVERS) 15 | 16 | 17 | #### docker 18 | 19 | ``` 20 | ###环境变量node为节点名称 21 | 22 | docker run -d -p 80:80 -e node=Server jialezi/html5-speedtest 23 | 24 | ``` 25 | 26 | #### preview 27 | 28 | ![preview](https://github.com/jialezi/html5-speedtest/raw/master/preview.png) 29 | 30 | 31 | 32 | ### html5-speedtest项目地址:https://github.com/adolfintel/speedtest 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN (apk --no-cache upgrade ;\ 4 | apk add php7-apache2 curl php7-cli php7-json php7-phar php7-openssl php7-mbstring php7-zlib ;\ 5 | curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer ;\ 6 | sed -i "s/AllowOverride none/AllowOverride All/" /etc/apache2/httpd.conf ;\ 7 | sed -i "s/#ServerName www.example.com:80/ServerName localhost:80/" /etc/apache2/httpd.conf ;\ 8 | rm -f /var/www/localhost/htdocs/* ;\ 9 | mkdir /run/apache2/ ;\ 10 | rm -f /var/cache/apk/* ) 11 | 12 | ADD entrypoint.sh /entrypoint.sh 13 | ADD . /var/www/localhost/htdocs/ 14 | 15 | RUN (chown -R apache:apache /var/www/localhost/htdocs/ ;\ 16 | chmod 755 /entrypoint.sh ) 17 | 18 | ENV node Server 19 | 20 | EXPOSE 80 21 | 22 | ENTRYPOINT [ "sh", "/entrypoint.sh" ] -------------------------------------------------------------------------------- /backend/garbage.php: -------------------------------------------------------------------------------- 1 | 1024){$chunks = 1024;} 27 | for($i=0;$i<$chunks;$i++){ 28 | echo $data; 29 | flush(); 30 | } 31 | ?> 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Server1 - HTML5 Speedtest 7 | 8 | 9 | 52 | 53 | 174 | 175 | 176 |

HTML5 Speedtest

177 |
Server1

178 |
179 | 180 | 181 |
182 |
183 |
184 |
185 |
Download
186 |
187 |
Mbps
188 |
189 |
190 |
Upload
191 |
192 |
Mbps
193 |
194 |
195 |
196 |
197 |
Ping
198 |
199 |
ms
200 |
201 |
202 |
Jitter
203 |
204 |
ms
205 |
206 |
207 |
208 | IP Address: 209 |
210 |
211 | Original Source - Modified by jialezi
212 | 215 | 216 | -------------------------------------------------------------------------------- /single-nogitalk.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Server1 - HTML5 Speedtest 7 | 8 | 9 | 52 | 53 | 174 | 175 | 176 |

HTML5 Speedtest

177 |
Server1

178 |
179 | 180 | 181 |
182 |
183 |
184 |
185 |
Download
186 |
187 |
Mbps
188 |
189 |
190 |
Upload
191 |
192 |
Mbps
193 |
194 |
195 |
196 |
197 |
Ping
198 |
199 |
ms
200 |
201 |
202 |
Jitter
203 |
204 |
ms
205 |
206 |
207 |
208 | IP Address: 209 |
210 |
211 | Original Source - Modified by jialezi
212 | 215 | 216 | -------------------------------------------------------------------------------- /backend/getIP.php: -------------------------------------------------------------------------------- 1 | $ip . " - localhost IPv6 access", 'rawIspInfo' => ""]); 32 | die(); 33 | } 34 | if (stripos($ip, 'fe80:') === 0) { // simplified IPv6 link-local address (should match fe80::/10) 35 | echo json_encode(['processedString' => $ip . " - link-local IPv6 access", 'rawIspInfo' => ""]); 36 | die(); 37 | } 38 | if (strpos($ip, '127.') === 0) { //anything within the 127/8 range is localhost ipv4, the ip must start with 127.0 39 | echo json_encode(['processedString' => $ip . " - localhost IPv4 access", 'rawIspInfo' => ""]); 40 | die(); 41 | } 42 | if (strpos($ip, '10.') === 0) { // 10/8 private IPv4 43 | echo json_encode(['processedString' => $ip . " - private IPv4 access", 'rawIspInfo' => ""]); 44 | die(); 45 | } 46 | if (preg_match('/^172\.(1[6-9]|2\d|3[01])\./', $ip) === 1) { // 172.16/12 private IPv4 47 | echo json_encode(['processedString' => $ip . " - private IPv4 access", 'rawIspInfo' => ""]); 48 | die(); 49 | } 50 | if (strpos($ip, '192.168.') === 0) { // 192.168/16 private IPv4 51 | echo json_encode(['processedString' => $ip . " - private IPv4 access", 'rawIspInfo' => ""]); 52 | die(); 53 | } 54 | if (strpos($ip, '169.254.') === 0) { // IPv4 link-local 55 | echo json_encode(['processedString' => $ip . " - link-local IPv4 access", 'rawIspInfo' => ""]); 56 | die(); 57 | } 58 | 59 | /** 60 | * Optimized algorithm from http://www.codexworld.com 61 | * 62 | * @param float $latitudeFrom 63 | * @param float $longitudeFrom 64 | * @param float $latitudeTo 65 | * @param float $longitudeTo 66 | * 67 | * @return float [km] 68 | */ 69 | function distance($latitudeFrom, $longitudeFrom, $latitudeTo, $longitudeTo) { 70 | $rad = M_PI / 180; 71 | $theta = $longitudeFrom - $longitudeTo; 72 | $dist = sin($latitudeFrom * $rad) * sin($latitudeTo * $rad) + cos($latitudeFrom * $rad) * cos($latitudeTo * $rad) * cos($theta * $rad); 73 | return acos($dist) / $rad * 60 * 1.853; 74 | } 75 | function getIpInfoTokenString(){ 76 | $apikeyFile="getIP_ipInfo_apikey.php"; 77 | if(!file_exists($apikeyFile)) return ""; 78 | require $apikeyFile; 79 | if(empty($IPINFO_APIKEY)) return ""; 80 | return "?token=".$IPINFO_APIKEY; 81 | } 82 | if (isset($_GET["isp"])) { 83 | $isp = ""; 84 | $rawIspInfo=null; 85 | try { 86 | $json = file_get_contents("https://ipinfo.io/" . $ip . "/json".getIpInfoTokenString()); 87 | $details = json_decode($json, true); 88 | $rawIspInfo=$details; 89 | if (array_key_exists("org", $details)){ 90 | $isp .= $details["org"]; 91 | $isp=preg_replace("/AS\d{1,}\s/","",$isp); //Remove AS##### from ISP name, if present 92 | }else{ 93 | $isp .= "Unknown ISP"; 94 | } 95 | if (array_key_exists("country", $details)){ 96 | $isp .= ", " . $details["country"]; 97 | } 98 | $clientLoc = NULL; 99 | $serverLoc = NULL; 100 | if (array_key_exists("loc", $details)){ 101 | $clientLoc = $details["loc"]; 102 | } 103 | if (isset($_GET["distance"])) { 104 | if ($clientLoc) { 105 | $locFile="getIP_serverLocation.php"; 106 | $serverLoc=null; 107 | if(file_exists($locFile)){ 108 | require $locFile; 109 | }else{ 110 | $json = file_get_contents("https://ipinfo.io/json".getIpInfoTokenString()); 111 | $details = json_decode($json, true); 112 | if (array_key_exists("loc", $details)){ 113 | $serverLoc = $details["loc"]; 114 | } 115 | if($serverLoc){ 116 | $lf=fopen($locFile,"w"); 117 | fwrite($lf,chr(60)."?php\n"); 118 | fwrite($lf,'$serverLoc="'.addslashes($serverLoc).'";'); 119 | fwrite($lf,"\n"); 120 | fwrite($lf,"?".chr(62)); 121 | fclose($lf); 122 | } 123 | } 124 | if ($serverLoc) { 125 | try { 126 | $clientLoc = explode(",", $clientLoc); 127 | $serverLoc = explode(",", $serverLoc); 128 | $dist = distance($clientLoc[0], $clientLoc[1], $serverLoc[0], $serverLoc[1]); 129 | if ($_GET["distance"] == "mi") { 130 | $dist /= 1.609344; 131 | $dist = round($dist, -1); 132 | if ($dist < 15) 133 | $dist = "<15"; 134 | $isp .= " (" . $dist . " mi)"; 135 | }else if ($_GET["distance"] == "km") { 136 | $dist = round($dist, -1); 137 | if ($dist < 20) 138 | $dist = "<20"; 139 | $isp .= " (" . $dist . " km)"; 140 | } 141 | } catch (Exception $e) { 142 | 143 | } 144 | } 145 | } 146 | } 147 | } catch (Exception $ex) { 148 | $isp = "Unknown ISP"; 149 | } 150 | echo json_encode(['processedString' => $ip . " - " . $isp, 'rawIspInfo' => $rawIspInfo]); 151 | } else { 152 | echo json_encode(['processedString' => $ip, 'rawIspInfo' => ""]); 153 | } 154 | ?> 155 | -------------------------------------------------------------------------------- /single.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Server1 - HTML5 Speedtest 7 | 8 | 9 | 10 | 11 | 54 | 55 | 194 | 195 | 196 |

HTML5 Speedtest

197 |
Server1

198 |
199 | 200 | 201 |
202 |
203 |
204 |
205 |
Download
206 |
207 |
Mbps
208 |
209 |
210 |
Upload
211 |
212 |
Mbps
213 |
214 |
215 |
216 |
217 |
Ping
218 |
219 |
ms
220 |
221 |
222 |
Jitter
223 |
224 |
ms
225 |
226 |
227 |
228 | IP Address: 229 |
230 |
231 | 232 |
233 |

234 | 测试结果如何?评论一下吧
235 | 请按”节点名称+评论“发表 236 |

237 |
238 | 251 | 252 | 253 | 254 | Original Source - Modified by jialezi
255 | 256 | 257 | 258 | 261 | 262 | -------------------------------------------------------------------------------- /mult-pretty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | HTML5 Speedtest 8 | 9 | 10 | 11 | 82 | 83 | 207 | 208 | 209 |

HTML5 Speedtest

210 |
Selecting server...

211 |
212 | 213 |
214 |
215 |
216 |
Download
217 |
218 |
Mbps
219 |
220 |
221 |
Upload
222 |
223 |
Mbps
224 |
225 |
226 |
227 |
228 |
Ping
229 |
230 |
ms
231 |
232 |
233 |
Jitter
234 |
235 |
ms
236 |
237 |
238 |
239 | IP Address: 240 |
241 |
242 | 243 |
244 |

245 | 测试结果如何?评论一下吧
246 | 请按”节点名称+评论“发表 247 |

248 |
249 | 262 | 263 | Original Source - Modified by jialezi
264 | 265 | 269 | 270 | -------------------------------------------------------------------------------- /mult.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 180 | 383 | HTML5 Speedtest 384 | 385 | 386 |

HTML5 Speedtest

387 |
388 |

Selecting a server...

389 |
390 | 461 | 462 | 463 | -------------------------------------------------------------------------------- /speedtest.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Speedtest - Main 3 | by Federico Dossena 4 | https://github.com/adolfintel/speedtest/ 5 | GNU LGPLv3 License 6 | */ 7 | 8 | /* 9 | This is the main interface between your webpage and the speedtest. 10 | It hides the speedtest web worker to the page, and provides many convenient functions to control the test. 11 | 12 | The best way to learn how to use this is to look at the basic example, but here's some documentation. 13 | 14 | To initialize the test, create a new Speedtest object: 15 | var s=new Speedtest(); 16 | Now you can think of this as a finite state machine. These are the states (use getState() to see them): 17 | - 0: here you can change the speedtest settings (such as test duration) with the setParameter("parameter",value) method. From here you can either start the test using start() (goes to state 3) or you can add multiple test points using addTestPoint(server) or addTestPoints(serverList) (goes to state 1). Additionally, this is the perfect moment to set up callbacks for the onupdate(data) and onend(aborted) events. 18 | - 1: here you can add test points. You only need to do this if you want to use multiple test points. 19 | A server is defined as an object like this: 20 | { 21 | name: "User friendly name", 22 | server:"http://yourBackend.com/", <---- URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol 23 | dlURL:"garbage.php" <----- path to garbage.php or its replacement on the server 24 | ulURL:"empty.php" <----- path to empty.php or its replacement on the server 25 | pingURL:"empty.php" <----- path to empty.php or its replacement on the server. This is used to ping the server by this selector 26 | getIpURL:"getIP.php" <----- path to getIP.php or its replacement on the server 27 | } 28 | While in state 1, you can only add test points, you cannot change the test settings. When you're done, use selectServer(callback) to select the test point with the lowest ping. This is asynchronous, when it's done, it will call your callback function and move to state 2. Calling setSelectedServer(server) will manually select a server and move to state 2. 29 | - 2: test point selected, ready to start the test. Use start() to begin, this will move to state 3 30 | - 3: test running. Here, your onupdate event calback will be called periodically, with data coming from the worker about speed and progress. A data object will be passed to your onupdate function, with the following items: 31 | - dlStatus: download speed in mbps 32 | - ulStatus: upload speed in mbps 33 | - pingStatus: ping in ms 34 | - jitterStatus: jitter in ms 35 | - dlProgress: progress of the download test as a float 0-1 36 | - ulProgress: progress of the upload test as a float 0-1 37 | - pingProgress: progress of the ping/jitter test as a float 0-1 38 | - testState: state of the test (-1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=aborted) 39 | - clientIp: IP address of the client performing the test (and optionally ISP and distance) 40 | At the end of the test, the onend function will be called, with a boolean specifying whether the test was aborted or if it ended normally. 41 | The test can be aborted at any time with abort(). 42 | At the end of the test, it will move to state 4 43 | - 4: test finished. You can run it again by calling start() if you want. 44 | */ 45 | 46 | function Speedtest() { 47 | this._serverList = []; //when using multiple points of test, this is a list of test points 48 | this._selectedServer = null; //when using multiple points of test, this is the selected server 49 | this._settings = {}; //settings for the speedtest worker 50 | this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done 51 | console.log( 52 | "HTML5 Speedtest by Federico Dossena v5.0 - https://github.com/adolfintel/speedtest" 53 | ); 54 | } 55 | 56 | Speedtest.prototype = { 57 | constructor: Speedtest, 58 | /** 59 | * Returns the state of the test: 0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done 60 | */ 61 | getState: function() { 62 | return this._state; 63 | }, 64 | /** 65 | * Change one of the test settings from their defaults. 66 | * - parameter: string with the name of the parameter that you want to set 67 | * - value: new value for the parameter 68 | * 69 | * Invalid values or nonexistant parameters will be ignored by the speedtest worker. 70 | */ 71 | setParameter: function(parameter, value) { 72 | if (this._state != 0) 73 | throw "You cannot change the test settings after adding server or starting the test"; 74 | this._settings[parameter] = value; 75 | if(parameter === "temeletry_extra"){ 76 | this._originalExtra=this._settings.telemetry_extra; 77 | } 78 | }, 79 | /** 80 | * Used internally to check if a server object contains all the required elements. 81 | * Also fixes the server URL if needed. 82 | */ 83 | _checkServerDefinition: function(server) { 84 | try { 85 | if (typeof server.name !== "string") 86 | throw "Name string missing from server definition (name)"; 87 | if (typeof server.server !== "string") 88 | throw "Server address string missing from server definition (server)"; 89 | if (server.server.charAt(server.server.length - 1) != "/") 90 | server.server += "/"; 91 | if (server.server.indexOf("//") == 0) 92 | server.server = location.protocol + server.server; 93 | if (typeof server.dlURL !== "string") 94 | throw "Download URL string missing from server definition (dlURL)"; 95 | if (typeof server.ulURL !== "string") 96 | throw "Upload URL string missing from server definition (ulURL)"; 97 | if (typeof server.pingURL !== "string") 98 | throw "Ping URL string missing from server definition (pingURL)"; 99 | if (typeof server.getIpURL !== "string") 100 | throw "GetIP URL string missing from server definition (getIpURL)"; 101 | } catch (e) { 102 | throw "Invalid server definition"; 103 | } 104 | }, 105 | /** 106 | * Add a test point (multiple points of test) 107 | * server: the server to be added as an object. Must contain the following elements: 108 | * { 109 | * name: "User friendly name", 110 | * server:"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol 111 | * dlURL:"garbage.php" path to garbage.php or its replacement on the server 112 | * ulURL:"empty.php" path to empty.php or its replacement on the server 113 | * pingURL:"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this selector 114 | * getIpURL:"getIP.php" path to getIP.php or its replacement on the server 115 | * } 116 | */ 117 | addTestPoint: function(server) { 118 | this._checkServerDefinition(server); 119 | if (this._state == 0) this._state = 1; 120 | if (this._state != 1) throw "You can't add a server after server selection"; 121 | this._settings.mpot = true; 122 | this._serverList.push(server); 123 | }, 124 | /** 125 | * Same as addTestPoint, but you can pass an array of servers 126 | */ 127 | addTestPoints: function(list) { 128 | for (var i = 0; i < list.length; i++) this.addTestPoint(list[i]); 129 | }, 130 | /** 131 | * Returns the selected server (multiple points of test) 132 | */ 133 | getSelectedServer: function() { 134 | if (this._state < 2 || this._selectedServer == null) 135 | throw "No server is selected"; 136 | return this._selectedServer; 137 | }, 138 | /** 139 | * Manually selects one of the test points (multiple points of test) 140 | */ 141 | setSelectedServer: function(server) { 142 | this._checkServerDefinition(server); 143 | if (this._state == 3) 144 | throw "You can't select a server while the test is running"; 145 | this._selectedServer = server; 146 | this._state = 2; 147 | }, 148 | /** 149 | * Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test) 150 | * The process is asynchronous and the passed result callback function will be called when it's done, then the test can be started. 151 | */ 152 | selectServer: function(result) { 153 | if (this._state != 1) { 154 | if (this._state == 0) throw "No test points added"; 155 | if (this._state == 2) throw "Server already selected"; 156 | if (this._state >= 3) 157 | throw "You can't select a server while the test is running"; 158 | } 159 | if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true; 160 | /*this function goes through a list of servers. For each server, the ping is measured, then the server with the function result is called with the best server, or null if all the servers were down. 161 | */ 162 | var select = function(serverList, result) { 163 | //pings the specified URL, then calls the function result. Result will receive a parameter which is either the time it took to ping the URL, or -1 if something went wrong. 164 | var PING_TIMEOUT = 2000; 165 | var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers 166 | if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) { 167 | //IE11 doesn't support XHR timeout 168 | USE_PING_TIMEOUT = false; 169 | } 170 | var ping = function(url, result) { 171 | url += (url.match(/\?/) ? "&" : "?") + "cors=true"; 172 | var xhr = new XMLHttpRequest(); 173 | var t = new Date().getTime(); 174 | xhr.onload = function() { 175 | if (xhr.responseText.length == 0) { 176 | //we expect an empty response 177 | var instspd = new Date().getTime() - t; //rough timing estimate 178 | try { 179 | //try to get more accurate timing using performance API 180 | var p = performance.getEntriesByName(url); 181 | p = p[p.length - 1]; 182 | var d = p.responseStart - p.requestStart; 183 | if (d <= 0) d = p.duration; 184 | if (d > 0 && d < instspd) instspd = d; 185 | } catch (e) {} 186 | result(instspd); 187 | } else result(-1); 188 | }.bind(this); 189 | xhr.onerror = function() { 190 | result(-1); 191 | }.bind(this); 192 | xhr.open("GET", url); 193 | if (USE_PING_TIMEOUT) { 194 | try { 195 | xhr.timeout = PING_TIMEOUT; 196 | xhr.ontimeout = xhr.onerror; 197 | } catch (e) {} 198 | } 199 | xhr.send(); 200 | }.bind(this); 201 | 202 | //this function repeatedly pings a server to get a good estimate of the ping. When it's done, it calls the done function without parameters. At the end of the execution, the server will have a new parameter called pingT, which is either the best ping we got from the server or -1 if something went wrong. 203 | var PINGS = 3, //up to 3 pings are performed, unless the server is down... 204 | SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold 205 | var checkServer = function(server, done) { 206 | var i = 0; 207 | server.pingT = -1; 208 | if (server.server.indexOf(location.protocol) == -1) done(); 209 | else { 210 | var nextPing = function() { 211 | if (i++ == PINGS) { 212 | done(); 213 | return; 214 | } 215 | ping( 216 | server.server + server.pingURL, 217 | function(t) { 218 | if (t >= 0) { 219 | if (t < server.pingT || server.pingT == -1) server.pingT = t; 220 | if (t < SLOW_THRESHOLD) nextPing(); 221 | else done(); 222 | } else done(); 223 | }.bind(this) 224 | ); 225 | }.bind(this); 226 | nextPing(); 227 | } 228 | }.bind(this); 229 | //check servers in list, one by one 230 | var i = 0; 231 | var done = function() { 232 | var bestServer = null; 233 | for (var i = 0; i < serverList.length; i++) { 234 | if ( 235 | serverList[i].pingT != -1 && 236 | (bestServer == null || serverList[i].pingT < bestServer.pingT) 237 | ) 238 | bestServer = serverList[i]; 239 | } 240 | result(bestServer); 241 | }.bind(this); 242 | var nextServer = function() { 243 | if (i == serverList.length) { 244 | done(); 245 | return; 246 | } 247 | checkServer(serverList[i++], nextServer); 248 | }.bind(this); 249 | nextServer(); 250 | }.bind(this); 251 | 252 | //parallel server selection 253 | var CONCURRENCY = 6; 254 | var serverLists = []; 255 | for (var i = 0; i < CONCURRENCY; i++) { 256 | serverLists[i] = []; 257 | } 258 | for (var i = 0; i < this._serverList.length; i++) { 259 | serverLists[i % CONCURRENCY].push(this._serverList[i]); 260 | } 261 | var completed = 0; 262 | var bestServer = null; 263 | for (var i = 0; i < CONCURRENCY; i++) { 264 | select( 265 | serverLists[i], 266 | function(server) { 267 | if (server != null) { 268 | if (bestServer == null || server.pingT < bestServer.pingT) 269 | bestServer = server; 270 | } 271 | completed++; 272 | if (completed == CONCURRENCY) { 273 | this._selectedServer = bestServer; 274 | this._state = 2; 275 | if (result) result(bestServer); 276 | } 277 | }.bind(this) 278 | ); 279 | } 280 | }, 281 | /** 282 | * Starts the test. 283 | * During the test, the onupdate(data) callback function will be called periodically with data from the worker. 284 | * At the end of the test, the onend(aborted) function will be called with a boolean telling you if the test was aborted or if it ended normally. 285 | */ 286 | start: function() { 287 | if (this._state == 3) throw "Test already running"; 288 | this.worker = new Worker("speedtest_worker.js?r=" + Math.random()); 289 | this.worker.onmessage = function(e) { 290 | if (e.data === this._prevData) return; 291 | else this._prevData = e.data; 292 | var data = JSON.parse(e.data); 293 | try { 294 | if (this.onupdate) this.onupdate(data); 295 | } catch (e) { 296 | console.error("Speedtest onupdate event threw exception: " + e); 297 | } 298 | if (data.testState >= 4) { 299 | try { 300 | if (this.onend) this.onend(data.testState == 5); 301 | } catch (e) { 302 | console.error("Speedtest onend event threw exception: " + e); 303 | } 304 | clearInterval(this.updater); 305 | this._state = 4; 306 | } 307 | }.bind(this); 308 | this.updater = setInterval( 309 | function() { 310 | this.worker.postMessage("status"); 311 | }.bind(this), 312 | 200 313 | ); 314 | if (this._state == 1) 315 | throw "When using multiple points of test, you must call selectServer before starting the test"; 316 | if (this._state == 2) { 317 | this._settings.url_dl = 318 | this._selectedServer.server + this._selectedServer.dlURL; 319 | this._settings.url_ul = 320 | this._selectedServer.server + this._selectedServer.ulURL; 321 | this._settings.url_ping = 322 | this._selectedServer.server + this._selectedServer.pingURL; 323 | this._settings.url_getIp = 324 | this._selectedServer.server + this._selectedServer.getIpURL; 325 | if (typeof this._originalExtra !== "undefined") { 326 | this._settings.telemetry_extra = JSON.stringify({ 327 | server: this._selectedServer.name, 328 | extra: this._originalExtra 329 | }); 330 | } else 331 | this._settings.telemetry_extra = JSON.stringify({ 332 | server: this._selectedServer.name 333 | }); 334 | } 335 | this._state = 3; 336 | this.worker.postMessage("start " + JSON.stringify(this._settings)); 337 | }, 338 | /** 339 | * Aborts the test while it's running. 340 | */ 341 | abort: function() { 342 | if (this._state < 3) throw "You cannot abort a test that's not started yet"; 343 | if (this._state < 4) this.worker.postMessage("abort"); 344 | } 345 | }; 346 | -------------------------------------------------------------------------------- /speedtest_worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Speedtest - Worker 3 | by Federico Dossena 4 | https://github.com/adolfintel/speedtest/ 5 | GNU LGPLv3 License 6 | */ 7 | 8 | // data reported to main thread 9 | var testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort 10 | var dlStatus = ""; // download speed in megabit/s with 2 decimal digits 11 | var ulStatus = ""; // upload speed in megabit/s with 2 decimal digits 12 | var pingStatus = ""; // ping in milliseconds with 2 decimal digits 13 | var jitterStatus = ""; // jitter in milliseconds with 2 decimal digits 14 | var clientIp = ""; // client's IP address as reported by getIP.php 15 | var dlProgress = 0; //progress of download test 0-1 16 | var ulProgress = 0; //progress of upload test 0-1 17 | var pingProgress = 0; //progress of ping+jitter test 0-1 18 | var testId = null; //test ID (sent back by telemetry if used, null otherwise) 19 | 20 | var log = ""; //telemetry log 21 | function tlog(s) { 22 | if (settings.telemetry_level >= 2) { 23 | log += Date.now() + ": " + s + "\n"; 24 | } 25 | } 26 | function tverb(s) { 27 | if (settings.telemetry_level >= 3) { 28 | log += Date.now() + ": " + s + "\n"; 29 | } 30 | } 31 | function twarn(s) { 32 | if (settings.telemetry_level >= 2) { 33 | log += Date.now() + " WARN: " + s + "\n"; 34 | } 35 | console.warn(s); 36 | } 37 | 38 | // test settings. can be overridden by sending specific values with the start command 39 | var settings = { 40 | mpot: false, //set to true when in MPOT mode 41 | test_order: "IP_D_U", //order in which tests will be performed as a string. D=Download, U=Upload, P=Ping+Jitter, I=IP, _=1 second delay 42 | time_ul_max: 15, // max duration of upload test in seconds 43 | time_dl_max: 15, // max duration of download test in seconds 44 | time_auto: true, // if set to true, tests will take less time on faster connections 45 | time_ulGraceTime: 3, //time to wait in seconds before actually measuring ul speed (wait for buffers to fill) 46 | time_dlGraceTime: 1.5, //time to wait in seconds before actually measuring dl speed (wait for TCP window to increase) 47 | count_ping: 10, // number of pings to perform in ping test 48 | url_dl: "backend/garbage.php", // path to a large file or garbage.php, used for download test. must be relative to this js file 49 | url_ul: "backend/empty.php", // path to an empty file, used for upload test. must be relative to this js file 50 | url_ping: "backend/empty.php", // path to an empty file, used for ping test. must be relative to this js file 51 | url_getIp: "backend/getIP.php", // path to getIP.php relative to this js file, or a similar thing that outputs the client's ip 52 | getIp_ispInfo: true, //if set to true, the server will include ISP info with the IP address 53 | 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 54 | xhr_dlMultistream: 6, // number of download streams to use (can be different if enable_quirks is active) 55 | xhr_ulMultistream: 3, // number of upload streams to use (can be different if enable_quirks is active) 56 | xhr_multistreamDelay: 300, //how much concurrent requests should be delayed 57 | xhr_ignoreErrors: 1, // 0=fail on errors, 1=attempt to restart a stream if it fails, 2=ignore all errors 58 | 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) 59 | xhr_ul_blob_megabytes: 20, //size in megabytes of the upload blobs sent in the upload test (forced to 4 on chrome mobile) 60 | garbagePhp_chunkSize: 100, // size of chunks sent by garbage.php (can be different if enable_quirks is active) 61 | 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 62 | ping_allowPerformanceApi: true, // if enabled, the ping test will attempt to calculate the ping more precisely using the Performance API. Currently works perfectly in Chrome, badly in Edge, and not at all in Firefox. If Performance API is not supported or the result is obviously wrong, a fallback is provided. 63 | overheadCompensationFactor: 1.06, //can be changed to compensatie for transport overhead. (see doc.md for some other values) 64 | useMebibits: false, //if set to true, speed will be reported in mebibits/s instead of megabits/s 65 | telemetry_level: 0, // 0=disabled, 1=basic (results only), 2=full (results and timing) 3=debug (results+log) 66 | url_telemetry: "results/telemetry.php", // path to the script that adds telemetry data to the database 67 | telemetry_extra: "" //extra data that can be passed to the telemetry through the settings 68 | }; 69 | 70 | var xhr = null; // array of currently active xhr requests 71 | var interval = null; // timer used in tests 72 | var test_pointer = 0; //pointer to the next test to run inside settings.test_order 73 | 74 | /* 75 | this function is used on URLs passed in the settings to determine whether we need a ? or an & as a separator 76 | */ 77 | function url_sep(url) { 78 | return url.match(/\?/) ? "&" : "?"; 79 | } 80 | 81 | /* 82 | listener for commands from main thread to this worker. 83 | commands: 84 | -status: returns the current status as a JSON string containing testState, dlStatus, ulStatus, pingStatus, clientIp, jitterStatus, dlProgress, ulProgress, pingProgress 85 | -abort: aborts the current test 86 | -start: starts the test. optionally, settings can be passed as JSON. 87 | example: start {"time_ul_max":"10", "time_dl_max":"10", "count_ping":"50"} 88 | */ 89 | this.addEventListener("message", function(e) { 90 | var params = e.data.split(" "); 91 | if (params[0] === "status") { 92 | // return status 93 | postMessage( 94 | JSON.stringify({ 95 | testState: testState, 96 | dlStatus: dlStatus, 97 | ulStatus: ulStatus, 98 | pingStatus: pingStatus, 99 | clientIp: clientIp, 100 | jitterStatus: jitterStatus, 101 | dlProgress: dlProgress, 102 | ulProgress: ulProgress, 103 | pingProgress: pingProgress, 104 | testId: testId 105 | }) 106 | ); 107 | } 108 | if (params[0] === "start" && testState === -1) { 109 | // start new test 110 | testState = 0; 111 | try { 112 | // parse settings, if present 113 | var s = {}; 114 | try { 115 | var ss = e.data.substring(5); 116 | if (ss) s = JSON.parse(ss); 117 | } catch (e) { 118 | twarn("Error parsing custom settings JSON. Please check your syntax"); 119 | } 120 | //copy custom settings 121 | for (var key in s) { 122 | if (typeof settings[key] !== "undefined") settings[key] = s[key]; 123 | else twarn("Unknown setting ignored: " + key); 124 | } 125 | // quirks for specific browsers. apply only if not overridden. more may be added in future releases 126 | if (settings.enable_quirks || (typeof s.enable_quirks !== "undefined" && s.enable_quirks)) { 127 | var ua = navigator.userAgent; 128 | if (/Firefox.(\d+\.\d+)/i.test(ua)) { 129 | if (typeof s.xhr_ulMultistream === "undefined") { 130 | // ff more precise with 1 upload stream 131 | settings.xhr_ulMultistream = 1; 132 | } 133 | if (typeof s.xhr_ulMultistream === "undefined") { 134 | // ff performance API sucks 135 | settings.ping_allowPerformanceApi = false; 136 | } 137 | } 138 | if (/Edge.(\d+\.\d+)/i.test(ua)) { 139 | if (typeof s.xhr_dlMultistream === "undefined") { 140 | // edge more precise with 3 download streams 141 | settings.xhr_dlMultistream = 3; 142 | } 143 | } 144 | if (/Chrome.(\d+)/i.test(ua) && !!self.fetch) { 145 | if (typeof s.xhr_dlMultistream === "undefined") { 146 | // chrome more precise with 5 streams 147 | settings.xhr_dlMultistream = 5; 148 | } 149 | } 150 | } 151 | if (/Edge.(\d+\.\d+)/i.test(ua)) { 152 | //Edge 15 introduced a bug that causes onprogress events to not get fired, we have to use the "small chunks" workaround that reduces accuracy 153 | settings.forceIE11Workaround = true; 154 | } 155 | if (/PlayStation 4.(\d+\.\d+)/i.test(ua)) { 156 | //PS4 browser has the same bug as IE11/Edge 157 | settings.forceIE11Workaround = true; 158 | } 159 | if (/Chrome.(\d+)/i.test(ua) && /Android|iPhone|iPad|iPod|Windows Phone/i.test(ua)) { 160 | //cheap af 161 | //Chrome mobile introduced a limitation somewhere around version 65, we have to limit XHR upload size to 4 megabytes 162 | settings.xhr_ul_blob_megabytes = 4; 163 | } 164 | if (/^((?!chrome|android|crios|fxios).)*safari/i.test(ua)) { 165 | //Safari also needs the IE11 workaround but only for the MPOT version 166 | settings.forceIE11Workaround = true; 167 | } 168 | //telemetry_level has to be parsed and not just copied 169 | if (typeof s.telemetry_level !== "undefined") settings.telemetry_level = s.telemetry_level === "basic" ? 1 : s.telemetry_level === "full" ? 2 : s.telemetry_level === "debug" ? 3 : 0; // telemetry level 170 | //transform test_order to uppercase, just in case 171 | settings.test_order = settings.test_order.toUpperCase(); 172 | } catch (e) { 173 | twarn("Possible error in custom test settings. Some settings might not have been applied. Exception: " + e); 174 | } 175 | // run the tests 176 | tverb(JSON.stringify(settings)); 177 | test_pointer = 0; 178 | var iRun = false, 179 | dRun = false, 180 | uRun = false, 181 | pRun = false; 182 | var runNextTest = function() { 183 | if (testState == 5) return; 184 | if (test_pointer >= settings.test_order.length) { 185 | //test is finished 186 | if (settings.telemetry_level > 0) 187 | sendTelemetry(function(id) { 188 | testState = 4; 189 | if (id != null) testId = id; 190 | }); 191 | else testState = 4; 192 | return; 193 | } 194 | switch (settings.test_order.charAt(test_pointer)) { 195 | case "I": 196 | { 197 | test_pointer++; 198 | if (iRun) { 199 | runNextTest(); 200 | return; 201 | } else iRun = true; 202 | getIp(runNextTest); 203 | } 204 | break; 205 | case "D": 206 | { 207 | test_pointer++; 208 | if (dRun) { 209 | runNextTest(); 210 | return; 211 | } else dRun = true; 212 | testState = 1; 213 | dlTest(runNextTest); 214 | } 215 | break; 216 | case "U": 217 | { 218 | test_pointer++; 219 | if (uRun) { 220 | runNextTest(); 221 | return; 222 | } else uRun = true; 223 | testState = 3; 224 | ulTest(runNextTest); 225 | } 226 | break; 227 | case "P": 228 | { 229 | test_pointer++; 230 | if (pRun) { 231 | runNextTest(); 232 | return; 233 | } else pRun = true; 234 | testState = 2; 235 | pingTest(runNextTest); 236 | } 237 | break; 238 | case "_": 239 | { 240 | test_pointer++; 241 | setTimeout(runNextTest, 1000); 242 | } 243 | break; 244 | default: 245 | test_pointer++; 246 | } 247 | }; 248 | runNextTest(); 249 | } 250 | if (params[0] === "abort") { 251 | // abort command 252 | if (testState >= 4) return; 253 | tlog("manually aborted"); 254 | clearRequests(); // stop all xhr activity 255 | runNextTest = null; 256 | if (interval) clearInterval(interval); // clear timer if present 257 | if (settings.telemetry_level > 1) sendTelemetry(function() {}); 258 | testState = 5; //set test as aborted 259 | dlStatus = ""; 260 | ulStatus = ""; 261 | pingStatus = ""; 262 | jitterStatus = ""; 263 | clientIp = ""; 264 | dlProgress = 0; 265 | ulProgress = 0; 266 | pingProgress = 0; 267 | } 268 | }); 269 | // stops all XHR activity, aggressively 270 | function clearRequests() { 271 | tverb("stopping pending XHRs"); 272 | if (xhr) { 273 | for (var i = 0; i < xhr.length; i++) { 274 | try { 275 | xhr[i].onprogress = null; 276 | xhr[i].onload = null; 277 | xhr[i].onerror = null; 278 | } catch (e) {} 279 | try { 280 | xhr[i].upload.onprogress = null; 281 | xhr[i].upload.onload = null; 282 | xhr[i].upload.onerror = null; 283 | } catch (e) {} 284 | try { 285 | xhr[i].abort(); 286 | } catch (e) {} 287 | try { 288 | delete xhr[i]; 289 | } catch (e) {} 290 | } 291 | xhr = null; 292 | } 293 | } 294 | // gets client's IP using url_getIp, then calls the done function 295 | var ipCalled = false; // used to prevent multiple accidental calls to getIp 296 | var ispInfo = ""; //used for telemetry 297 | function getIp(done) { 298 | tverb("getIp"); 299 | if (ipCalled) return; 300 | else ipCalled = true; // getIp already called? 301 | var startT = new Date().getTime(); 302 | xhr = new XMLHttpRequest(); 303 | xhr.onload = function() { 304 | tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms"); 305 | try { 306 | var data = JSON.parse(xhr.responseText); 307 | clientIp = data.processedString; 308 | ispInfo = data.rawIspInfo; 309 | } catch (e) { 310 | clientIp = xhr.responseText; 311 | ispInfo = ""; 312 | } 313 | done(); 314 | }; 315 | xhr.onerror = function() { 316 | tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms"); 317 | done(); 318 | }; 319 | xhr.open("GET", settings.url_getIp + url_sep(settings.url_getIp) + (settings.mpot ? "cors=true&" : "") + (settings.getIp_ispInfo ? "isp=true" + (settings.getIp_ispInfo_distance ? "&distance=" + settings.getIp_ispInfo_distance + "&" : "&") : "&") + "r=" + Math.random(), true); 320 | xhr.send(); 321 | } 322 | // download test, calls done function when it's over 323 | var dlCalled = false; // used to prevent multiple accidental calls to dlTest 324 | function dlTest(done) { 325 | tverb("dlTest"); 326 | if (dlCalled) return; 327 | else dlCalled = true; // dlTest already called? 328 | var totLoaded = 0.0, // total number of loaded bytes 329 | startT = new Date().getTime(), // timestamp when test was started 330 | bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) 331 | graceTimeDone = false, //set to true after the grace time is past 332 | failed = false; // set to true if a stream fails 333 | xhr = []; 334 | // function to create a download stream. streams are slightly delayed so that they will not end at the same time 335 | var testStream = function(i, delay) { 336 | setTimeout( 337 | function() { 338 | if (testState !== 1) return; // delayed stream ended up starting after the end of the download test 339 | tverb("dl test stream started " + i + " " + delay); 340 | var prevLoaded = 0; // number of bytes loaded last time onprogress was called 341 | var x = new XMLHttpRequest(); 342 | xhr[i] = x; 343 | xhr[i].onprogress = function(event) { 344 | tverb("dl stream progress event " + i + " " + event.loaded); 345 | if (testState !== 1) { 346 | try { 347 | x.abort(); 348 | } catch (e) {} 349 | } // just in case this XHR is still running after the download test 350 | // progress event, add number of new loaded bytes to totLoaded 351 | var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; 352 | if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case 353 | totLoaded += loadDiff; 354 | prevLoaded = event.loaded; 355 | }.bind(this); 356 | xhr[i].onload = function() { 357 | // the large file has been loaded entirely, start again 358 | tverb("dl stream finished " + i); 359 | try { 360 | xhr[i].abort(); 361 | } catch (e) {} // reset the stream data to empty ram 362 | testStream(i, 0); 363 | }.bind(this); 364 | xhr[i].onerror = function() { 365 | // error 366 | tverb("dl stream failed " + i); 367 | if (settings.xhr_ignoreErrors === 0) failed = true; //abort 368 | try { 369 | xhr[i].abort(); 370 | } catch (e) {} 371 | delete xhr[i]; 372 | if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream 373 | }.bind(this); 374 | // send xhr 375 | try { 376 | if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob"; 377 | else xhr[i].responseType = "arraybuffer"; 378 | } catch (e) {} 379 | xhr[i].open("GET", settings.url_dl + url_sep(settings.url_dl) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random() + "&ckSize=" + settings.garbagePhp_chunkSize, true); // random string to prevent caching 380 | xhr[i].send(); 381 | }.bind(this), 382 | 1 + delay 383 | ); 384 | }.bind(this); 385 | // open streams 386 | for (var i = 0; i < settings.xhr_dlMultistream; i++) { 387 | testStream(i, settings.xhr_multistreamDelay * i); 388 | } 389 | // every 200ms, update dlStatus 390 | interval = setInterval( 391 | function() { 392 | tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)")); 393 | var t = new Date().getTime() - startT; 394 | if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000); 395 | if (t < 200) return; 396 | if (!graceTimeDone) { 397 | if (t > 1000 * settings.time_dlGraceTime) { 398 | if (totLoaded > 0) { 399 | // if the connection is so slow that we didn't get a single chunk yet, do not reset 400 | startT = new Date().getTime(); 401 | bonusT = 0; 402 | totLoaded = 0.0; 403 | } 404 | graceTimeDone = true; 405 | } 406 | } else { 407 | var speed = totLoaded / (t / 1000.0); 408 | if (settings.time_auto) { 409 | //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here 410 | var bonus = (6.4 * speed) / 100000; 411 | bonusT += bonus > 800 ? 800 : bonus; 412 | } 413 | //update status 414 | dlStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits 415 | if ((t + bonusT) / 1000.0 > settings.time_dl_max || failed) { 416 | // test is over, stop streams and timer 417 | if (failed || isNaN(dlStatus)) dlStatus = "Fail"; 418 | clearRequests(); 419 | clearInterval(interval); 420 | dlProgress = 1; 421 | tlog("dlTest: " + dlStatus + ", took " + (new Date().getTime() - startT) + "ms"); 422 | done(); 423 | } 424 | } 425 | }.bind(this), 426 | 200 427 | ); 428 | } 429 | // upload test, calls done function whent it's over 430 | var ulCalled = false; // used to prevent multiple accidental calls to ulTest 431 | function ulTest(done) { 432 | tverb("ulTest"); 433 | if (ulCalled) return; 434 | else ulCalled = true; // ulTest already called? 435 | // garbage data for upload test 436 | var r = new ArrayBuffer(1048576); 437 | var maxInt = Math.pow(2, 32) - 1; 438 | try { 439 | r = new Uint32Array(r); 440 | for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; 441 | } catch (e) {} 442 | var req = []; 443 | var reqsmall = []; 444 | for (var i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r); 445 | req = new Blob(req); 446 | r = new ArrayBuffer(262144); 447 | try { 448 | r = new Uint32Array(r); 449 | for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt; 450 | } catch (e) {} 451 | reqsmall.push(r); 452 | reqsmall = new Blob(reqsmall); 453 | var testFunction = function() { 454 | var totLoaded = 0.0, // total number of transmitted bytes 455 | startT = new Date().getTime(), // timestamp when test was started 456 | bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections) 457 | graceTimeDone = false, //set to true after the grace time is past 458 | failed = false; // set to true if a stream fails 459 | xhr = []; 460 | // function to create an upload stream. streams are slightly delayed so that they will not end at the same time 461 | var testStream = function(i, delay) { 462 | setTimeout( 463 | function() { 464 | if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test 465 | tverb("ul test stream started " + i + " " + delay); 466 | var prevLoaded = 0; // number of bytes transmitted last time onprogress was called 467 | var x = new XMLHttpRequest(); 468 | xhr[i] = x; 469 | var ie11workaround; 470 | if (settings.forceIE11Workaround) ie11workaround = true; 471 | else { 472 | try { 473 | xhr[i].upload.onprogress; 474 | ie11workaround = false; 475 | } catch (e) { 476 | ie11workaround = true; 477 | } 478 | } 479 | if (ie11workaround) { 480 | // 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 481 | xhr[i].onload = xhr[i].onerror = function() { 482 | tverb("ul stream progress event (ie11wa)"); 483 | totLoaded += reqsmall.size; 484 | testStream(i, 0); 485 | }; 486 | xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching 487 | try { 488 | xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) 489 | } catch (e) {} 490 | //No Content-Type header in MPOT branch because it triggers bugs in some browsers 491 | xhr[i].send(reqsmall); 492 | } else { 493 | // REGULAR version, no workaround 494 | xhr[i].upload.onprogress = function(event) { 495 | tverb("ul stream progress event " + i + " " + event.loaded); 496 | if (testState !== 3) { 497 | try { 498 | x.abort(); 499 | } catch (e) {} 500 | } // just in case this XHR is still running after the upload test 501 | // progress event, add number of new loaded bytes to totLoaded 502 | var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded; 503 | if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case 504 | totLoaded += loadDiff; 505 | prevLoaded = event.loaded; 506 | }.bind(this); 507 | xhr[i].upload.onload = function() { 508 | // this stream sent all the garbage data, start again 509 | tverb("ul stream finished " + i); 510 | testStream(i, 0); 511 | }.bind(this); 512 | xhr[i].upload.onerror = function() { 513 | tverb("ul stream failed " + i); 514 | if (settings.xhr_ignoreErrors === 0) failed = true; //abort 515 | try { 516 | xhr[i].abort(); 517 | } catch (e) {} 518 | delete xhr[i]; 519 | if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream 520 | }.bind(this); 521 | // send xhr 522 | xhr[i].open("POST", settings.url_ul + url_sep(settings.url_ul) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching 523 | try { 524 | xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway) 525 | } catch (e) {} 526 | //No Content-Type header in MPOT branch because it triggers bugs in some browsers 527 | xhr[i].send(req); 528 | } 529 | }.bind(this), 530 | 1 531 | ); 532 | }.bind(this); 533 | // open streams 534 | for (var i = 0; i < settings.xhr_ulMultistream; i++) { 535 | testStream(i, settings.xhr_multistreamDelay * i); 536 | } 537 | // every 200ms, update ulStatus 538 | interval = setInterval( 539 | function() { 540 | tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)")); 541 | var t = new Date().getTime() - startT; 542 | if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000); 543 | if (t < 200) return; 544 | if (!graceTimeDone) { 545 | if (t > 1000 * settings.time_ulGraceTime) { 546 | if (totLoaded > 0) { 547 | // if the connection is so slow that we didn't get a single chunk yet, do not reset 548 | startT = new Date().getTime(); 549 | bonusT = 0; 550 | totLoaded = 0.0; 551 | } 552 | graceTimeDone = true; 553 | } 554 | } else { 555 | var speed = totLoaded / (t / 1000.0); 556 | if (settings.time_auto) { 557 | //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here 558 | var bonus = (6.4 * speed) / 100000; 559 | bonusT += bonus > 800 ? 800 : bonus; 560 | } 561 | //update status 562 | ulStatus = ((speed * 8 * settings.overheadCompensationFactor) / (settings.useMebibits ? 1048576 : 1000000)).toFixed(2); // speed is multiplied by 8 to go from bytes to bits, overhead compensation is applied, then everything is divided by 1048576 or 1000000 to go to megabits/mebibits 563 | if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) { 564 | // test is over, stop streams and timer 565 | if (failed || isNaN(ulStatus)) ulStatus = "Fail"; 566 | clearRequests(); 567 | clearInterval(interval); 568 | ulProgress = 1; 569 | tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms"); 570 | done(); 571 | } 572 | } 573 | }.bind(this), 574 | 200 575 | ); 576 | }.bind(this); 577 | if (settings.mpot) { 578 | tverb("Sending POST request before performing upload test"); 579 | xhr = []; 580 | xhr[0] = new XMLHttpRequest(); 581 | xhr[0].onload = xhr[0].onerror = function() { 582 | tverb("POST request sent, starting upload test"); 583 | testFunction(); 584 | }.bind(this); 585 | xhr[0].open("POST", settings.url_ul); 586 | xhr[0].send(); 587 | } else testFunction(); 588 | } 589 | // ping+jitter test, function done is called when it's over 590 | var ptCalled = false; // used to prevent multiple accidental calls to pingTest 591 | function pingTest(done) { 592 | tverb("pingTest"); 593 | if (ptCalled) return; 594 | else ptCalled = true; // pingTest already called? 595 | var startT = new Date().getTime(); //when the test was started 596 | var prevT = null; // last time a pong was received 597 | var ping = 0.0; // current ping value 598 | var jitter = 0.0; // current jitter value 599 | var i = 0; // counter of pongs received 600 | var prevInstspd = 0; // last ping time, used for jitter calculation 601 | xhr = []; 602 | // ping function 603 | var doPing = function() { 604 | tverb("ping"); 605 | pingProgress = i / settings.count_ping; 606 | prevT = new Date().getTime(); 607 | xhr[0] = new XMLHttpRequest(); 608 | xhr[0].onload = function() { 609 | // pong 610 | tverb("pong"); 611 | if (i === 0) { 612 | prevT = new Date().getTime(); // first pong 613 | } else { 614 | var instspd = new Date().getTime() - prevT; 615 | if (settings.ping_allowPerformanceApi) { 616 | try { 617 | //try to get accurate performance timing using performance api 618 | var p = performance.getEntries(); 619 | p = p[p.length - 1]; 620 | var d = p.responseStart - p.requestStart; 621 | if (d <= 0) d = p.duration; 622 | if (d > 0 && d < instspd) instspd = d; 623 | } catch (e) { 624 | //if not possible, keep the estimate 625 | tverb("Performance API not supported, using estimate"); 626 | } 627 | } 628 | //noticed that some browsers randomly have 0ms ping 629 | if (instspd < 1) instspd = prevInstspd; 630 | if (instspd < 1) instspd = 1; 631 | var instjitter = Math.abs(instspd - prevInstspd); 632 | if (i === 1) ping = instspd; 633 | /* first ping, can't tell jitter yet*/ else { 634 | ping = instspd < ping ? instspd : ping * 0.8 + instspd * 0.2; // update ping, weighted average. if the instant ping is lower than the current average, it is set to that value instead of averaging 635 | if (i === 2) jitter = instjitter; 636 | //discard the first jitter measurement because it might be much higher than it should be 637 | else jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2; // update jitter, weighted average. spikes in ping values are given more weight. 638 | } 639 | prevInstspd = instspd; 640 | } 641 | pingStatus = ping.toFixed(2); 642 | jitterStatus = jitter.toFixed(2); 643 | i++; 644 | tverb("ping: " + pingStatus + " jitter: " + jitterStatus); 645 | if (i < settings.count_ping) doPing(); 646 | else { 647 | // more pings to do? 648 | pingProgress = 1; 649 | tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms"); 650 | done(); 651 | } 652 | }.bind(this); 653 | xhr[0].onerror = function() { 654 | // a ping failed, cancel test 655 | tverb("ping failed"); 656 | if (settings.xhr_ignoreErrors === 0) { 657 | //abort 658 | pingStatus = "Fail"; 659 | jitterStatus = "Fail"; 660 | clearRequests(); 661 | tlog("ping test failed, took " + (new Date().getTime() - startT) + "ms"); 662 | pingProgress = 1; 663 | done(); 664 | } 665 | if (settings.xhr_ignoreErrors === 1) doPing(); //retry ping 666 | if (settings.xhr_ignoreErrors === 2) { 667 | //ignore failed ping 668 | i++; 669 | if (i < settings.count_ping) doPing(); 670 | else { 671 | // more pings to do? 672 | pingProgress = 1; 673 | tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms"); 674 | done(); 675 | } 676 | } 677 | }.bind(this); 678 | // send xhr 679 | xhr[0].open("GET", settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); // random string to prevent caching 680 | xhr[0].send(); 681 | }.bind(this); 682 | doPing(); // start first ping 683 | } 684 | // telemetry 685 | function sendTelemetry(done) { 686 | if (settings.telemetry_level < 1) return; 687 | xhr = new XMLHttpRequest(); 688 | xhr.onload = function() { 689 | try { 690 | var parts = xhr.responseText.split(" "); 691 | if (parts[0] == "id") { 692 | try { 693 | var id = parts[1]; 694 | done(id); 695 | } catch (e) { 696 | done(null); 697 | } 698 | } else done(null); 699 | } catch (e) { 700 | done(null); 701 | } 702 | }; 703 | xhr.onerror = function() { 704 | console.log("TELEMETRY ERROR " + xhr.status); 705 | done(null); 706 | }; 707 | xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true); 708 | var telemetryIspInfo = { 709 | processedString: clientIp, 710 | rawIspInfo: typeof ispInfo === "object" ? ispInfo : "" 711 | }; 712 | try { 713 | var fd = new FormData(); 714 | fd.append("ispinfo", JSON.stringify(telemetryIspInfo)); 715 | fd.append("dl", dlStatus); 716 | fd.append("ul", ulStatus); 717 | fd.append("ping", pingStatus); 718 | fd.append("jitter", jitterStatus); 719 | fd.append("log", settings.telemetry_level > 1 ? log : ""); 720 | fd.append("extra", settings.telemetry_extra); 721 | xhr.send(fd); 722 | } catch (ex) { 723 | var postData = "extra=" + encodeURIComponent(settings.telemetry_extra) + "&ispinfo=" + encodeURIComponent(JSON.stringify(telemetryIspInfo)) + "&dl=" + encodeURIComponent(dlStatus) + "&ul=" + encodeURIComponent(ulStatus) + "&ping=" + encodeURIComponent(pingStatus) + "&jitter=" + encodeURIComponent(jitterStatus) + "&log=" + encodeURIComponent(settings.telemetry_level > 1 ? log : ""); 724 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 725 | xhr.send(postData); 726 | } 727 | } 728 | --------------------------------------------------------------------------------