From ed602fdb5ff4d8467b621dea5892925c136b3981 Mon Sep 17 00:00:00 2001 From: sneak Date: Sat, 31 Jan 2026 13:58:18 -0800 Subject: [PATCH] Fix timing: use performance.now() instead of Resource Timing API Resource Timing entries are added asynchronously after fetch resolves, causing a race condition. The simple performance.now() around fetch gives accurate latency measurements without this issue. --- src/main.js | 56 +++++++++++------------------------------------------ 1 file changed, 11 insertions(+), 45 deletions(-) diff --git a/src/main.js b/src/main.js index 13a7d09..24209fb 100644 --- a/src/main.js +++ b/src/main.js @@ -32,71 +32,37 @@ const hostState = HOSTS.map(host => ({ status: 'pending', // 'online', 'offline', 'pending', 'error' })) -// Extract timing from Resource Timing API -function getTimingForUrl(url) { - const entries = performance.getEntriesByName(url, 'resource') - if (entries.length > 0) { - const entry = entries[entries.length - 1] - // Use responseEnd - requestStart for actual network time - // Fall back to duration if detailed timing not available (cross-origin) - let latency - if (entry.requestStart > 0 && entry.responseEnd > 0) { - latency = entry.responseEnd - entry.requestStart - } else { - // For opaque responses, duration includes queuing time but is still more accurate - latency = entry.duration - } - return Math.round(latency) - } - return null -} - -// Measure latency using HEAD request with Resource Timing API +// Measure latency using HEAD request async function measureLatency(url) { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT) // Add cache-busting parameter const targetUrl = new URL(url) - const cacheBuster = `${Date.now()}-${Math.random().toString(36).slice(2)}` - targetUrl.searchParams.set('_cb', cacheBuster) - const finalUrl = targetUrl.toString() + targetUrl.searchParams.set('_cb', Date.now().toString()) - let fetchError = null + const start = performance.now() try { - await fetch(finalUrl, { + await fetch(targetUrl.toString(), { method: 'HEAD', mode: 'no-cors', cache: 'no-store', signal: controller.signal, }) - } catch (err) { - fetchError = err - } - clearTimeout(timeoutId) - - // Always check timing - even 4xx/5xx responses have timing data - const latency = getTimingForUrl(finalUrl) - performance.clearResourceTimings() - - // If we got timing data, return it regardless of fetch error - if (latency !== null) { + const latency = Math.round(performance.now() - start) + clearTimeout(timeoutId) return { latency, error: null } - } + } catch (err) { + clearTimeout(timeoutId) - // No timing data - determine error type - if (fetchError?.name === 'AbortError') { - return { latency: null, error: 'timeout' } - } + if (err.name === 'AbortError') { + return { latency: null, error: 'timeout' } + } - if (fetchError) { return { latency: null, error: 'unreachable' } } - - // Fetch succeeded but no timing (shouldn't happen) - return { latency: null, error: null } } // Get latency color based on value