blob: 4b0958a1cf96d953113093d96f2b85d2ce3aa116 [file] [log] [blame]
Sergiusz Bazanskia51df9c2019-07-13 16:07:13 +02001/*
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
9var testState = -1; // -1=not started, 0=starting, 1=download test, 2=ping+jitter test, 3=upload test, 4=finished, 5=abort
10var dlStatus = ""; // download speed in megabit/s with 2 decimal digits
11var ulStatus = ""; // upload speed in megabit/s with 2 decimal digits
12var pingStatus = ""; // ping in milliseconds with 2 decimal digits
13var jitterStatus = ""; // jitter in milliseconds with 2 decimal digits
14var clientIp = ""; // client's IP address as reported by getIP.php
15var dlProgress = 0; //progress of download test 0-1
16var ulProgress = 0; //progress of upload test 0-1
17var pingProgress = 0; //progress of ping+jitter test 0-1
18var testId = null; //test ID (sent back by telemetry if used, null otherwise)
19
20var log = ""; //telemetry log
21function tlog(s) {
22 if (settings.telemetry_level >= 2) {
23 log += Date.now() + ": " + s + "\n";
24 }
25}
26function tverb(s) {
27 if (settings.telemetry_level >= 3) {
28 log += Date.now() + ": " + s + "\n";
29 }
30}
31function 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
39var 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: "garbage", // path to a large file or garbage.php, used for download test. must be relative to this js file
49 url_ul: "empty", // path to an empty file, used for upload test. must be relative to this js file
50 url_ping: "empty", // path to an empty file, used for ping test. must be relative to this js file
51 url_getIp: "ip", // 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
70var xhr = null; // array of currently active xhr requests
71var interval = null; // timer used in tests
72var 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*/
77function 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*/
89this.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 tlog("manually aborted");
253 clearRequests(); // stop all xhr activity
254 runNextTest = null;
255 if (interval) clearInterval(interval); // clear timer if present
256 if (settings.telemetry_level > 1) sendTelemetry(function() {});
257 testState = 5; //set test as aborted
258 dlStatus = "";
259 ulStatus = "";
260 pingStatus = "";
261 jitterStatus = "";
262 clientIp = "";
263 dlProgress = 0;
264 ulProgress = 0;
265 pingProgress = 0;
266 }
267});
268// stops all XHR activity, aggressively
269function clearRequests() {
270 tverb("stopping pending XHRs");
271 if (xhr) {
272 for (var i = 0; i < xhr.length; i++) {
273 try {
274 xhr[i].onprogress = null;
275 xhr[i].onload = null;
276 xhr[i].onerror = null;
277 } catch (e) {}
278 try {
279 xhr[i].upload.onprogress = null;
280 xhr[i].upload.onload = null;
281 xhr[i].upload.onerror = null;
282 } catch (e) {}
283 try {
284 xhr[i].abort();
285 } catch (e) {}
286 try {
287 delete xhr[i];
288 } catch (e) {}
289 }
290 xhr = null;
291 }
292}
293// gets client's IP using url_getIp, then calls the done function
294var ipCalled = false; // used to prevent multiple accidental calls to getIp
295var ispInfo = ""; //used for telemetry
296function getIp(done) {
297 tverb("getIp");
298 if (ipCalled) return;
299 else ipCalled = true; // getIp already called?
300 var startT = new Date().getTime();
301 xhr = new XMLHttpRequest();
302 xhr.onload = function() {
303 tlog("IP: " + xhr.responseText + ", took " + (new Date().getTime() - startT) + "ms");
304 try {
305 var data = JSON.parse(xhr.responseText);
306 clientIp = data.processedString;
307 ispInfo = data.rawIspInfo;
308 } catch (e) {
309 clientIp = xhr.responseText;
310 ispInfo = "";
311 }
312 done();
313 };
314 xhr.onerror = function() {
315 tlog("getIp failed, took " + (new Date().getTime() - startT) + "ms");
316 done();
317 };
318 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);
319 xhr.send();
320}
321// download test, calls done function when it's over
322var dlCalled = false; // used to prevent multiple accidental calls to dlTest
323function dlTest(done) {
324 tverb("dlTest");
325 if (dlCalled) return;
326 else dlCalled = true; // dlTest already called?
327 var totLoaded = 0.0, // total number of loaded bytes
328 startT = new Date().getTime(), // timestamp when test was started
329 bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
330 graceTimeDone = false, //set to true after the grace time is past
331 failed = false; // set to true if a stream fails
332 xhr = [];
333 // function to create a download stream. streams are slightly delayed so that they will not end at the same time
334 var testStream = function(i, delay) {
335 setTimeout(
336 function() {
337 if (testState !== 1) return; // delayed stream ended up starting after the end of the download test
338 tverb("dl test stream started " + i + " " + delay);
339 var prevLoaded = 0; // number of bytes loaded last time onprogress was called
340 var x = new XMLHttpRequest();
341 xhr[i] = x;
342 xhr[i].onprogress = function(event) {
343 tverb("dl stream progress event " + i + " " + event.loaded);
344 if (testState !== 1) {
345 try {
346 x.abort();
347 } catch (e) {}
348 } // just in case this XHR is still running after the download test
349 // progress event, add number of new loaded bytes to totLoaded
350 var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
351 if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
352 totLoaded += loadDiff;
353 prevLoaded = event.loaded;
354 }.bind(this);
355 xhr[i].onload = function() {
356 // the large file has been loaded entirely, start again
357 tverb("dl stream finished " + i);
358 try {
359 xhr[i].abort();
360 } catch (e) {} // reset the stream data to empty ram
361 testStream(i, 0);
362 }.bind(this);
363 xhr[i].onerror = function() {
364 // error
365 tverb("dl stream failed " + i);
366 if (settings.xhr_ignoreErrors === 0) failed = true; //abort
367 try {
368 xhr[i].abort();
369 } catch (e) {}
370 delete xhr[i];
371 if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream
372 }.bind(this);
373 // send xhr
374 try {
375 if (settings.xhr_dlUseBlob) xhr[i].responseType = "blob";
376 else xhr[i].responseType = "arraybuffer";
377 } catch (e) {}
378 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
379 xhr[i].send();
380 }.bind(this),
381 1 + delay
382 );
383 }.bind(this);
384 // open streams
385 for (var i = 0; i < settings.xhr_dlMultistream; i++) {
386 testStream(i, settings.xhr_multistreamDelay * i);
387 }
388 // every 200ms, update dlStatus
389 interval = setInterval(
390 function() {
391 tverb("DL: " + dlStatus + (graceTimeDone ? "" : " (in grace time)"));
392 var t = new Date().getTime() - startT;
393 if (graceTimeDone) dlProgress = (t + bonusT) / (settings.time_dl_max * 1000);
394 if (t < 200) return;
395 if (!graceTimeDone) {
396 if (t > 1000 * settings.time_dlGraceTime) {
397 if (totLoaded > 0) {
398 // if the connection is so slow that we didn't get a single chunk yet, do not reset
399 startT = new Date().getTime();
400 bonusT = 0;
401 totLoaded = 0.0;
402 }
403 graceTimeDone = true;
404 }
405 } else {
406 var speed = totLoaded / (t / 1000.0);
407 if (settings.time_auto) {
408 //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
409 var bonus = (6.4 * speed) / 100000;
410 bonusT += bonus > 800 ? 800 : bonus;
411 }
412 //update status
413 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
414 if ((t + bonusT) / 1000.0 > settings.time_dl_max || failed) {
415 // test is over, stop streams and timer
416 if (failed || isNaN(dlStatus)) dlStatus = "Fail";
417 clearRequests();
418 clearInterval(interval);
419 dlProgress = 1;
420 tlog("dlTest: " + dlStatus + ", took " + (new Date().getTime() - startT) + "ms");
421 done();
422 }
423 }
424 }.bind(this),
425 200
426 );
427}
428// upload test, calls done function whent it's over
429var ulCalled = false; // used to prevent multiple accidental calls to ulTest
430function ulTest(done) {
431 tverb("ulTest");
432 if (ulCalled) return;
433 else ulCalled = true; // ulTest already called?
434 // garbage data for upload test
435 var r = new ArrayBuffer(1048576);
436 var maxInt = Math.pow(2, 32) - 1;
437 try {
438 r = new Uint32Array(r);
439 for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
440 } catch (e) {}
441 var req = [];
442 var reqsmall = [];
443 for (var i = 0; i < settings.xhr_ul_blob_megabytes; i++) req.push(r);
444 req = new Blob(req);
445 r = new ArrayBuffer(262144);
446 try {
447 r = new Uint32Array(r);
448 for (var i = 0; i < r.length; i++) r[i] = Math.random() * maxInt;
449 } catch (e) {}
450 reqsmall.push(r);
451 reqsmall = new Blob(reqsmall);
452 var testFunction = function() {
453 var totLoaded = 0.0, // total number of transmitted bytes
454 startT = new Date().getTime(), // timestamp when test was started
455 bonusT = 0, //how many milliseconds the test has been shortened by (higher on faster connections)
456 graceTimeDone = false, //set to true after the grace time is past
457 failed = false; // set to true if a stream fails
458 xhr = [];
459 // function to create an upload stream. streams are slightly delayed so that they will not end at the same time
460 var testStream = function(i, delay) {
461 setTimeout(
462 function() {
463 if (testState !== 3) return; // delayed stream ended up starting after the end of the upload test
464 tverb("ul test stream started " + i + " " + delay);
465 var prevLoaded = 0; // number of bytes transmitted last time onprogress was called
466 var x = new XMLHttpRequest();
467 xhr[i] = x;
468 var ie11workaround;
469 if (settings.forceIE11Workaround) ie11workaround = true;
470 else {
471 try {
472 xhr[i].upload.onprogress;
473 ie11workaround = false;
474 } catch (e) {
475 ie11workaround = true;
476 }
477 }
478 if (ie11workaround) {
479 // 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
480 xhr[i].onload = xhr[i].onerror = function() {
481 tverb("ul stream progress event (ie11wa)");
482 totLoaded += reqsmall.size;
483 testStream(i, 0);
484 };
485 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
486 try {
487 xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
488 } catch (e) {}
489 //No Content-Type header in MPOT branch because it triggers bugs in some browsers
490 xhr[i].send(reqsmall);
491 } else {
492 // REGULAR version, no workaround
493 xhr[i].upload.onprogress = function(event) {
494 tverb("ul stream progress event " + i + " " + event.loaded);
495 if (testState !== 3) {
496 try {
497 x.abort();
498 } catch (e) {}
499 } // just in case this XHR is still running after the upload test
500 // progress event, add number of new loaded bytes to totLoaded
501 var loadDiff = event.loaded <= 0 ? 0 : event.loaded - prevLoaded;
502 if (isNaN(loadDiff) || !isFinite(loadDiff) || loadDiff < 0) return; // just in case
503 totLoaded += loadDiff;
504 prevLoaded = event.loaded;
505 }.bind(this);
506 xhr[i].upload.onload = function() {
507 // this stream sent all the garbage data, start again
508 tverb("ul stream finished " + i);
509 testStream(i, 0);
510 }.bind(this);
511 xhr[i].upload.onerror = function() {
512 tverb("ul stream failed " + i);
513 if (settings.xhr_ignoreErrors === 0) failed = true; //abort
514 try {
515 xhr[i].abort();
516 } catch (e) {}
517 delete xhr[i];
518 if (settings.xhr_ignoreErrors === 1) testStream(i, 0); //restart stream
519 }.bind(this);
520 // send xhr
521 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
522 try {
523 xhr[i].setRequestHeader("Content-Encoding", "identity"); // disable compression (some browsers may refuse it, but data is incompressible anyway)
524 } catch (e) {}
525 //No Content-Type header in MPOT branch because it triggers bugs in some browsers
526 xhr[i].send(req);
527 }
528 }.bind(this),
529 1
530 );
531 }.bind(this);
532 // open streams
533 for (var i = 0; i < settings.xhr_ulMultistream; i++) {
534 testStream(i, settings.xhr_multistreamDelay * i);
535 }
536 // every 200ms, update ulStatus
537 interval = setInterval(
538 function() {
539 tverb("UL: " + ulStatus + (graceTimeDone ? "" : " (in grace time)"));
540 var t = new Date().getTime() - startT;
541 if (graceTimeDone) ulProgress = (t + bonusT) / (settings.time_ul_max * 1000);
542 if (t < 200) return;
543 if (!graceTimeDone) {
544 if (t > 1000 * settings.time_ulGraceTime) {
545 if (totLoaded > 0) {
546 // if the connection is so slow that we didn't get a single chunk yet, do not reset
547 startT = new Date().getTime();
548 bonusT = 0;
549 totLoaded = 0.0;
550 }
551 graceTimeDone = true;
552 }
553 } else {
554 var speed = totLoaded / (t / 1000.0);
555 if (settings.time_auto) {
556 //decide how much to shorten the test. Every 200ms, the test is shortened by the bonusT calculated here
557 var bonus = (6.4 * speed) / 100000;
558 bonusT += bonus > 800 ? 800 : bonus;
559 }
560 //update status
561 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
562 if ((t + bonusT) / 1000.0 > settings.time_ul_max || failed) {
563 // test is over, stop streams and timer
564 if (failed || isNaN(ulStatus)) ulStatus = "Fail";
565 clearRequests();
566 clearInterval(interval);
567 ulProgress = 1;
568 tlog("ulTest: " + ulStatus + ", took " + (new Date().getTime() - startT) + "ms");
569 done();
570 }
571 }
572 }.bind(this),
573 200
574 );
575 }.bind(this);
576 if (settings.mpot) {
577 tverb("Sending POST request before performing upload test");
578 xhr = [];
579 xhr[0] = new XMLHttpRequest();
580 xhr[0].onload = xhr[0].onerror = function() {
581 tverb("POST request sent, starting upload test");
582 testFunction();
583 }.bind(this);
584 xhr[0].open("POST", settings.url_ul);
585 xhr[0].send();
586 } else testFunction();
587}
588// ping+jitter test, function done is called when it's over
589var ptCalled = false; // used to prevent multiple accidental calls to pingTest
590function pingTest(done) {
591 tverb("pingTest");
592 if (ptCalled) return;
593 else ptCalled = true; // pingTest already called?
594 var startT = new Date().getTime(); //when the test was started
595 var prevT = null; // last time a pong was received
596 var ping = 0.0; // current ping value
597 var jitter = 0.0; // current jitter value
598 var i = 0; // counter of pongs received
599 var prevInstspd = 0; // last ping time, used for jitter calculation
600 xhr = [];
601 // ping function
602 var doPing = function() {
603 tverb("ping");
604 pingProgress = i / settings.count_ping;
605 prevT = new Date().getTime();
606 xhr[0] = new XMLHttpRequest();
607 xhr[0].onload = function() {
608 // pong
609 tverb("pong");
610 if (i === 0) {
611 prevT = new Date().getTime(); // first pong
612 } else {
613 var instspd = new Date().getTime() - prevT;
614 if (settings.ping_allowPerformanceApi) {
615 try {
616 //try to get accurate performance timing using performance api
617 var p = performance.getEntries();
618 p = p[p.length - 1];
619 var d = p.responseStart - p.requestStart;
620 if (d <= 0) d = p.duration;
621 if (d > 0 && d < instspd) instspd = d;
622 } catch (e) {
623 //if not possible, keep the estimate
624 tverb("Performance API not supported, using estimate");
625 }
626 }
627 //noticed that some browsers randomly have 0ms ping
628 if (instspd < 1) instspd = prevInstspd;
629 if (instspd < 1) instspd = 1;
630 var instjitter = Math.abs(instspd - prevInstspd);
631 if (i === 1) ping = instspd;
632 /* first ping, can't tell jitter yet*/ else {
633 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
634 if (i === 2) jitter = instjitter;
635 //discard the first jitter measurement because it might be much higher than it should be
636 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.
637 }
638 prevInstspd = instspd;
639 }
640 pingStatus = ping.toFixed(2);
641 jitterStatus = jitter.toFixed(2);
642 i++;
643 tverb("ping: " + pingStatus + " jitter: " + jitterStatus);
644 if (i < settings.count_ping) doPing();
645 else {
646 // more pings to do?
647 pingProgress = 1;
648 tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
649 done();
650 }
651 }.bind(this);
652 xhr[0].onerror = function() {
653 // a ping failed, cancel test
654 tverb("ping failed");
655 if (settings.xhr_ignoreErrors === 0) {
656 //abort
657 pingStatus = "Fail";
658 jitterStatus = "Fail";
659 clearRequests();
660 tlog("ping test failed, took " + (new Date().getTime() - startT) + "ms");
661 pingProgress = 1;
662 done();
663 }
664 if (settings.xhr_ignoreErrors === 1) doPing(); //retry ping
665 if (settings.xhr_ignoreErrors === 2) {
666 //ignore failed ping
667 i++;
668 if (i < settings.count_ping) doPing();
669 else {
670 // more pings to do?
671 pingProgress = 1;
672 tlog("ping: " + pingStatus + " jitter: " + jitterStatus + ", took " + (new Date().getTime() - startT) + "ms");
673 done();
674 }
675 }
676 }.bind(this);
677 // send xhr
678 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
679 xhr[0].send();
680 }.bind(this);
681 doPing(); // start first ping
682}
683// telemetry
684function sendTelemetry(done) {
685 if (settings.telemetry_level < 1) return;
686 xhr = new XMLHttpRequest();
687 xhr.onload = function() {
688 try {
689 var parts = xhr.responseText.split(" ");
690 if (parts[0] == "id") {
691 try {
692 var id = parts[1];
693 done(id);
694 } catch (e) {
695 done(null);
696 }
697 } else done(null);
698 } catch (e) {
699 done(null);
700 }
701 };
702 xhr.onerror = function() {
703 console.log("TELEMETRY ERROR " + xhr.status);
704 done(null);
705 };
706 xhr.open("POST", settings.url_telemetry + url_sep(settings.url_telemetry) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(), true);
707 var telemetryIspInfo = {
708 processedString: clientIp,
709 rawIspInfo: typeof ispInfo === "object" ? ispInfo : ""
710 };
711 try {
712 var fd = new FormData();
713 fd.append("ispinfo", JSON.stringify(telemetryIspInfo));
714 fd.append("dl", dlStatus);
715 fd.append("ul", ulStatus);
716 fd.append("ping", pingStatus);
717 fd.append("jitter", jitterStatus);
718 fd.append("log", settings.telemetry_level > 1 ? log : "");
719 fd.append("extra", settings.telemetry_extra);
720 xhr.send(fd);
721 } catch (ex) {
722 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 : "");
723 xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
724 xhr.send(postData);
725 }
726}