fixed flickering
This commit is contained in:
@@ -1352,7 +1352,11 @@ function decimateAsync(cacheKey, t, v, threshold, gen) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cached || null; // stale entry, or nothing to draw yet
|
// Never hand out a stale decimation: drawing it (at its old timestamps) and
|
||||||
|
// then the fresh one a frame later is what makes the trace jump/shimmer on
|
||||||
|
// every push. Return null instead — the caller holds the previous render
|
||||||
|
// until the worker's fresh result lands (it flags the plot for redraw).
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evict stale decimation cache entries for a plot (call when zoom range changes).
|
// Evict stale decimation cache entries for a plot (call when zoom range changes).
|
||||||
@@ -2467,8 +2471,12 @@ function buildLiveData(p) {
|
|||||||
let dec;
|
let dec;
|
||||||
if (cached) {
|
if (cached) {
|
||||||
dec = cached;
|
dec = cached;
|
||||||
|
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
|
||||||
|
// Fresh decimation not ready yet — hold the previous render so the trace
|
||||||
|
// does not flicker between a stale decimation and the fresh one.
|
||||||
|
return p.uplot.data;
|
||||||
} else {
|
} else {
|
||||||
// Worker job submitted — sync fallback this frame so the plot isn't blank.
|
// First render: worker job submitted, nothing on screen yet — sync.
|
||||||
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
|
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
|
||||||
}
|
}
|
||||||
sharedT = dec.t;
|
sharedT = dec.t;
|
||||||
@@ -2541,7 +2549,14 @@ function buildTrigData(p) {
|
|||||||
// same-length snapshot slice for the same range, so it is tagged separately.
|
// same-length snapshot slice for the same range, so it is tagged separately.
|
||||||
const cacheKey = `${p.id}:${masterKey}:${t0.toFixed(6)}:${t1.toFixed(6)}:${masterRaw.t.length}:${usedFetched ? 'hi' : 'snap'}`;
|
const cacheKey = `${p.id}:${masterKey}:${t0.toFixed(6)}:${t1.toFixed(6)}:${masterRaw.t.length}:${usedFetched ? 'hi' : 'snap'}`;
|
||||||
const cachedDec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts);
|
const cachedDec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts);
|
||||||
const dec = cachedDec || decimate(masterRaw.t, masterRaw.v, targetPts);
|
let dec;
|
||||||
|
if (cachedDec) {
|
||||||
|
dec = cachedDec;
|
||||||
|
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
|
||||||
|
return p.uplot.data; // hold the previous render until the fresh decimation lands
|
||||||
|
} else {
|
||||||
|
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
|
||||||
|
}
|
||||||
// Convert absolute → relative seconds
|
// Convert absolute → relative seconds
|
||||||
const sharedT = new Float64Array(dec.t.length);
|
const sharedT = new Float64Array(dec.t.length);
|
||||||
for (let i = 0; i < dec.t.length; i++) sharedT[i] = dec.t[i] - trigT;
|
for (let i = 0; i < dec.t.length; i++) sharedT[i] = dec.t[i] - trigT;
|
||||||
@@ -2598,8 +2613,15 @@ function buildTrigFillData(p) {
|
|||||||
masterV = masterRaw.v;
|
masterV = masterRaw.v;
|
||||||
} else {
|
} else {
|
||||||
const cacheKey = `${p.id}:${masterKey}:trigfill`;
|
const cacheKey = `${p.id}:${masterKey}:trigfill`;
|
||||||
const dec = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts, _dataGen) ||
|
const decd = decimateAsync(cacheKey, masterRaw.t, masterRaw.v, targetPts, _dataGen);
|
||||||
decimate(masterRaw.t, masterRaw.v, targetPts);
|
let dec;
|
||||||
|
if (decd) {
|
||||||
|
dec = decd;
|
||||||
|
} else if (p.uplot && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length) {
|
||||||
|
return p.uplot.data; // hold until the fresh decimation is ready
|
||||||
|
} else {
|
||||||
|
dec = decimate(masterRaw.t, masterRaw.v, targetPts);
|
||||||
|
}
|
||||||
sharedAbsT = dec.t;
|
sharedAbsT = dec.t;
|
||||||
masterV = dec.v;
|
masterV = dec.v;
|
||||||
}
|
}
|
||||||
@@ -3884,6 +3906,10 @@ function deletePlot(plotId) {
|
|||||||
let _dbgTick = 0;
|
let _dbgTick = 0;
|
||||||
let _dataGen = 0; // incremented each time new data arrives
|
let _dataGen = 0; // incremented each time new data arrives
|
||||||
function renderDirtyPlots() {
|
function renderDirtyPlots() {
|
||||||
|
// Schedule the next frame FIRST: an exception below must never kill the
|
||||||
|
// animation loop, or every plot would freeze until a page refresh.
|
||||||
|
requestAnimationFrame(renderDirtyPlots);
|
||||||
|
try {
|
||||||
// Compute global "now" once — shared by all rolling-window plots this frame.
|
// Compute global "now" once — shared by all rolling-window plots this frame.
|
||||||
const globalPlotNow = getGlobalNow();
|
const globalPlotNow = getGlobalNow();
|
||||||
|
|
||||||
@@ -3953,7 +3979,7 @@ function renderDirtyPlots() {
|
|||||||
|
|
||||||
plots.forEach(p => {
|
plots.forEach(p => {
|
||||||
if (!p.needsRedraw || !p.uplot || p.traces.length === 0) return;
|
if (!p.needsRedraw || !p.uplot || p.traces.length === 0) return;
|
||||||
|
try {
|
||||||
const inTrigModeNow = inTrigWindow();
|
const inTrigModeNow = inTrigWindow();
|
||||||
// The x tick formatter and the cursor-sync group are baked into the uPlot
|
// The x tick formatter and the cursor-sync group are baked into the uPlot
|
||||||
// options at construction. A plot built in live mode therefore keeps
|
// options at construction. A plot built in live mode therefore keeps
|
||||||
@@ -3968,7 +3994,10 @@ function renderDirtyPlots() {
|
|||||||
if (isRolling && _dataGen === p.lastDataGen && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length > 0) {
|
if (isRolling && _dataGen === p.lastDataGen && p.uplot.data && p.uplot.data[0] && p.uplot.data[0].length > 0) {
|
||||||
p.needsRedraw = false;
|
p.needsRedraw = false;
|
||||||
zoomGuard = true;
|
zoomGuard = true;
|
||||||
p.uplot.setScale('x', { min: globalPlotNow - windowSec, max: globalPlotNow });
|
// Use the same per-plot anchor as the rebuild path, so the rolling window
|
||||||
|
// does not jump when the frame switches between the two.
|
||||||
|
const plotNow = computePlotNow(p);
|
||||||
|
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
|
||||||
zoomGuard = false;
|
zoomGuard = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -4005,12 +4034,25 @@ function renderDirtyPlots() {
|
|||||||
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
|
p.uplot.setScale('x', { min: plotNow - windowSec, max: plotNow });
|
||||||
}
|
}
|
||||||
zoomGuard = false;
|
zoomGuard = false;
|
||||||
|
p._errCount = 0;
|
||||||
|
} catch (e) {
|
||||||
|
// One bad plot must not kill the whole render loop. Track consecutive
|
||||||
|
// failures and self-heal by rebuilding the uPlot instance.
|
||||||
|
p._errCount = (p._errCount || 0) + 1;
|
||||||
|
console.error(`[render] plot ${p.id}:`, e);
|
||||||
|
p.needsRedraw = true; // retry next frame
|
||||||
|
if (p._errCount >= 30) {
|
||||||
|
p._errCount = 0;
|
||||||
|
try { createUPlot(p); } catch (e2) { console.error(`[render] rebuild plot ${p.id}:`, e2); }
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep per-plot cursor value readouts in sync with live data.
|
// Keep per-plot cursor value readouts in sync with live data.
|
||||||
if (cursors.mode === 'on') updatePlotCursorReadouts();
|
if (cursors.mode === 'on') updatePlotCursorReadouts();
|
||||||
|
} catch (e) {
|
||||||
requestAnimationFrame(renderDirtyPlots);
|
console.error('[render]', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user