Remove Blinking During Data Refresh
This commit is contained in:
@@ -293,6 +293,16 @@ function paint(canvasId, lines, opts = {}) {
|
||||
drawChart($(canvasId), lines, { ...opts, maxPoints: seriesMax() })
|
||||
}
|
||||
|
||||
let overviewPaintRaf = 0
|
||||
/** Coalesce Overview redraws into one frame to avoid spark blink. */
|
||||
function scheduleRedrawAll() {
|
||||
if (overviewPaintRaf) return
|
||||
overviewPaintRaf = requestAnimationFrame(() => {
|
||||
overviewPaintRaf = 0
|
||||
redrawAll()
|
||||
})
|
||||
}
|
||||
|
||||
function panelForChart(chart) {
|
||||
if (chart === 'system.cpu') return els.panelCpu
|
||||
if (chart === 'mem.available' || chart === 'system.ram') return els.panelRam
|
||||
@@ -739,7 +749,7 @@ function onSamples(samples, conn) {
|
||||
}
|
||||
// Metrics wall only ingests the active agent
|
||||
if (isActive) metricsDashboard.onSamples(samples || [])
|
||||
redrawAll()
|
||||
scheduleRedrawAll()
|
||||
}
|
||||
|
||||
function prependAnomaly(ev) {
|
||||
|
||||
+30
-25
@@ -53,17 +53,34 @@ export function padLeftFor(units, showYAxis = true) {
|
||||
return u.length > 6 ? 52 : 46
|
||||
}
|
||||
|
||||
export function drawMultiChart(canvas, lines, opts = {}) {
|
||||
if (!canvas) return
|
||||
/**
|
||||
* Size the backing store only when needed. Resetting canvas.width every paint
|
||||
* clears the buffer and causes a visible blink on live updates.
|
||||
* @param {HTMLCanvasElement} canvas
|
||||
* @returns {{ ctx: CanvasRenderingContext2D, w: number, h: number }|null}
|
||||
*/
|
||||
function prepareCanvas(canvas) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
if (!ctx) return null
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const w = canvas.clientWidth || 320
|
||||
const h = canvas.clientHeight || canvas.height || 140
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
const w = Math.max(1, Math.round(canvas.clientWidth || 320))
|
||||
const h = Math.max(1, Math.round(canvas.clientHeight || canvas.height || 140))
|
||||
const bw = Math.max(1, Math.round(w * dpr))
|
||||
const bh = Math.max(1, Math.round(h * dpr))
|
||||
if (canvas.width !== bw || canvas.height !== bh) {
|
||||
canvas.width = bw
|
||||
canvas.height = bh
|
||||
}
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
return { ctx, w, h }
|
||||
}
|
||||
|
||||
export function drawMultiChart(canvas, lines, opts = {}) {
|
||||
if (!canvas) return
|
||||
const preparedCanvas = prepareCanvas(canvas)
|
||||
if (!preparedCanvas) return
|
||||
const { ctx, w, h } = preparedCanvas
|
||||
|
||||
const mode = normalizeChartMode(opts.mode || (opts.stacked ? 'stacked' : 'line'))
|
||||
const maxPoints = opts.maxPoints || 90
|
||||
@@ -123,15 +140,9 @@ export function drawMultiChart(canvas, lines, opts = {}) {
|
||||
*/
|
||||
export function drawBarChart(canvas, lines, opts = {}) {
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const w = canvas.clientWidth || 320
|
||||
const h = canvas.clientHeight || canvas.height || 140
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
const preparedCanvas = prepareCanvas(canvas)
|
||||
if (!preparedCanvas) return
|
||||
const { ctx, w, h } = preparedCanvas
|
||||
|
||||
const maxPoints = opts.maxPoints || 90
|
||||
const units = opts.units ? String(opts.units) : ''
|
||||
@@ -227,15 +238,9 @@ export function drawBarChart(canvas, lines, opts = {}) {
|
||||
*/
|
||||
export function drawPieChart(canvas, lines, opts = {}) {
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
const w = canvas.clientWidth || 320
|
||||
const h = canvas.clientHeight || canvas.height || 140
|
||||
canvas.width = w * dpr
|
||||
canvas.height = h * dpr
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
const preparedCanvas = prepareCanvas(canvas)
|
||||
if (!preparedCanvas) return
|
||||
const { ctx, w, h } = preparedCanvas
|
||||
|
||||
const prepared = prepareSeries(lines, opts.maxPoints || 90)
|
||||
const slices = prepared
|
||||
|
||||
+32
-12
@@ -364,10 +364,13 @@ export function createMetricsDashboard(opts) {
|
||||
const gen = (state.fetchGen.get(id) || 0) + 1
|
||||
state.fetchGen.set(id, gen)
|
||||
|
||||
// Stale-while-revalidate: keep previous series painted; never blank the canvas.
|
||||
// Stale-while-revalidate: keep previous series painted; never blank or dim.
|
||||
if (hadData) {
|
||||
card.updating = true
|
||||
paintCard(id)
|
||||
// Status text only — no repaint (avoids blink before new data arrives)
|
||||
const article = state.cardEls.get(id)
|
||||
const statusEl = article?.querySelector('.metric-card-status')
|
||||
if (statusEl) statusEl.textContent = ''
|
||||
} else {
|
||||
card.status = 'loading'
|
||||
card.emptyReason = 'Waiting for first sample…'
|
||||
@@ -457,6 +460,18 @@ export function createMetricsDashboard(opts) {
|
||||
/**
|
||||
* @param {Array<{ chart: string, values: Record<string, number> }>} samples
|
||||
*/
|
||||
/** @type {Set<string>} */
|
||||
const pendingSamplePaint = new Set()
|
||||
let samplePaintRaf = 0
|
||||
|
||||
function flushSamplePaints() {
|
||||
samplePaintRaf = 0
|
||||
for (const id of pendingSamplePaint) paintCard(id)
|
||||
pendingSamplePaint.clear()
|
||||
updateHoverReadout()
|
||||
refreshSectionKpis()
|
||||
}
|
||||
|
||||
function onSamples(samples) {
|
||||
if (!state.playing || !state.built || state.endOffset > 0) return
|
||||
const max = maxPoints()
|
||||
@@ -472,12 +487,12 @@ export function createMetricsDashboard(opts) {
|
||||
if (!card.labels.includes(dim)) card.labels.push(dim)
|
||||
}
|
||||
card.status = 'ok'
|
||||
paintCard(s.chart)
|
||||
card.updating = false
|
||||
pendingSamplePaint.add(s.chart)
|
||||
any = true
|
||||
}
|
||||
if (any) {
|
||||
updateHoverReadout()
|
||||
refreshSectionKpis()
|
||||
if (any && !samplePaintRaf) {
|
||||
samplePaintRaf = requestAnimationFrame(flushSamplePaints)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,10 +522,10 @@ export function createMetricsDashboard(opts) {
|
||||
article.classList.toggle('related', state.relatedIds.has(id))
|
||||
const pinBtn = article.querySelector('.metric-pin-btn')
|
||||
if (pinBtn) pinBtn.textContent = state.pinned.has(id) ? '★' : '☆'
|
||||
article.classList.toggle('updating', Boolean(card?.updating))
|
||||
// Never dim the card during SWR — opacity/filter changes look like a blink
|
||||
article.classList.remove('updating')
|
||||
if (statusEl) {
|
||||
if (card?.updating) statusEl.textContent = 'updating'
|
||||
else if (card?.status === 'loading') statusEl.textContent = 'loading'
|
||||
if (card?.status === 'loading') statusEl.textContent = 'loading'
|
||||
else if (card?.status === 'error') statusEl.textContent = 'error'
|
||||
else if (card?.status === 'empty') statusEl.textContent = 'empty'
|
||||
else if (card?.status === 'ok' && card.source && card.source !== 'memory') {
|
||||
@@ -519,7 +534,8 @@ export function createMetricsDashboard(opts) {
|
||||
}
|
||||
if (!card || !canvas) return
|
||||
const height = cardHeightFor(id, card)
|
||||
canvas.style.height = `${height}px`
|
||||
const heightPx = `${height}px`
|
||||
if (canvas.style.height !== heightPx) canvas.style.height = heightPx
|
||||
const { dims, hidden } = sortedDims(card, id)
|
||||
const mode = normalizeChartMode(card.mode || defaultModeFromMeta(card.meta))
|
||||
card.mode = mode
|
||||
@@ -559,7 +575,7 @@ export function createMetricsDashboard(opts) {
|
||||
threshold: mode === 'pie' ? null : anomaly?.threshold ?? null,
|
||||
severity: anomaly?.severity || null,
|
||||
emptyMessage: emptyMsg,
|
||||
dimmed: Boolean(card.updating),
|
||||
dimmed: false,
|
||||
units,
|
||||
windowSeconds: mode === 'pie' ? null : state.afterSeconds,
|
||||
endOffset: state.endOffset,
|
||||
@@ -1314,8 +1330,12 @@ export function createMetricsDashboard(opts) {
|
||||
})
|
||||
}
|
||||
tops.sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
|
||||
const slice = tops.slice(0, 6)
|
||||
const sig = slice.map((r) => `${r.id}:${formatVal(r.value)}:${r.units}`).join('|')
|
||||
if (strip.dataset.sig === sig) continue
|
||||
strip.dataset.sig = sig
|
||||
strip.innerHTML = ''
|
||||
for (const row of tops.slice(0, 6)) {
|
||||
for (const row of slice) {
|
||||
const btn = document.createElement('button')
|
||||
btn.type = 'button'
|
||||
btn.className = 'section-kpi'
|
||||
|
||||
@@ -1435,14 +1435,6 @@ button.metrics-tf.thin-history:not(.active) {
|
||||
}
|
||||
}
|
||||
|
||||
.metric-card.updating {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.metric-card.updating canvas {
|
||||
filter: saturate(0.92);
|
||||
}
|
||||
|
||||
.metric-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
Reference in New Issue
Block a user