stability_worker.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. /*
  2. LibreSpeed - Stability Test Worker
  3. https://github.com/librespeed/speedtest/
  4. GNU LGPLv3 License
  5. */
  6. // data reported to main thread
  7. let testState = -1; // -1=idle, 0=starting, 1=running, 4=finished, 5=aborted
  8. let currentPing = 0;
  9. let avgPing = 0;
  10. let minPing = -1;
  11. let maxPing = 0;
  12. let jitter = 0;
  13. let packetLoss = 0;
  14. let elapsed = 0;
  15. let progress = 0;
  16. let pingData = []; // all ping data points {t: elapsedMs, ping: ms, lost: bool}
  17. let lastReportedIndex = 0; // for delta delivery
  18. let totalSamples = 0;
  19. let failedSamples = 0;
  20. let pingSum = 0;
  21. let settings = {
  22. url_ping: "backend/empty.php",
  23. url_ping_external: "", // external URL to ping (uses fetch no-cors, e.g. "https://www.google.com/generate_204")
  24. duration: 60, // seconds
  25. ping_allowPerformanceApi: true,
  26. mpot: false
  27. };
  28. let xhr = null;
  29. let startTime = 0;
  30. let prevInstspd = 0;
  31. let aborted = false;
  32. function url_sep(url) {
  33. return url.match(/\?/) ? "&" : "?";
  34. }
  35. this.addEventListener("message", function (e) {
  36. const params = e.data.split(" ");
  37. if (params[0] === "status") {
  38. // return current state with delta ping data
  39. const newData = pingData.slice(lastReportedIndex);
  40. lastReportedIndex = pingData.length;
  41. postMessage(
  42. JSON.stringify({
  43. testState: testState,
  44. currentPing: currentPing,
  45. avgPing: avgPing,
  46. minPing: minPing,
  47. maxPing: maxPing,
  48. jitter: jitter,
  49. packetLoss: packetLoss,
  50. elapsed: elapsed,
  51. duration: settings.duration,
  52. progress: progress,
  53. pingData: newData,
  54. totalSamples: totalSamples,
  55. failedSamples: failedSamples
  56. })
  57. );
  58. }
  59. if (params[0] === "start" && testState === -1) {
  60. testState = 0;
  61. try {
  62. let s = {};
  63. try {
  64. const ss = e.data.substring(6);
  65. if (ss) s = JSON.parse(ss);
  66. } catch (e) {
  67. console.warn("Error parsing settings JSON");
  68. }
  69. for (let key in s) {
  70. if (typeof settings[key] !== "undefined") settings[key] = s[key];
  71. }
  72. } catch (e) {
  73. console.warn("Error applying settings: " + e);
  74. }
  75. // start the stability test
  76. aborted = false;
  77. startTime = new Date().getTime();
  78. testState = 1;
  79. doPing();
  80. }
  81. if (params[0] === "abort") {
  82. if (testState >= 4) return;
  83. aborted = true;
  84. testState = 5;
  85. if (xhr) {
  86. try {
  87. xhr.abort();
  88. } catch (e) {}
  89. }
  90. }
  91. });
  92. function recordPing(instspd) {
  93. // guard against 0ms pings
  94. if (instspd < 1) instspd = prevInstspd;
  95. if (instspd < 1) instspd = 1;
  96. totalSamples++;
  97. currentPing = instspd;
  98. pingSum += instspd;
  99. avgPing = parseFloat((pingSum / (totalSamples - failedSamples)).toFixed(2));
  100. if (minPing === -1 || instspd < minPing) minPing = instspd;
  101. if (instspd > maxPing) maxPing = instspd;
  102. // jitter calculation (same weighted formula as speedtest_worker.js)
  103. if (totalSamples > 1 && prevInstspd > 0) {
  104. const instjitter = Math.abs(instspd - prevInstspd);
  105. if (totalSamples === 2) {
  106. jitter = instjitter;
  107. } else {
  108. jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2;
  109. }
  110. }
  111. prevInstspd = instspd;
  112. // packet loss
  113. packetLoss = totalSamples > 0 ? parseFloat(((failedSamples / totalSamples) * 100).toFixed(2)) : 0;
  114. // record data point
  115. const now = new Date().getTime();
  116. elapsed = (now - startTime) / 1000;
  117. pingData.push({ t: elapsed, ping: parseFloat(instspd.toFixed(2)), lost: false });
  118. }
  119. function recordLoss() {
  120. const now = new Date().getTime();
  121. totalSamples++;
  122. failedSamples++;
  123. packetLoss = parseFloat(((failedSamples / totalSamples) * 100).toFixed(2));
  124. elapsed = (now - startTime) / 1000;
  125. pingData.push({ t: elapsed, ping: 0, lost: true });
  126. }
  127. function doPing() {
  128. if (aborted || testState >= 4) return;
  129. // check if duration exceeded
  130. const now = new Date().getTime();
  131. elapsed = (now - startTime) / 1000;
  132. progress = Math.min(1, elapsed / settings.duration);
  133. if (elapsed >= settings.duration) {
  134. testState = 4;
  135. progress = 1;
  136. return;
  137. }
  138. // external ping mode: use fetch with no-cors
  139. if (settings.url_ping_external) {
  140. doPingExternal();
  141. return;
  142. }
  143. const prevT = new Date().getTime();
  144. xhr = new XMLHttpRequest();
  145. xhr.onload = function () {
  146. if (aborted || testState >= 4) return;
  147. const now = new Date().getTime();
  148. let instspd = now - prevT;
  149. if (settings.ping_allowPerformanceApi) {
  150. try {
  151. let p = performance.getEntries();
  152. p = p[p.length - 1];
  153. let d = p.responseStart - p.requestStart;
  154. if (d <= 0) d = p.duration;
  155. if (d > 0 && d < instspd) instspd = d;
  156. } catch (e) {
  157. // Performance API not available, use estimate
  158. }
  159. }
  160. recordPing(instspd);
  161. doPing();
  162. };
  163. xhr.onerror = function () {
  164. if (aborted || testState >= 4) return;
  165. recordLoss();
  166. doPing();
  167. };
  168. xhr.ontimeout = xhr.onerror;
  169. xhr.open(
  170. "GET",
  171. settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(),
  172. true
  173. );
  174. try {
  175. xhr.timeout = 5000;
  176. } catch (e) {}
  177. xhr.send();
  178. }
  179. // ping an external host using fetch with no-cors (opaque response, but timing still works)
  180. function doPingExternal() {
  181. const prevT = new Date().getTime();
  182. const url =
  183. settings.url_ping_external + (settings.url_ping_external.indexOf("?") >= 0 ? "&" : "?") + "r=" + Math.random();
  184. fetch(url, { mode: "no-cors", cache: "no-store" })
  185. .then(function () {
  186. if (aborted || testState >= 4) return;
  187. const instspd = new Date().getTime() - prevT;
  188. recordPing(instspd);
  189. doPing();
  190. })
  191. .catch(function () {
  192. if (aborted || testState >= 4) return;
  193. recordLoss();
  194. doPing();
  195. });
  196. }