mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-02-18 20:29:11 -06:00
234 lines
7.2 KiB
JavaScript
234 lines
7.2 KiB
JavaScript
/**
|
|
*
|
|
* This file is part of speed-cloudflare-cli (https://github.com/KNawm/speed-cloudflare-cli),
|
|
* which is released under the MIT License.
|
|
*
|
|
* This file has been modified to be used inside MySpeed.
|
|
*
|
|
* Copyright (c) 2020 Tomás Arias
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
const { performance } = require("perf_hooks");
|
|
const https = require("https");
|
|
const interfaces = require("../util/loadInterfaces");
|
|
const config = require("../controller/config");
|
|
|
|
function average(values) {
|
|
let total = 0;
|
|
|
|
for (let i = 0; i < values.length; i += 1) {
|
|
total += values[i];
|
|
}
|
|
|
|
return total / values.length;
|
|
}
|
|
|
|
function median(values) {
|
|
const half = Math.floor(values.length / 2);
|
|
|
|
values.sort((a, b) => a - b);
|
|
|
|
if (values.length % 2) return values[half];
|
|
|
|
return (values[half - 1] + values[half]) / 2;
|
|
}
|
|
|
|
function quartile(values, percentile) {
|
|
values.sort((a, b) => a - b);
|
|
const pos = (values.length - 1) * percentile;
|
|
const base = Math.floor(pos);
|
|
const rest = pos - base;
|
|
|
|
if (values[base + 1] !== undefined) {
|
|
return values[base] + rest * (values[base + 1] - values[base]);
|
|
}
|
|
|
|
return values[base];
|
|
}
|
|
|
|
function jitter(values) {
|
|
// Average distance between consecutive latency measurements...
|
|
let jitters = [];
|
|
|
|
for (let i = 0; i < values.length - 1; i += 1) {
|
|
jitters.push(Math.abs(values[i] - values[i+1]));
|
|
}
|
|
|
|
return average(jitters);
|
|
}
|
|
|
|
function request(localAddress, options, data = "") {
|
|
let started;
|
|
let dnsLookup;
|
|
let tcpHandshake;
|
|
let sslHandshake;
|
|
let ttfb;
|
|
let ended;
|
|
|
|
options.localAddress = localAddress;
|
|
options.family = localAddress.includes(":") ? 6 : 4;
|
|
options.agent = new https.Agent(options);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
started = performance.now();
|
|
const req = https.request(options, (res) => {
|
|
res.once("readable", () => {
|
|
ttfb = performance.now();
|
|
});
|
|
res.on("data", () => {});
|
|
res.on("end", () => {
|
|
ended = performance.now();
|
|
resolve([started, dnsLookup, tcpHandshake, sslHandshake, ttfb, ended, parseFloat(res.headers["server-timing"]?.slice(22))]);
|
|
});
|
|
});
|
|
|
|
req.on("socket", (socket) => {
|
|
socket.on("lookup", () => {
|
|
dnsLookup = performance.now();
|
|
});
|
|
socket.on("connect", () => {
|
|
tcpHandshake = performance.now();
|
|
});
|
|
socket.on("secureConnect", () => {
|
|
sslHandshake = performance.now();
|
|
});
|
|
});
|
|
|
|
req.on("error", (error) => {
|
|
reject(error.message);
|
|
});
|
|
|
|
req.write(data);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function download(ip, bytes) {
|
|
const options = {
|
|
hostname: "speed.cloudflare.com",
|
|
path: `/__down?bytes=${bytes}`,
|
|
method: "GET",
|
|
};
|
|
|
|
return request(ip, options);
|
|
}
|
|
|
|
function upload(ip, bytes) {
|
|
const data = "0".repeat(bytes);
|
|
const options = {
|
|
hostname: "speed.cloudflare.com",
|
|
path: "/__up",
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Length": Buffer.byteLength(data),
|
|
},
|
|
};
|
|
|
|
return request(ip, options, data);
|
|
}
|
|
|
|
function measureSpeed(bytes, duration) {
|
|
return (bytes * 8) / (duration / 1000) / 1e6;
|
|
}
|
|
|
|
async function measureLatency(ip) {
|
|
const measurements = [];
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
await download(ip, 1000).then(
|
|
(response) => {
|
|
// TTFB - Server processing time
|
|
measurements.push(response[4] - response[0] - response[6]);
|
|
},
|
|
(error) => {
|
|
console.log(`Error while pinging: ${error}`);
|
|
},
|
|
);
|
|
}
|
|
|
|
return [Math.min(...measurements), Math.max(...measurements), average(measurements), median(measurements), jitter(measurements)];
|
|
}
|
|
|
|
async function measureDownload(ip, bytes, iterations) {
|
|
const measurements = [];
|
|
|
|
for (let i = 0; i < iterations; i += 1) {
|
|
await download(ip, bytes).then(
|
|
(response) => {
|
|
const transferTime = response[5] - response[4];
|
|
measurements.push(measureSpeed(bytes, transferTime));
|
|
},
|
|
(error) => {
|
|
console.log(`Error while downloading: ${error}`);
|
|
},
|
|
);
|
|
}
|
|
|
|
return measurements;
|
|
}
|
|
|
|
async function measureUpload(ip, bytes, iterations) {
|
|
const measurements = [];
|
|
|
|
for (let i = 0; i < iterations; i += 1) {
|
|
await upload(ip, bytes).then(
|
|
(response) => {
|
|
const transferTime = response[6];
|
|
measurements.push(measureSpeed(bytes, transferTime));
|
|
},
|
|
(error) => {
|
|
console.log(`Error while uploading: ${error}`);
|
|
},
|
|
);
|
|
}
|
|
|
|
return measurements;
|
|
}
|
|
|
|
module.exports = async function speedTest() {
|
|
let result = {};
|
|
try {
|
|
const currentInterface = await config.getValue("interface");
|
|
const interfaceIp = interfaces.interfaces[currentInterface];
|
|
if (!interfaceIp) {
|
|
throw new Error("Invalid interface");
|
|
}
|
|
|
|
result["ping"] = Math.round((await measureLatency(interfaceIp))[3]);
|
|
|
|
const testDown1 = await measureDownload(interfaceIp, 101000, 1);
|
|
const testDown2 = await measureDownload(interfaceIp, 1001000, 8);
|
|
const testDown3 = await measureDownload(interfaceIp, 10001000, 6);
|
|
const testDown4 = await measureDownload(interfaceIp, 25001000, 4);
|
|
const testDown5 = await measureDownload(interfaceIp, 100001000, 1);
|
|
|
|
result["download"] = quartile([...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5], 0.9).toFixed(2);
|
|
|
|
const testUp1 = await measureUpload(interfaceIp, 11000, 10);
|
|
const testUp2 = await measureUpload(interfaceIp, 101000, 10);
|
|
const testUp3 = await measureUpload(interfaceIp, 1001000, 8);
|
|
result["upload"] = quartile([...testUp1, ...testUp2, ...testUp3], 0.9).toFixed(2);
|
|
} catch (error) {
|
|
console.error("Error while using cloudflare speedtest: " + error.message);
|
|
result = {error: error.message};
|
|
}
|
|
|
|
return result;
|
|
} |