fix: improve cursor telemetry accuracy under CPU load

- Replace setInterval with drift-compensating recursive setTimeout for
  cursor sampling. Under CPU load setInterval bunches or skips callbacks,
  creating irregular gaps in telemetry data.
- Add binary search + linear interpolation to export cursor lookup
  (modernFrameRenderer) matching the playback path. Was using O(n)
  nearest-neighbor which caused visible cursor jumping in exports.
- Apply same interpolation fix to extension API getCursorAt().
This commit is contained in:
webadderall committed 2026-04-18 19:03:36 +10:00
1 parent c4ada3505c
commit 2fc74b200f
3 files changed
+94 -28

No files matched your search

+43 -11
View File
@@ -2336,21 +2336,53 @@ export class FrameRenderer {
return null;
}
let closest = telemetry[0];
let minDist = Math.abs(telemetry[0].timeMs - timeMs);
for (let i = 1; i < telemetry.length; i++) {
const dist = Math.abs(telemetry[i].timeMs - timeMs);
if (dist < minDist) {
minDist = dist;
closest = telemetry[i];
}
if (telemetry[i].timeMs > timeMs) {
break;
// Clamp to first/last sample when out of range
if (timeMs <= telemetry[0].timeMs) {
const s = telemetry[0];
return mapCursorToCanvasNormalized(
{ cx: s.cx, cy: s.cy, interactionType: s.interactionType },
{
maskRect: this.layoutCache?.maskRect,
canvasWidth: this.config.width,
canvasHeight: this.config.height,
},
);
}
if (timeMs >= telemetry[telemetry.length - 1].timeMs) {
const s = telemetry[telemetry.length - 1];
return mapCursorToCanvasNormalized(
{ cx: s.cx, cy: s.cy, interactionType: s.interactionType },
{
maskRect: this.layoutCache?.maskRect,
canvasWidth: this.config.width,
canvasHeight: this.config.height,
},
);
}
// Binary search for surrounding samples
let lo = 0;
let hi = telemetry.length - 1;
while (lo < hi - 1) {
const mid = (lo + hi) >> 1;
if (telemetry[mid].timeMs <= timeMs) {
lo = mid;
} else {
hi = mid;
}
}
const a = telemetry[lo];
const b = telemetry[hi];
const span = b.timeMs - a.timeMs;
// Linear interpolation between samples
const t = span > 0 ? (timeMs - a.timeMs) / span : 0;
const cx = a.cx + (b.cx - a.cx) * t;
const cy = a.cy + (b.cy - a.cy) * t;
return mapCursorToCanvasNormalized(
{ cx: closest.cx, cy: closest.cy, interactionType: closest.interactionType },
{ cx, cy, interactionType: a.interactionType },
{
maskRect: this.layoutCache?.maskRect,
canvasWidth: this.config.width,
+23 -9
View File
@@ -854,17 +854,31 @@ export class ExtensionHost {
const t = host._cursorTelemetry;
if (!t || t.length === 0) return null;
let closest = t[0];
let minDist = Math.abs(t[0].timeMs - timeMs);
for (let i = 1; i < t.length; i++) {
const dist = Math.abs(t[i].timeMs - timeMs);
if (dist < minDist) {
minDist = dist;
closest = t[i];
if (timeMs <= t[0].timeMs) return { ...t[0] };
if (timeMs >= t[t.length - 1].timeMs) return { ...t[t.length - 1] };
let lo = 0;
let hi = t.length - 1;
while (lo < hi - 1) {
const mid = (lo + hi) >> 1;
if (t[mid].timeMs <= timeMs) {
lo = mid;
} else {
hi = mid;
}
if (t[i].timeMs > timeMs) break;
}
return { ...closest };
const a = t[lo];
const b = t[hi];
const span = b.timeMs - a.timeMs;
const frac = span > 0 ? (timeMs - a.timeMs) / span : 0;
return {
...a,
cx: a.cx + (b.cx - a.cx) * frac,
cy: a.cy + (b.cy - a.cy) * frac,
timeMs,
};
},
getSmoothedCursor() {