fix(tech-diag): warm-up fetch + no-redirect host so ping matches reality
Techs reported cloudflare.com showing 300+ms on the diagnostic page while OS-level ICMP ping returned 5ms. The gap is entirely protocol overhead: - fetch() ≠ ICMP. Every call pays DNS + TCP + TLS + HTTP on top of the real RTT, which is easily 150–300ms cold on mobile LTE when the radio has to wake the RRC connection. - Bare cloudflare.com redirects 301 → www.cloudflare.com, forcing a second DNS + TCP + TLS handshake for every "ping" and doubling the measured latency. - TechDiagnosticPage.vue was also labeling the full 10MB download time as "Latence", so the number on the speed-test card was never a latency measurement at all. Fixes, applied to both surfaces (Ops /j/diagnostic + Field /diagnostic): - Swap cloudflare.com → 1.1.1.1/cdn-cgi/trace. 88-byte response, no redirect, no keepalive games — canonical "internet is up" endpoint. - Warm-up fetch before every measurement. First call absorbs DNS + TCP + TLS + LTE wake; second call reports steady-state RTT. This applies to checkHosts() (ops) and resolveHost() (field composable). - Split runSpeed() into separate ping + throughput measurements. Ping hits speed.cloudflare.com/cdn-cgi/trace (88 bytes on a warm connection); throughput hits /__down on the same origin so the TLS session is reused. Deployed to production; smoke-verified: - ops bundle TechDiagnosticPage.b925e02c.js contains '1.1.1.1/cdn-cgi/trace' - field bundle DiagnosticPage.38a45f65.js contains the same - zero bare 'cloudflare.com' hostname in either hosts array Files: - apps/ops/src/modules/tech/pages/TechDiagnosticPage.vue - apps/field/src/composables/useSpeedTest.js - apps/field/src/pages/DiagnosticPage.vue Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
30a867a326
commit
07365d3b71
|
|
@ -55,11 +55,21 @@ export function useSpeedTest () {
|
|||
}
|
||||
|
||||
// HTTP resolve — check if a host is reachable via HTTP
|
||||
//
|
||||
// Why two fetches: browser fetch() includes DNS + TCP + TLS + HTTP on top
|
||||
// of real RTT. First call pays the cold-connection tax (easily 200–400ms
|
||||
// on mobile LTE when the radio is idle); second call on the now-warm
|
||||
// connection reports something close to actual RTT. Before this change
|
||||
// techs saw "cloudflare.com a répondu en 350ms" on what was actually a
|
||||
// 5ms link and opened false-positive tickets.
|
||||
async function resolveHost (host) {
|
||||
resolveResult.value = null
|
||||
const url = host.startsWith('http') ? host : 'https://' + host
|
||||
|
||||
try {
|
||||
// Warm-up — result discarded. Pays DNS + TCP + TLS + LTE wake-up.
|
||||
await fetch(url, { mode: 'no-cors', cache: 'no-store' })
|
||||
// Steady-state measurement on the warm connection.
|
||||
const t0 = performance.now()
|
||||
const res = await fetch(url, { mode: 'no-cors', cache: 'no-store' })
|
||||
const t1 = performance.now()
|
||||
|
|
|
|||
|
|
@ -91,7 +91,10 @@ const resolveResults = ref([])
|
|||
const batchRunning = ref(false)
|
||||
const batchResults = ref([])
|
||||
|
||||
const quickHosts = ['google.ca', 'erp.gigafibre.ca', 'cloudflare.com', '8.8.8.8']
|
||||
// 1.1.1.1/cdn-cgi/trace instead of bare cloudflare.com — the bare host
|
||||
// redirects 301 → www.cloudflare.com, doubling the handshake cost. The
|
||||
// trace endpoint is 88 bytes, no redirect, canonical "internet is up" probe.
|
||||
const quickHosts = ['google.ca', 'erp.gigafibre.ca', '1.1.1.1/cdn-cgi/trace', '8.8.8.8']
|
||||
const batchHosts = [
|
||||
'erp.gigafibre.ca',
|
||||
'dispatch.gigafibre.ca',
|
||||
|
|
|
|||
|
|
@ -40,32 +40,60 @@ const speed = ref(null)
|
|||
const latency = ref(null)
|
||||
const checking = ref(false)
|
||||
|
||||
// ─── Hosts to check ─────────────────────────────────────────────────────────
|
||||
// We deliberately use 1.1.1.1 (not cloudflare.com) because the bare
|
||||
// cloudflare.com redirects 301 → www.cloudflare.com, forcing a second DNS +
|
||||
// TCP + TLS handshake on every "ping" and making the browser report
|
||||
// 300–500ms on a 5ms link. 1.1.1.1/cdn-cgi/trace is a single no-redirect
|
||||
// 88-byte response — the canonical "is the internet up" endpoint.
|
||||
const hosts = ref([
|
||||
{ host: 'google.ca', status: null, ms: null },
|
||||
{ host: 'erp.gigafibre.ca', status: null, ms: null },
|
||||
{ host: 'cloudflare.com', status: null, ms: null },
|
||||
{ host: '1.1.1.1/cdn-cgi/trace', status: null, ms: null },
|
||||
])
|
||||
|
||||
// ─── Speed test ─────────────────────────────────────────────────────────────
|
||||
// Browser fetch() ≠ ICMP ping. A cold HTTPS request pays DNS + TCP + TLS +
|
||||
// HTTP on top of the real RTT, which on mobile LTE is ~150–300ms before
|
||||
// the first byte moves. We split this into two separate measurements:
|
||||
// 1. Ping: warm-up + measured call against /cdn-cgi/trace (88 bytes),
|
||||
// so the second call runs on a warm connection and reports close to
|
||||
// real RTT.
|
||||
// 2. Throughput: 10MB download on the SAME warm connection (same origin
|
||||
// → connection reuse, no second handshake tax).
|
||||
async function runSpeed () {
|
||||
testing.value = true; speed.value = null; latency.value = null
|
||||
try {
|
||||
const url = 'https://speed.cloudflare.com/__down?bytes=10000000'
|
||||
const pingUrl = 'https://speed.cloudflare.com/cdn-cgi/trace'
|
||||
// Warm-up — pays DNS + TCP + TLS so the next call is steady-state.
|
||||
await fetch(pingUrl, { mode: 'no-cors', cache: 'no-store' })
|
||||
// Measured ping on a warm connection.
|
||||
const p0 = performance.now()
|
||||
await fetch(pingUrl, { mode: 'no-cors', cache: 'no-store' })
|
||||
latency.value = Math.round(performance.now() - p0)
|
||||
|
||||
// Throughput — 10MB download, same origin so TLS session is reused.
|
||||
const dlUrl = 'https://speed.cloudflare.com/__down?bytes=10000000'
|
||||
const t0 = performance.now()
|
||||
const res = await fetch(url, { mode: 'no-cors' })
|
||||
const t1 = performance.now()
|
||||
const ms = t1 - t0
|
||||
latency.value = Math.round(ms)
|
||||
// Approximate: 10MB / time = speed
|
||||
speed.value = Math.round((10 * 8) / (ms / 1000))
|
||||
await fetch(dlUrl, { mode: 'no-cors' })
|
||||
const dlMs = performance.now() - t0
|
||||
speed.value = Math.round((10 * 8) / (dlMs / 1000))
|
||||
} catch {
|
||||
speed.value = 0
|
||||
} finally { testing.value = false }
|
||||
}
|
||||
|
||||
// Warm-up + measure pattern so "cloudflare.com a répondu en 300ms" stops
|
||||
// showing up for what is actually a 5ms link. First call absorbs the
|
||||
// cold-connection penalty (DNS + TCP + TLS + LTE RRC wake); second call
|
||||
// reports real RTT + HTTP round trip.
|
||||
async function checkHosts () {
|
||||
checking.value = true
|
||||
for (const h of hosts.value) {
|
||||
try {
|
||||
// Warm-up — result discarded.
|
||||
await fetch('https://' + h.host, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
||||
// Measurement.
|
||||
const t0 = performance.now()
|
||||
await fetch('https://' + h.host, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
||||
h.ms = Math.round(performance.now() - t0)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user