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
|
// 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) {
|
async function resolveHost (host) {
|
||||||
resolveResult.value = null
|
resolveResult.value = null
|
||||||
const url = host.startsWith('http') ? host : 'https://' + host
|
const url = host.startsWith('http') ? host : 'https://' + host
|
||||||
|
|
||||||
try {
|
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 t0 = performance.now()
|
||||||
const res = await fetch(url, { mode: 'no-cors', cache: 'no-store' })
|
const res = await fetch(url, { mode: 'no-cors', cache: 'no-store' })
|
||||||
const t1 = performance.now()
|
const t1 = performance.now()
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,10 @@ const resolveResults = ref([])
|
||||||
const batchRunning = ref(false)
|
const batchRunning = ref(false)
|
||||||
const batchResults = ref([])
|
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 = [
|
const batchHosts = [
|
||||||
'erp.gigafibre.ca',
|
'erp.gigafibre.ca',
|
||||||
'dispatch.gigafibre.ca',
|
'dispatch.gigafibre.ca',
|
||||||
|
|
|
||||||
|
|
@ -40,32 +40,60 @@ const speed = ref(null)
|
||||||
const latency = ref(null)
|
const latency = ref(null)
|
||||||
const checking = ref(false)
|
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([
|
const hosts = ref([
|
||||||
{ host: 'google.ca', status: null, ms: null },
|
{ host: 'google.ca', status: null, ms: null },
|
||||||
{ host: 'erp.gigafibre.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 () {
|
async function runSpeed () {
|
||||||
testing.value = true; speed.value = null; latency.value = null
|
testing.value = true; speed.value = null; latency.value = null
|
||||||
try {
|
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 t0 = performance.now()
|
||||||
const res = await fetch(url, { mode: 'no-cors' })
|
await fetch(dlUrl, { mode: 'no-cors' })
|
||||||
const t1 = performance.now()
|
const dlMs = performance.now() - t0
|
||||||
const ms = t1 - t0
|
speed.value = Math.round((10 * 8) / (dlMs / 1000))
|
||||||
latency.value = Math.round(ms)
|
|
||||||
// Approximate: 10MB / time = speed
|
|
||||||
speed.value = Math.round((10 * 8) / (ms / 1000))
|
|
||||||
} catch {
|
} catch {
|
||||||
speed.value = 0
|
speed.value = 0
|
||||||
} finally { testing.value = false }
|
} 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 () {
|
async function checkHosts () {
|
||||||
checking.value = true
|
checking.value = true
|
||||||
for (const h of hosts.value) {
|
for (const h of hosts.value) {
|
||||||
try {
|
try {
|
||||||
|
// Warm-up — result discarded.
|
||||||
|
await fetch('https://' + h.host, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
||||||
|
// Measurement.
|
||||||
const t0 = performance.now()
|
const t0 = performance.now()
|
||||||
await fetch('https://' + h.host, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
await fetch('https://' + h.host, { mode: 'no-cors', signal: AbortSignal.timeout(5000) })
|
||||||
h.ms = Math.round(performance.now() - t0)
|
h.ms = Math.round(performance.now() - t0)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user