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:
parent
83d9d15b6c
commit
8be7002ad9
63
src/main.js
63
src/main.js
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user