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 })) })