smartnetwork/ts/smartnetwork.classes.cloudflarespeed.ts

261 lines
7.4 KiB
TypeScript
Raw Normal View History

2022-03-24 23:11:53 +01:00
import * as plugins from './smartnetwork.plugins.js';
import { getLogger } from './logging.js';
import { NetworkError, TimeoutError } from './errors.js';
2022-03-24 23:11:53 +01:00
import * as stats from './helpers/stats.js';
2021-04-28 13:41:55 +00:00
export class CloudflareSpeed {
constructor() {}
public async speedTest() {
const latency = await this.measureLatency();
2021-04-28 14:27:22 +00:00
2021-04-28 13:41:55 +00:00
const serverLocations = await this.fetchServerLocations();
const cgiData = await this.fetchCfCdnCgiTrace();
2021-04-28 14:32:56 +00:00
// lets test the download speed
2021-04-28 14:27:22 +00:00
const testDown1 = await this.measureDownload(101000, 10);
const testDown2 = await this.measureDownload(1001000, 8);
const testDown3 = await this.measureDownload(10001000, 6);
const testDown4 = await this.measureDownload(25001000, 4);
const testDown5 = await this.measureDownload(100001000, 1);
const downloadTests = [...testDown1, ...testDown2, ...testDown3, ...testDown4, ...testDown5];
2021-04-28 14:31:30 +00:00
const speedDownload = stats.quartile(downloadTests, 0.9).toFixed(2);
2021-04-28 14:27:22 +00:00
2021-04-28 14:32:56 +00:00
// lets test the upload speed
2021-04-28 14:27:22 +00:00
const testUp1 = await this.measureUpload(11000, 10);
const testUp2 = await this.measureUpload(101000, 10);
const testUp3 = await this.measureUpload(1001000, 8);
const uploadTests = [...testUp1, ...testUp2, ...testUp3];
2021-04-28 14:31:30 +00:00
const speedUpload = stats.quartile(uploadTests, 0.9).toFixed(2);
2021-04-28 14:27:22 +00:00
2021-04-28 13:41:55 +00:00
return {
...latency,
ip: cgiData.ip,
serverLocation: {
shortId: cgiData.colo,
name: serverLocations[cgiData.colo],
2021-04-28 14:27:22 +00:00
availableLocations: serverLocations,
},
downloadSpeed: speedDownload,
uploadSpeed: speedUpload,
2021-04-28 13:41:55 +00:00
};
}
public async measureLatency() {
const measurements: number[] = [];
for (let i = 0; i < 20; i += 1) {
await this.download(1000).then(
(response) => {
// TTFB - Server processing time
measurements.push(response[4] - response[0] - response[6]);
},
(error) => {
getLogger().error('Error measuring latency:', error);
},
2021-04-28 13:41:55 +00:00
);
}
return {
maxTime: Math.max(...measurements),
minTime: Math.min(...measurements),
averageTime: stats.average(measurements),
medianTime: stats.median(measurements),
jitter: stats.jitter(measurements),
2021-04-28 14:27:22 +00:00
};
}
public async measureDownload(bytes: number, iterations: number) {
2022-02-16 23:28:12 +01:00
const measurements: number[] = [];
2021-04-28 14:27:22 +00:00
for (let i = 0; i < iterations; i += 1) {
await this.download(bytes).then(
async (response) => {
const transferTime = response[5] - response[4];
measurements.push(await this.measureSpeed(bytes, transferTime));
},
(error) => {
getLogger().error('Error measuring download chunk:', error);
},
2021-04-28 14:27:22 +00:00
);
}
return measurements;
}
public async measureUpload(bytes: number, iterations: number) {
2022-02-16 23:28:12 +01:00
const measurements: number[] = [];
2021-04-28 14:27:22 +00:00
for (let i = 0; i < iterations; i += 1) {
await this.upload(bytes).then(
async (response) => {
const transferTime = response[6];
measurements.push(await this.measureSpeed(bytes, transferTime));
},
(error) => {
getLogger().error('Error measuring upload chunk:', error);
},
2021-04-28 14:27:22 +00:00
);
2021-04-28 13:41:55 +00:00
}
2021-04-28 14:27:22 +00:00
return measurements;
}
public async measureSpeed(bytes: number, duration: number) {
return (bytes * 8) / (duration / 1000) / 1e6;
2021-04-28 13:41:55 +00:00
}
2021-04-28 14:27:22 +00:00
public async fetchServerLocations(): Promise<{ [key: string]: string }> {
const res = JSON.parse(
await this.get('speed.cloudflare.com', '/locations'),
) as Array<{ iata: string; city: string }>;
return res.reduce(
(data: Record<string, string>, optionsArg) => {
data[optionsArg.iata] = optionsArg.city;
return data;
},
{} as Record<string, string>,
);
2021-04-28 13:41:55 +00:00
}
public async get(hostname: string, path: string): Promise<string> {
return new Promise((resolve, reject) => {
const req = plugins.https.request(
{
hostname,
path,
method: 'GET',
// disable connection pooling to avoid listener accumulation
agent: false,
2021-04-28 13:41:55 +00:00
},
(res) => {
2022-02-16 23:28:12 +01:00
const body: Array<Buffer> = [];
2021-04-28 13:41:55 +00:00
res.on('data', (chunk) => {
body.push(chunk);
});
res.on('end', () => {
try {
resolve(Buffer.concat(body).toString());
} catch (e) {
reject(e);
}
});
req.on('error', (err: Error & { code?: string }) => {
reject(new NetworkError(err.message, err.code));
});
},
2021-04-28 13:41:55 +00:00
);
req.end();
});
}
2022-02-16 23:28:12 +01:00
public async download(bytes: number) {
2021-04-28 13:41:55 +00:00
const options = {
hostname: 'speed.cloudflare.com',
path: `/__down?bytes=${bytes}`,
method: 'GET',
};
return this.request(options);
}
2021-04-28 14:27:22 +00:00
public async upload(bytes: number) {
const data = '0'.repeat(bytes);
const options = {
hostname: 'speed.cloudflare.com',
path: '/__up',
method: 'POST',
headers: {
'Content-Length': Buffer.byteLength(data),
},
};
return this.request(options, data);
}
2022-02-16 23:28:12 +01:00
public async request(options: plugins.https.RequestOptions, data = ''): Promise<number[]> {
let started: number;
let dnsLookup: number;
2022-02-17 00:18:23 +01:00
let tcpHandshake: number;
2022-02-16 23:28:12 +01:00
let sslHandshake: number;
let ttfb: number;
let ended: number;
2021-04-28 13:41:55 +00:00
return new Promise((resolve, reject) => {
started = plugins.perfHooks.performance.now();
// disable connection pooling to avoid listener accumulation across requests
const reqOptions = { ...options, agent: false };
const req = plugins.https.request(reqOptions, (res) => {
2021-04-28 13:41:55 +00:00
res.once('readable', () => {
ttfb = plugins.perfHooks.performance.now();
});
res.on('data', () => {});
res.on('end', () => {
ended = plugins.perfHooks.performance.now();
resolve([
started,
dnsLookup,
tcpHandshake,
sslHandshake,
ttfb,
ended,
parseFloat((res.headers['server-timing'] as string).slice(22)),
]);
2021-04-28 13:41:55 +00:00
});
});
// Listen for timing events once per new socket
req.once('socket', (socket) => {
socket.once('lookup', () => {
2021-04-28 13:41:55 +00:00
dnsLookup = plugins.perfHooks.performance.now();
});
socket.once('connect', () => {
2021-04-28 13:41:55 +00:00
tcpHandshake = plugins.perfHooks.performance.now();
});
socket.once('secureConnect', () => {
2021-04-28 13:41:55 +00:00
sslHandshake = plugins.perfHooks.performance.now();
});
});
req.on('error', (error) => {
reject(error);
});
req.write(data);
req.end();
});
}
public async fetchCfCdnCgiTrace(): Promise<{
2021-04-28 14:27:22 +00:00
fl: string;
h: string;
ip: string;
ts: string;
visit_scheme: string;
uag: string;
colo: string;
http: string;
loc: string;
tls: string;
sni: string;
warp: string;
gateway: string;
2021-04-28 13:41:55 +00:00
}> {
2022-02-16 23:28:12 +01:00
const parseCfCdnCgiTrace = (text: string) =>
2021-04-28 13:41:55 +00:00
text
.split('\n')
.map((i) => {
const parts = i.split('=');
return [parts[0], parts[1]];
2021-04-28 13:41:55 +00:00
})
.reduce((data: Record<string, string>, [k, v]) => {
2021-04-28 13:41:55 +00:00
if (v === undefined) return data;
data[k] = v;
return data;
}, {} as Record<string, string>);
2021-04-28 13:41:55 +00:00
return this.get('speed.cloudflare.com', '/cdn-cgi/trace').then(parseCfCdnCgiTrace);
}
}