stability_worker.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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; // failed request percentage
  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_interval: 200, // minimum ms between pings to limit sample rate
  26. ping_timeout: 5000,
  27. ping_allowPerformanceApi: true,
  28. mpot: false
  29. };
  30. let xhr = null;
  31. let startTime = 0;
  32. let prevInstspd = 0;
  33. let aborted = false;
  34. function url_sep(url) {
  35. return url.match(/\?/) ? "&" : "?";
  36. }
  37. this.addEventListener("message", function (e) {
  38. const params = e.data.split(" ");
  39. if (params[0] === "status") {
  40. // return current state with delta ping data
  41. const newData = pingData.slice(lastReportedIndex);
  42. lastReportedIndex = pingData.length;
  43. postMessage(
  44. JSON.stringify({
  45. testState: testState,
  46. currentPing: currentPing,
  47. avgPing: avgPing,
  48. minPing: minPing,
  49. maxPing: maxPing,
  50. jitter: jitter,
  51. packetLoss: packetLoss,
  52. elapsed: elapsed,
  53. duration: settings.duration,
  54. progress: progress,
  55. pingData: newData,
  56. totalSamples: totalSamples,
  57. failedSamples: failedSamples
  58. })
  59. );
  60. }
  61. if (params[0] === "start" && testState === -1) {
  62. testState = 0;
  63. try {
  64. let s = {};
  65. try {
  66. const ss = e.data.substring(6);
  67. if (ss) s = JSON.parse(ss);
  68. } catch (e) {
  69. console.warn("Error parsing settings JSON");
  70. }
  71. for (let key in s) {
  72. if (typeof settings[key] !== "undefined") settings[key] = s[key];
  73. }
  74. } catch (e) {
  75. console.warn("Error applying settings: " + e);
  76. }
  77. // start the stability test
  78. aborted = false;
  79. startTime = new Date().getTime();
  80. testState = 1;
  81. doPing();
  82. }
  83. if (params[0] === "abort") {
  84. if (testState >= 4) return;
  85. aborted = true;
  86. testState = 5;
  87. if (xhr) {
  88. try {
  89. xhr.abort();
  90. } catch (e) {}
  91. }
  92. }
  93. });
  94. function recordPing(instspd) {
  95. // guard against 0ms pings
  96. if (instspd < 1) instspd = prevInstspd;
  97. if (instspd < 1) instspd = 1;
  98. totalSamples++;
  99. currentPing = instspd;
  100. pingSum += instspd;
  101. avgPing = parseFloat((pingSum / (totalSamples - failedSamples)).toFixed(2));
  102. if (minPing === -1 || instspd < minPing) minPing = instspd;
  103. if (instspd > maxPing) maxPing = instspd;
  104. // jitter calculation (same weighted formula as speedtest_worker.js)
  105. if (totalSamples > 1 && prevInstspd > 0) {
  106. const instjitter = Math.abs(instspd - prevInstspd);
  107. if (totalSamples === 2) {
  108. jitter = instjitter;
  109. } else {
  110. jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2;
  111. }
  112. }
  113. prevInstspd = instspd;
  114. // failed request percentage
  115. packetLoss = totalSamples > 0 ? parseFloat(((failedSamples / totalSamples) * 100).toFixed(2)) : 0;
  116. // record data point
  117. const now = new Date().getTime();
  118. elapsed = (now - startTime) / 1000;
  119. pingData.push({ t: elapsed, ping: parseFloat(instspd.toFixed(2)), lost: false });
  120. }
  121. function recordLoss() {
  122. const now = new Date().getTime();
  123. totalSamples++;
  124. failedSamples++;
  125. packetLoss = parseFloat(((failedSamples / totalSamples) * 100).toFixed(2));
  126. elapsed = (now - startTime) / 1000;
  127. pingData.push({ t: elapsed, ping: 0, lost: true });
  128. }
  129. // pace pings to avoid excessive sample rates on low-latency links
  130. function schedulePing(rtt) {
  131. const delay = Math.max(0, settings.ping_interval - rtt);
  132. if (delay > 0) {
  133. setTimeout(doPing, delay);
  134. } else {
  135. doPing();
  136. }
  137. }
  138. function doPing() {
  139. if (aborted || testState >= 4) return;
  140. // check if duration exceeded
  141. const now = new Date().getTime();
  142. elapsed = (now - startTime) / 1000;
  143. progress = Math.min(1, elapsed / settings.duration);
  144. if (elapsed >= settings.duration) {
  145. testState = 4;
  146. progress = 1;
  147. return;
  148. }
  149. // external ping mode: use fetch with no-cors
  150. if (settings.url_ping_external) {
  151. doPingExternal();
  152. return;
  153. }
  154. const prevT = new Date().getTime();
  155. xhr = new XMLHttpRequest();
  156. xhr.onload = function () {
  157. if (aborted || testState >= 4) return;
  158. const now = new Date().getTime();
  159. let instspd = now - prevT;
  160. if (settings.ping_allowPerformanceApi) {
  161. try {
  162. let p = performance.getEntries();
  163. p = p[p.length - 1];
  164. let d = p.responseStart - p.requestStart;
  165. if (d <= 0) d = p.duration;
  166. if (d > 0 && d < instspd) instspd = d;
  167. } catch (e) {
  168. // Performance API not available, use estimate
  169. }
  170. }
  171. recordPing(instspd);
  172. schedulePing(instspd);
  173. };
  174. xhr.onerror = function () {
  175. if (aborted || testState >= 4) return;
  176. recordLoss();
  177. schedulePing(0);
  178. };
  179. xhr.ontimeout = xhr.onerror;
  180. xhr.open(
  181. "GET",
  182. settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(),
  183. true
  184. );
  185. try {
  186. xhr.timeout = settings.ping_timeout;
  187. } catch (e) {}
  188. xhr.send();
  189. }
  190. // ping an external host using fetch with no-cors (opaque response, but timing still works)
  191. function doPingExternal() {
  192. const prevT = new Date().getTime();
  193. const remainingMs = Math.max(1, settings.duration * 1000 - (prevT - startTime));
  194. const timeoutMs = Math.min(settings.ping_timeout, remainingMs);
  195. const url =
  196. settings.url_ping_external + (settings.url_ping_external.indexOf("?") >= 0 ? "&" : "?") + "r=" + Math.random();
  197. let timeoutId = null;
  198. const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
  199. const fetchOptions = { mode: "no-cors", cache: "no-store" };
  200. if (controller) fetchOptions.signal = controller.signal;
  201. const timeout = new Promise(function (_, reject) {
  202. timeoutId = setTimeout(function () {
  203. if (controller) controller.abort();
  204. reject(new Error("timeout"));
  205. }, timeoutMs);
  206. });
  207. Promise.race([fetch(url, fetchOptions), timeout])
  208. .then(function () {
  209. if (timeoutId) clearTimeout(timeoutId);
  210. if (aborted || testState >= 4) return;
  211. const instspd = new Date().getTime() - prevT;
  212. recordPing(instspd);
  213. schedulePing(instspd);
  214. })
  215. .catch(function () {
  216. if (timeoutId) clearTimeout(timeoutId);
  217. if (aborted || testState >= 4) return;
  218. recordLoss();
  219. schedulePing(0);
  220. });
  221. }