stability_worker.js 6.0 KB

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