Fix latency measurement and chart direction

- Use Resource Timing API for accurate network latency instead of
  performance.now() around fetch (fixes ~600ms measurement error)
- Chart now shows latest sample at right edge, growing left
- Reduce request timeout from 5000ms to 1000ms
This commit is contained in:
Jeffrey Paul 2026-01-30 22:23:35 -08:00
parent 83d9d15b6c
commit 8be7002ad9

View File

@ -4,7 +4,7 @@ import './styles.css'
const UPDATE_INTERVAL = 2000 // ms
const HISTORY_DURATION = 300 // seconds
const MAX_HISTORY_POINTS = Math.ceil((HISTORY_DURATION * 1000) / UPDATE_INTERVAL) // 150 points
const REQUEST_TIMEOUT = 5000 // ms
const REQUEST_TIMEOUT = 1000 // ms
// Pause state
let isPaused = false
@ -32,39 +32,55 @@ const hostState = HOSTS.map(host => ({
status: 'pending', // 'online', 'offline', 'pending', 'error'
}))
// Measure latency using HEAD request
// Measure latency using HEAD request with Resource Timing API
async function measureLatency(url) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT)
const start = performance.now()
// 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()
try {
// Add cache-busting parameter
const targetUrl = new URL(url)
targetUrl.searchParams.set('_cb', Date.now().toString())
await fetch(targetUrl.toString(), {
await fetch(finalUrl, {
method: 'HEAD',
mode: 'no-cors', // Allow cross-origin without CORS headers
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal,
})
const end = performance.now()
clearTimeout(timeoutId)
return { latency: Math.round(end - start), error: null }
// Get accurate timing from Resource Timing API
const entries = performance.getEntriesByName(finalUrl, '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
}
// Clean up the entry
performance.clearResourceTimings()
return { latency: Math.round(latency), error: null }
}
// Fallback if no timing entry found
return { latency: null, error: null }
} catch (err) {
const end = performance.now()
clearTimeout(timeoutId)
performance.clearResourceTimings()
if (err.name === 'AbortError') {
return { latency: null, error: 'timeout' }
}
// For no-cors mode, network errors indicate unreachable
// But successful completion means we got through
return { latency: null, error: 'unreachable' }
}
}
@ -93,7 +109,7 @@ function getLatencyClass(latency, status) {
return 'text-red-500'
}
// Draw sparkline on canvas
// Draw sparkline on canvas - latest point at right edge, growing left
function drawSparkline(canvas, history, hostColor) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@ -134,7 +150,7 @@ function drawSparkline(canvas, history, hostColor) {
ctx.stroke()
}
// Draw the sparkline
// Draw the sparkline - right-aligned (latest at right edge)
ctx.strokeStyle = hostColor
ctx.lineWidth = 1.5
ctx.lineCap = 'round'
@ -142,13 +158,17 @@ function drawSparkline(canvas, history, hostColor) {
ctx.beginPath()
const pointWidth = (width - 2 * padding) / (MAX_HISTORY_POINTS - 1)
const historyLen = history.length
// Calculate x position: latest point at right edge
const getX = (i) => width - padding - (historyLen - 1 - i) * pointWidth
// Map all history points to x positions
let firstPoint = true
history.forEach((point, i) => {
if (point.latency === null) return
const x = padding + i * pointWidth
const x = getX(i)
const normalizedY = (point.latency - minLatency) / range
const y = height - padding - normalizedY * (height - 2 * padding)
@ -162,11 +182,11 @@ function drawSparkline(canvas, history, hostColor) {
ctx.stroke()
// Draw latest point indicator
// Draw latest point indicator (always at right edge)
const lastValidPoint = [...history].reverse().find(p => p.latency !== null)
if (lastValidPoint) {
const lastIndex = history.lastIndexOf(lastValidPoint)
const x = padding + lastIndex * pointWidth
const x = getX(lastIndex)
const normalizedY = (lastValidPoint.latency - minLatency) / range
const y = height - padding - normalizedY * (height - 2 * padding)
@ -182,7 +202,7 @@ function drawSparkline(canvas, history, hostColor) {
let errorStart = 0
history.forEach((point, i) => {
const x = padding + i * pointWidth
const x = getX(i)
if (point.latency === null && !inErrorRegion) {
inErrorRegion = true
@ -194,7 +214,8 @@ function drawSparkline(canvas, history, hostColor) {
})
if (inErrorRegion) {
ctx.fillRect(errorStart, 0, width - errorStart, height)
const lastX = getX(historyLen - 1)
ctx.fillRect(errorStart, 0, lastX - errorStart + padding, height)
}
}