mirror of
https://github.com/librespeed/speedtest.git
synced 2026-06-28 04:42:06 +00:00
227 lines
6 KiB
JavaScript
227 lines
6 KiB
JavaScript
/*
|
|
LibreSpeed - Stability Test Worker
|
|
https://github.com/librespeed/speedtest/
|
|
GNU LGPLv3 License
|
|
*/
|
|
|
|
// data reported to main thread
|
|
let testState = -1; // -1=idle, 0=starting, 1=running, 4=finished, 5=aborted
|
|
let currentPing = 0;
|
|
let avgPing = 0;
|
|
let minPing = -1;
|
|
let maxPing = 0;
|
|
let jitter = 0;
|
|
let packetLoss = 0; // failed request percentage
|
|
let elapsed = 0;
|
|
let progress = 0;
|
|
|
|
let pingData = []; // all ping data points {t: elapsedMs, ping: ms, lost: bool}
|
|
let lastReportedIndex = 0; // for delta delivery
|
|
let totalSamples = 0;
|
|
let failedSamples = 0;
|
|
let pingSum = 0;
|
|
|
|
let settings = {
|
|
url_ping: "backend/empty.php",
|
|
url_ping_external: "", // external URL to ping (uses fetch no-cors, e.g. "https://www.google.com/generate_204")
|
|
duration: 60, // seconds
|
|
ping_interval: 200, // minimum ms between pings to limit sample rate
|
|
ping_allowPerformanceApi: true,
|
|
mpot: false
|
|
};
|
|
|
|
let xhr = null;
|
|
let startTime = 0;
|
|
let prevInstspd = 0;
|
|
let aborted = false;
|
|
|
|
function url_sep(url) {
|
|
return url.match(/\?/) ? "&" : "?";
|
|
}
|
|
|
|
this.addEventListener("message", function (e) {
|
|
const params = e.data.split(" ");
|
|
if (params[0] === "status") {
|
|
// return current state with delta ping data
|
|
const newData = pingData.slice(lastReportedIndex);
|
|
lastReportedIndex = pingData.length;
|
|
postMessage(
|
|
JSON.stringify({
|
|
testState: testState,
|
|
currentPing: currentPing,
|
|
avgPing: avgPing,
|
|
minPing: minPing,
|
|
maxPing: maxPing,
|
|
jitter: jitter,
|
|
packetLoss: packetLoss,
|
|
elapsed: elapsed,
|
|
duration: settings.duration,
|
|
progress: progress,
|
|
pingData: newData,
|
|
totalSamples: totalSamples,
|
|
failedSamples: failedSamples
|
|
})
|
|
);
|
|
}
|
|
if (params[0] === "start" && testState === -1) {
|
|
testState = 0;
|
|
try {
|
|
let s = {};
|
|
try {
|
|
const ss = e.data.substring(6);
|
|
if (ss) s = JSON.parse(ss);
|
|
} catch (e) {
|
|
console.warn("Error parsing settings JSON");
|
|
}
|
|
for (let key in s) {
|
|
if (typeof settings[key] !== "undefined") settings[key] = s[key];
|
|
}
|
|
} catch (e) {
|
|
console.warn("Error applying settings: " + e);
|
|
}
|
|
// start the stability test
|
|
aborted = false;
|
|
startTime = new Date().getTime();
|
|
testState = 1;
|
|
doPing();
|
|
}
|
|
if (params[0] === "abort") {
|
|
if (testState >= 4) return;
|
|
aborted = true;
|
|
testState = 5;
|
|
if (xhr) {
|
|
try {
|
|
xhr.abort();
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
});
|
|
|
|
function recordPing(instspd) {
|
|
// guard against 0ms pings
|
|
if (instspd < 1) instspd = prevInstspd;
|
|
if (instspd < 1) instspd = 1;
|
|
|
|
totalSamples++;
|
|
currentPing = instspd;
|
|
pingSum += instspd;
|
|
avgPing = parseFloat((pingSum / (totalSamples - failedSamples)).toFixed(2));
|
|
|
|
if (minPing === -1 || instspd < minPing) minPing = instspd;
|
|
if (instspd > maxPing) maxPing = instspd;
|
|
|
|
// jitter calculation (same weighted formula as speedtest_worker.js)
|
|
if (totalSamples > 1 && prevInstspd > 0) {
|
|
const instjitter = Math.abs(instspd - prevInstspd);
|
|
if (totalSamples === 2) {
|
|
jitter = instjitter;
|
|
} else {
|
|
jitter = instjitter > jitter ? jitter * 0.3 + instjitter * 0.7 : jitter * 0.8 + instjitter * 0.2;
|
|
}
|
|
}
|
|
prevInstspd = instspd;
|
|
|
|
// failed request percentage
|
|
packetLoss = totalSamples > 0 ? parseFloat(((failedSamples / totalSamples) * 100).toFixed(2)) : 0;
|
|
|
|
// record data point
|
|
const now = new Date().getTime();
|
|
elapsed = (now - startTime) / 1000;
|
|
pingData.push({ t: elapsed, ping: parseFloat(instspd.toFixed(2)), lost: false });
|
|
}
|
|
|
|
function recordLoss() {
|
|
const now = new Date().getTime();
|
|
totalSamples++;
|
|
failedSamples++;
|
|
packetLoss = parseFloat(((failedSamples / totalSamples) * 100).toFixed(2));
|
|
elapsed = (now - startTime) / 1000;
|
|
pingData.push({ t: elapsed, ping: 0, lost: true });
|
|
}
|
|
|
|
// pace pings to avoid excessive sample rates on low-latency links
|
|
function schedulePing(rtt) {
|
|
const delay = Math.max(0, settings.ping_interval - rtt);
|
|
if (delay > 0) {
|
|
setTimeout(doPing, delay);
|
|
} else {
|
|
doPing();
|
|
}
|
|
}
|
|
|
|
function doPing() {
|
|
if (aborted || testState >= 4) return;
|
|
|
|
// check if duration exceeded
|
|
const now = new Date().getTime();
|
|
elapsed = (now - startTime) / 1000;
|
|
progress = Math.min(1, elapsed / settings.duration);
|
|
if (elapsed >= settings.duration) {
|
|
testState = 4;
|
|
progress = 1;
|
|
return;
|
|
}
|
|
|
|
// external ping mode: use fetch with no-cors
|
|
if (settings.url_ping_external) {
|
|
doPingExternal();
|
|
return;
|
|
}
|
|
|
|
const prevT = new Date().getTime();
|
|
xhr = new XMLHttpRequest();
|
|
xhr.onload = function () {
|
|
if (aborted || testState >= 4) return;
|
|
const now = new Date().getTime();
|
|
let instspd = now - prevT;
|
|
|
|
if (settings.ping_allowPerformanceApi) {
|
|
try {
|
|
let p = performance.getEntries();
|
|
p = p[p.length - 1];
|
|
let d = p.responseStart - p.requestStart;
|
|
if (d <= 0) d = p.duration;
|
|
if (d > 0 && d < instspd) instspd = d;
|
|
} catch (e) {
|
|
// Performance API not available, use estimate
|
|
}
|
|
}
|
|
|
|
recordPing(instspd);
|
|
schedulePing(instspd);
|
|
};
|
|
xhr.onerror = function () {
|
|
if (aborted || testState >= 4) return;
|
|
recordLoss();
|
|
schedulePing(0);
|
|
};
|
|
xhr.ontimeout = xhr.onerror;
|
|
xhr.open(
|
|
"GET",
|
|
settings.url_ping + url_sep(settings.url_ping) + (settings.mpot ? "cors=true&" : "") + "r=" + Math.random(),
|
|
true
|
|
);
|
|
try {
|
|
xhr.timeout = 5000;
|
|
} catch (e) {}
|
|
xhr.send();
|
|
}
|
|
|
|
// ping an external host using fetch with no-cors (opaque response, but timing still works)
|
|
function doPingExternal() {
|
|
const prevT = new Date().getTime();
|
|
const url =
|
|
settings.url_ping_external + (settings.url_ping_external.indexOf("?") >= 0 ? "&" : "?") + "r=" + Math.random();
|
|
fetch(url, { mode: "no-cors", cache: "no-store" })
|
|
.then(function () {
|
|
if (aborted || testState >= 4) return;
|
|
const instspd = new Date().getTime() - prevT;
|
|
recordPing(instspd);
|
|
schedulePing(instspd);
|
|
})
|
|
.catch(function () {
|
|
if (aborted || testState >= 4) return;
|
|
recordLoss();
|
|
schedulePing(0);
|
|
});
|
|
}
|