1/* 2 LibreSpeed - Main 3 by Federico Dossena 4 https://github.com/librespeed/speedtest/ 5 GNU LGPLv3 License 6*/ 7 8/* 9 This is the main interface between your webpage and the speed test. 10 It hides the speed test 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 speed test 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
25selector 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 callback 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 Mbit/s 32 - ulStatus: upload speed in Mbit/s 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, 6=send telemetry) 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 speed test worker 50 this._state = 0; //0=adding settings, 1=adding servers, 2=server selection done, 3=test running, 4=done 51 console.log( 52 "LibreSpeed by Federico Dossena v5.3.0 - https://github.com/librespeed/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 speed test worker. 70 */ 71 setParameter: function(parameter, value) { 72 if (this._state == 3) 73 throw "You cannot change the test settings while running the test"; 74 this._settings[parameter] = value; 75 if(parameter === "telemetry_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
113selector 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 * Load a JSON server list from URL (multiple points of test) 132 * url: the url where the server list can be fetched. Must be an array with objects containing the following elements: 133 * { 134 * "name": "User friendly name", 135 * "server":"http://yourBackend.com/", URL to your server. You can specify http:// or https://. If your server supports both, just write // without the protocol 136 * "dlURL":"garbage.php" path to garbage.php or its replacement on the server 137 * "ulURL":"empty.php" path to empty.php or its replacement on the server 138 * "pingURL":"empty.php" path to empty.php or its replacement on the server. This is used to ping the server by this
138selector 139 * "getIpURL":"getIP.php" path to getIP.php or its replacement on the server 140 * } 141 * result: callback to be called when the list is loaded correctly. An array with the loaded servers will be passed to this function, or null if it failed 142 */ 143 loadServerList: function(url,result) { 144 if (this._state == 0) this._state = 1; 145 if (this._state != 1) throw "You can't add a server after server selection"; 146 this._settings.mpot = true; 147 var xhr = new XMLHttpRequest(); 148 xhr.onload = function(){ 149 try{ 150 var servers=JSON.parse(xhr.responseText); 151 for(var i=0;i<servers.length;i++){ 152 this._checkServerDefinition(servers[i]); 153 } 154 this.addTestPoints(servers); 155 result(servers); 156 }catch(e){ 157 result(null); 158 } 159 }.bind(this); 160 xhr.onerror = function(){result(null);} 161 xhr.open("GET",url); 162 xhr.send(); 163 }, 164 /** 165 * Returns the selected server (multiple points of test) 166 */ 167 getSelectedServer: function() { 168 if (this._state < 2 || this._selectedServer == null) 169 throw "No server is selected"; 170 return this._selectedServer; 171 }, 172 /** 173 * Manually selects one of the test points (multiple points of test) 174 */ 175 setSelectedServer: function(server) { 176 this._checkServerDefinition(server); 177 if (this._state == 3) 178 throw "You can't select a server while the test is running"; 179 this._selectedServer = server; 180 this._state = 2; 181 }, 182 /** 183 * Automatically selects a server from the list of added test points. The server with the lowest ping will be chosen. (multiple points of test) 184 * The process is asynchronous and the passed result callback function will be called when it's done, then the test can be started. 185 */ 186 selectServer: function(result) { 187 if (this._state != 1) { 188 if (this._state == 0) throw "No test points added"; 189 if (this._state == 2) throw "Server already selected"; 190 if (this._state >= 3) 191 throw "You can't select a server while the test is running"; 192 } 193 if (this._selectServerCalled) throw "selectServer already called"; else this._selectServerCalled=true; 194 /*this function goes through a list of servers. For each server, the ping is measured, then the server with the function selected is called with the best server, or null if all the servers were down. 195 */ 196 var select = function(serverList, selected) { 197 //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. 198 var PING_TIMEOUT = 2000; 199 var USE_PING_TIMEOUT = true; //will be disabled on unsupported browsers 200 if (/MSIE.(\d+\.\d+)/i.test(navigator.userAgent)) { 201 //IE11 doesn't support XHR timeout 202 USE_PING_TIMEOUT = false; 203 } 204 var ping = function(url, rtt) { 205 url += (url.match(/\?/) ? "&" : "?") + "cors=true"; 206 var xhr = new XMLHttpRequest(); 207 var t = new Date().getTime(); 208 xhr.onload = function() { 209 if (xhr.responseText.length == 0) { 210 //we expect an empty response 211 var instspd = new Date().getTime() - t; //rough timing estimate 212 try { 213 //try to get more accurate timing using performance API 214 var p = performance.getEntriesByName(url); 215 p = p[p.length - 1]; 216 var d = p.responseStart - p.requestStart; 217 if (d <= 0) d = p.duration; 218 if (d > 0 && d < instspd) instspd = d; 219 } catch (e) {} 220 rtt(instspd); 221 } else rtt(-1); 222 }.bind(this); 223 xhr.onerror = function() { 224 rtt(-1); 225 }.bind(this); 226 xhr.open("GET", url); 227 if (USE_PING_TIMEOUT) { 228 try { 229 xhr.timeout = PING_TIMEOUT;
230 xhr.ontimeout = xhr.onerror; 231 } catch (e) {} 232 } 233 xhr.send(); 234 }.bind(this); 235 236 //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. 237 var PINGS = 3, //up to 3 pings are performed, unless the server is down... 238 SLOW_THRESHOLD = 500; //...or one of the pings is above this threshold 239 var checkServer = function(server, done) { 240 var i = 0; 241 server.pingT = -1; 242 if (server.server.indexOf(location.protocol) == -1) done(); 243 else { 244 var nextPing = function() { 245 if (i++ == PINGS) { 246 done(); 247 return; 248 } 249 ping( 250 server.server + server.pingURL, 251 function(t) { 252 if (t >= 0) { 253 if (t < server.pingT || server.pingT == -1) server.pingT = t; 254 if (t < SLOW_THRESHOLD) nextPing(); 255 else done(); 256 } else done(); 257 }.bind(this) 258 ); 259 }.bind(this); 260 nextPing(); 261 } 262 }.bind(this); 263 //check servers in list, one by one 264 var i = 0; 265 var done = function() { 266 var bestServer = null; 267 for (var i = 0; i < serverList.length; i++) { 268 if ( 269 serverList[i].pingT != -1 && 270 (bestServer == null || serverList[i].pingT < bestServer.pingT) 271 ) 272 bestServer = serverList[i]; 273 } 274 selected(bestServer); 275 }.bind(this); 276 var nextServer = function() { 277 if (i == serverList.length) { 278 done(); 279 return; 280 } 281 checkServer(serverList[i++], nextServer); 282 }.bind(this); 283 nextServer(); 284 }.bind(this); 285 286 //parallel server selection 287 var CONCURRENCY = 6; 288 var serverLists = []; 289 for (var i = 0; i < CONCURRENCY; i++) { 290 serverLists[i] = []; 291 } 292 for (var i = 0; i < this._serverList.length; i++) { 293 serverLists[i % CONCURRENCY].push(this._serverList[i]); 294 } 295 var completed = 0; 296 var bestServer = null; 297 for (var i = 0; i < CONCURRENCY; i++) { 298 select( 299 serverLists[i], 300 function(server) { 301 if (server != null) { 302 if (bestServer == null || server.pingT < bestServer.pingT) 303 bestServer = server; 304 } 305 completed++; 306 if (completed == CONCURRENCY) { 307 this._selectedServer = bestServer; 308 this._state = 2; 309 if (result) result(bestServer); 310 } 311 }.bind(this) 312 ); 313 } 314 }, 315 /** 316 * Starts the test. 317 * During the test, the onupdate(data) callback function will be called periodically with data from the worker. 318 * 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. 319 */ 320 start: function() { 321 if (this._state == 3) throw "Test already running"; 322 this.worker = new Worker("assets/scripts/speedtest_worker.js?r=" + Math.random()); 323 this.worker.onmessage = function(e) { 324 if (e.data === this._prevData) return; 325 else this._prevData = e.data; 326 var data = JSON.parse(e.data); 327 try { 328 if (this.onupdate) this.onupdate(data); 329 } catch (e) { 330 console.error("Speedtest onupdate event threw exception: " + e); 331 } 332 if (data.testState >= 4) { 333 this._state = 4; 334 try { 335 if (this.onend) this.onend(data.testState == 5); 336 } catch (e) { 337 console.error("Speedtest onend event threw exception: " + e); 338 } 339 } 340 if (data.testState >= 5) { 341 clearInterval(this.updater); 342 } 343 }.bind(this); 344 this.updater = setInterval( 345 function() { 346 this.worker.postMessage("status"); 347 }.bind(this), 348 200 349 ); 350 if (this._state == 1) 351 throw "When using multiple points of test, you must call selectServer before starting the test"; 352 if (this._state == 2) { 353 this._settings.url_dl = 354 this._selectedServer.server + this._selectedServer.dlURL; 355 this._settings.url_ul = 356 this._selectedServer.server + this._selectedServer.ulURL; 357 this._settings.url_ping = 358 this._selectedServer.server + this._selectedServer.pingURL; 359 this._settings.url_getIp = 360 // this._selectedServer.server + this._selectedServer.getIpURL; 361 this._settings.url_telemetry = 'https://www.cdntoolkit.com/api/tools/diagnostic'; // 362 if (typeof this._originalExtra !== "undefined") { 363 this._settings.telemetry_extra = JSON.stringify({ 364 server: this._selectedServer.name, 365 extra: this._originalExtra 366 }); 367 } else 368 this._settings.telemetry_extra = JSON.stringify({ 369 server: this._selectedServer.name 370 }); 371 } 372 this._state = 3; 373 this.worker.postMessage("start " + JSON.stringify(this._settings)); 374 }, 375 /** 376 * Aborts the test while it's running. 377 */ 378 abort: function() { 379 if (this._state < 3) throw "You cannot abort a test that's not started yet"; 380 if (this._state < 4) this.worker.postMessage("abort"); 381 }, 382 /* 383 * Send test data to upload 384 */ 385 sendTelemetry: function(testData) { 386 this.worker.postMessage("sendTelemetry " + JSON.stringify(testData)); 387 } 388 }; 389
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.