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