102 lines
5.5 KiB
JavaScript
102 lines
5.5 KiB
JavaScript
const fs = require('node:fs')
|
|||
|
|
const path = require('node:path')
|
||
|
|
const { execFile } = require('node:child_process')
|
||
|
|
const { promisify } = require('node:util')
|
||
|
|
|
||
|
|
const execute = promisify(execFile)
|
||
|
|
const directory = path.resolve(__dirname, '../../acceptance')
|
||
|
|
const startedAt = new Date().toISOString()
|
||
|
|
const reportPath = path.join(directory, `public-latency-${startedAt.replace(/[:.]/g, '-')}.json`)
|
||
|
|
const report = { started_at: startedAt, samples: [], summaries: [] }
|
||
|
|
const headerNames = ['cache-control', 'cf-cache-status', 'cf-ray', 'age', 'etag', 'last-modified', 'content-type', 'content-encoding', 'content-length', 'vary', 'server']
|
||
|
|
const measurementFormat = '\n__BEGIN__%{json}__HEADERS__%{header_json}__END__\n'
|
||
|
|
|
||
|
|
function decode(output, label, url) {
|
||
|
|
return [...output.matchAll(/__BEGIN__([\s\S]*?)__HEADERS__([\s\S]*?)__END__/g)].map((match, index) => {
|
||
|
|
const measurement = JSON.parse(match[1])
|
||
|
|
const headers = JSON.parse(match[2])
|
||
|
|
const ms = (value) => Math.round(value * 1000000) / 1000
|
||
|
|
return {
|
||
|
|
label, url, connection_sequence: index + 1,
|
||
|
|
status: measurement.http_code, exit_code: measurement.exitcode,
|
||
|
|
http_version: measurement.http_version, new_connections: measurement.num_connects,
|
||
|
|
remote_ip: measurement.remote_ip, downloaded_bytes: measurement.size_download,
|
||
|
|
dns_ms: ms(measurement.time_namelookup),
|
||
|
|
connect_ms: ms(measurement.time_connect), tls_ms: ms(measurement.time_appconnect),
|
||
|
|
ttfb_ms: ms(measurement.time_starttransfer), total_ms: ms(measurement.time_total),
|
||
|
|
wait_after_request_ready_ms: ms(measurement.time_starttransfer - measurement.time_pretransfer),
|
||
|
|
headers: Object.fromEntries(headerNames.filter(name => headers[name]).map(name => [name, headers[name]])),
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
async function measure(label, url, family, repetitions = 1) {
|
||
|
|
const args = ['--silent', '--show-error', '--compressed', '--connect-timeout', '8', '--max-time', '15', '--write-out', measurementFormat]
|
||
|
|
if (family) args.push(family)
|
||
|
|
for (let index = 0; index < repetitions; index++) args.push('--output', '/dev/null', url)
|
||
|
|
let result
|
||
|
|
try { result = await execute('curl', args, { maxBuffer: 1024 * 1024 }) }
|
||
|
|
catch (error) { result = error }
|
||
|
|
const samples = decode(result.stdout || '', label, url)
|
||
|
|
if (!samples.length) samples.push({ label, url, failed: true, exit_code: result.code || 1 })
|
||
|
|
report.samples.push(...samples)
|
||
|
|
console.log(JSON.stringify({ label, samples: samples.map(sample => ({ status: sample.status, ttfb_ms: sample.ttfb_ms, total_ms: sample.total_ms, exit_code: sample.exit_code })) }))
|
||
|
|
}
|
||
|
|
|
||
|
|
async function tunnelMetrics() {
|
||
|
|
try {
|
||
|
|
const response = await fetch('http://127.0.0.1:20241/metrics', { signal: AbortSignal.timeout(3000) })
|
||
|
|
const lines = (await response.text()).split('\n')
|
||
|
|
return lines.filter(line => /^cloudflared_tunnel_(?:ha_connections|request_errors|total_requests|server_locations)/.test(line))
|
||
|
|
} catch { return ['metrics_unavailable'] }
|
||
|
|
}
|
||
|
|
|
||
|
|
function summarize() {
|
||
|
|
for (const label of new Set(report.samples.map(sample => sample.label))) {
|
||
|
|
const samples = report.samples.filter(sample => sample.label === label && sample.exit_code === 0)
|
||
|
|
const median = key => {
|
||
|
|
const values = samples.map(sample => sample[key]).sort((a, b) => a - b)
|
||
|
|
const middle = Math.floor(values.length / 2)
|
||
|
|
return values.length ? (values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2) : null
|
||
|
|
}
|
||
|
|
report.summaries.push({ label, successful_samples: samples.length,
|
||
|
|
dns_median_ms: median('dns_ms'), connect_median_ms: median('connect_ms'), tls_median_ms: median('tls_ms'),
|
||
|
|
ttfb_median_ms: median('ttfb_ms'), total_median_ms: median('total_ms'), wait_median_ms: median('wait_after_request_ready_ms'),
|
||
|
|
cache_states: [...new Set(samples.flatMap(sample => sample.headers?.['cf-cache-status'] || []))],
|
||
|
|
edge_locations: [...new Set(samples.flatMap(sample => sample.headers?.['cf-ray']?.map(ray => ray.split('-').at(-1)) || []))],
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function main() {
|
||
|
|
report.tunnel_before = await tunnelMetrics()
|
||
|
|
const targets = [
|
||
|
|
['html', '/login'],
|
||
|
|
['small_static', '/assets/favicon-bEsoBimf.ico'],
|
||
|
|
['api_guard', '/api/v1/auth/me'],
|
||
|
|
]
|
||
|
|
for (let round = 0; round < 3; round++) {
|
||
|
|
for (const [name, resource] of targets) {
|
||
|
|
await measure(`origin_${name}`, `http://127.0.0.1:18088${resource}`)
|
||
|
|
await measure(`public_ipv4_${name}`, `https://agent.pducv.cn${resource}`, '--ipv4')
|
||
|
|
await measure(`public_ipv6_${name}`, `https://agent.pducv.cn${resource}`, '--ipv6')
|
||
|
|
}
|
||
|
|
await measure('backend_health', 'http://127.0.0.1:18080/health')
|
||
|
|
}
|
||
|
|
for (const [label, family] of [['ipv4', '--ipv4'], ['ipv6', '--ipv6']]) {
|
||
|
|
await measure(`public_${label}_warm_html`, 'https://agent.pducv.cn/login', family, 3)
|
||
|
|
await measure(`public_${label}_config`, 'https://agent.pducv.cn/config.js', family)
|
||
|
|
await measure(`public_${label}_icons`, 'https://agent.pducv.cn/tdesign-icons/0.4.1/fonts/index.js', family)
|
||
|
|
}
|
||
|
|
await measure('origin_config', 'http://127.0.0.1:18088/config.js')
|
||
|
|
await measure('origin_icons', 'http://127.0.0.1:18088/tdesign-icons/0.4.1/fonts/index.js')
|
||
|
|
report.tunnel_after = await tunnelMetrics()
|
||
|
|
}
|
||
|
|
|
||
|
|
main().catch(error => { report.error = error.name; process.exitCode = 1 }).finally(() => {
|
||
|
|
report.completed_at = new Date().toISOString()
|
||
|
|
summarize()
|
||
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + '\n', { flag: 'wx' })
|
||
|
|
console.log(JSON.stringify({ report: reportPath, summaries: report.summaries, error: report.error }))
|
||
|
|
})
|