performance fixes

This commit is contained in:
Chris Amow 2026-08-17 00:50:20 -05:00
parent fc09da7053
commit 90f8a1b567
2 changed files with 89 additions and 45 deletions

View file

@ -1112,6 +1112,17 @@ made `.price` undefined and axis/price drag stayed dead. Use
the scale is still auto; a mostly-vertical mouse drag now sets the scale is still auto; a mostly-vertical mouse drag now sets
`manualPriceRange` the same way two-finger pan does. `manualPriceRange` the same way two-finger pan does.
### 2026-08-16 — frontend CPU was overlay work on every mouse pixel
`subscribeCrosshairMove` rebuilt context-label DOM every move; those labels
do not depend on the cursor. `visibleLogicalRangeChange` and every tick
redrew handles, bridges, comments and labels without coalescing. Three
`window` pointermove listeners ran while idle.
Context labels now update only with the view. All overlay redraws share
one rAF and skip if the logical/price range did not change. Pointermoves
attach only for an armed tool or an in-progress drag.
### 2026-08-16 — the rest of the same session, none of it subtle ### 2026-08-16 — the rest of the same session, none of it subtle
Written because they were skipped the first time and then asked for. Written because they were skipped the first time and then asked for.

View file

@ -66,7 +66,10 @@ class ConfluenceChart {
this.commentNodes = new Map(); this.commentNodes = new Map();
this.commentLayer = null; this.commentLayer = null;
this.contextLabelLayer = null; this.contextLabelLayer = null;
this.contextLabelFrame = null; this.overlayFrame = null;
this.overlayForce = false;
this.overlayRangeKey = '';
this.pointerAttached = { tool: false, anchor: false, line: false };
this.onCommentToggle = null; this.onCommentToggle = null;
this.onCommentMove = null; this.onCommentMove = null;
this.onCommentSelect = null; this.onCommentSelect = null;
@ -254,9 +257,7 @@ class ConfluenceChart {
this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight }); this.chart.applyOptions({ width: el.clientWidth, height: el.clientHeight });
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.observePlotCanvas(); this.observePlotCanvas();
this.renderAnchorHandles(); this.scheduleOverlays(true);
this.renderComments();
this.renderContextLabels();
}); });
}); });
this.resizeObserver.observe(el); this.resizeObserver.observe(el);
@ -397,16 +398,10 @@ class ConfluenceChart {
this.toolMoveListener = event => this.moveToolGesture(event); this.toolMoveListener = event => this.moveToolGesture(event);
this.toolUpListener = event => this.finishToolGesture(event); this.toolUpListener = event => this.finishToolGesture(event);
el.addEventListener('pointerdown', this.toolDownListener); el.addEventListener('pointerdown', this.toolDownListener);
window.addEventListener('pointermove', this.toolMoveListener);
window.addEventListener('pointerup', this.toolUpListener);
this.anchorMoveListener = event => this.moveAnchor(event); this.anchorMoveListener = event => this.moveAnchor(event);
this.anchorUpListener = event => this.finishAnchorDrag(event); this.anchorUpListener = event => this.finishAnchorDrag(event);
window.addEventListener('pointermove', this.anchorMoveListener);
window.addEventListener('pointerup', this.anchorUpListener);
this.lineMoveListener = event => this.moveLine(event); this.lineMoveListener = event => this.moveLine(event);
this.lineUpListener = event => this.finishLineDrag(event); this.lineUpListener = event => this.finishLineDrag(event);
window.addEventListener('pointermove', this.lineMoveListener);
window.addEventListener('pointerup', this.lineUpListener);
this.contextListener = event => this.showContextMenu(event); this.contextListener = event => this.showContextMenu(event);
el.addEventListener('contextmenu', this.contextListener); el.addEventListener('contextmenu', this.contextListener);
this.clickListener = event => { this.clickListener = event => {
@ -423,7 +418,6 @@ class ConfluenceChart {
}; };
el.addEventListener('click', this.clickListener); el.addEventListener('click', this.clickListener);
this.chart.subscribeCrosshairMove(param => { this.chart.subscribeCrosshairMove(param => {
this.scheduleContextLabels();
if (!param.point || !param.time) { if (!param.point || !param.time) {
this.tooltip.hidden = true; this.tooltip.hidden = true;
this.hideOhlc(); this.hideOhlc();
@ -434,11 +428,7 @@ class ConfluenceChart {
}); });
requestAnimationFrame(() => this.observePlotCanvas()); requestAnimationFrame(() => this.observePlotCanvas());
this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => { this.chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
this.renderAnchorHandles(); this.scheduleOverlays();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
}); });
this.installPinchZoom(); this.installPinchZoom();
} }
@ -469,12 +459,7 @@ class ConfluenceChart {
}; };
this.chart.timeScale().setVisibleRange(this.pendingView); this.chart.timeScale().setVisibleRange(this.pendingView);
} }
requestAnimationFrame(() => this.renderAnchorHandles()); this.scheduleOverlays(true);
// The bar grid just changed underneath every pinned comment.
this.renderComments();
this.renderContextLabels();
this.renderLineBridges();
this.renderProjectionDiagnostic();
} }
setTrendlineGeometry(geometry) { setTrendlineGeometry(geometry) {
@ -591,10 +576,7 @@ class ConfluenceChart {
if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar; if (this.bars.length && this.bars[this.bars.length - 1].t === bar.t) this.bars[this.bars.length - 1] = bar;
else this.bars.push(bar); else this.bars.push(bar);
if (advanced) this.extendManualLineSeries(last, bar); if (advanced) this.extendManualLineSeries(last, bar);
this.renderAnchorHandles(); this.scheduleOverlays(true);
if (advanced || this.lineBridgeLayer?.childElementCount) this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
} }
extendManualLineSeries(previous, bar) { extendManualLineSeries(previous, bar) {
@ -623,7 +605,7 @@ class ConfluenceChart {
setComments(comments) { setComments(comments) {
this.comments = comments; this.comments = comments;
this.renderComments(); this.scheduleOverlays(true);
} }
setCurrentPriceAnimation(enabled) { setCurrentPriceAnimation(enabled) {
@ -804,9 +786,7 @@ class ConfluenceChart {
this.candles.applyOptions({ autoscaleInfoProvider: this.candleAutoscaleProvider }); this.candles.applyOptions({ autoscaleInfoProvider: this.candleAutoscaleProvider });
} }
} }
this.renderAnchorHandles(); this.scheduleOverlays(true);
this.renderComments();
this.renderContextLabels();
this.updateCurrentPrice(this.lastCurrentPrice); this.updateCurrentPrice(this.lastCurrentPrice);
}; };
this.touchEndListener = event => { this.touchEndListener = event => {
@ -1174,10 +1154,7 @@ class ConfluenceChart {
this.chart.timeScale().setVisibleRange(this.pendingView); this.chart.timeScale().setVisibleRange(this.pendingView);
this.pendingView = null; this.pendingView = null;
} }
this.renderAnchorHandles(); this.scheduleOverlays(true);
this.renderLineBridges();
this.renderContextLabels();
this.renderProjectionDiagnostic();
} }
renderContextLabels() { renderContextLabels() {
@ -1215,14 +1192,61 @@ class ConfluenceChart {
})); }));
} }
scheduleContextLabels() { overlayKey() {
if (this.contextLabelFrame != null) return; const range = this.chart?.timeScale().getVisibleLogicalRange();
this.contextLabelFrame = requestAnimationFrame(() => { if (!range) return '';
this.contextLabelFrame = null; const price = this.manualPriceRange;
return `${range.from.toFixed(4)}:${range.to.toFixed(4)}:${price?.minValue ?? ''}:${price?.maxValue ?? ''}`;
}
scheduleOverlays(force = false) {
this.overlayForce = this.overlayForce || force;
if (this.overlayFrame != null) return;
this.overlayFrame = requestAnimationFrame(() => {
this.overlayFrame = null;
const forceDraw = this.overlayForce;
this.overlayForce = false;
const key = this.overlayKey();
if (!forceDraw && key && key === this.overlayRangeKey) return;
this.overlayRangeKey = key;
this.renderAnchorHandles();
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels(); this.renderContextLabels();
}); });
} }
attachPointers(kind) {
if (this.pointerAttached[kind]) return;
this.pointerAttached[kind] = true;
if (kind === 'tool') {
window.addEventListener('pointermove', this.toolMoveListener);
window.addEventListener('pointerup', this.toolUpListener);
} else if (kind === 'anchor') {
window.addEventListener('pointermove', this.anchorMoveListener);
window.addEventListener('pointerup', this.anchorUpListener);
} else if (kind === 'line') {
window.addEventListener('pointermove', this.lineMoveListener);
window.addEventListener('pointerup', this.lineUpListener);
}
}
detachPointers(kind) {
if (!this.pointerAttached[kind]) return;
this.pointerAttached[kind] = false;
if (kind === 'tool') {
window.removeEventListener('pointermove', this.toolMoveListener);
window.removeEventListener('pointerup', this.toolUpListener);
} else if (kind === 'anchor') {
window.removeEventListener('pointermove', this.anchorMoveListener);
window.removeEventListener('pointerup', this.anchorUpListener);
} else if (kind === 'line') {
window.removeEventListener('pointermove', this.lineMoveListener);
window.removeEventListener('pointerup', this.lineUpListener);
}
}
// --- tool arming and placement gestures --------------------------------- // --- tool arming and placement gestures ---------------------------------
setToolCompleteHandler(handler) { this.onToolComplete = handler; } setToolCompleteHandler(handler) { this.onToolComplete = handler; }
@ -1255,6 +1279,8 @@ class ConfluenceChart {
this.pendingAnchor = null; this.pendingAnchor = null;
this.clearGesture(); this.clearGesture();
this.clearLinePreview(); this.clearLinePreview();
if (tool) this.attachPointers('tool');
else this.detachPointers('tool');
} }
/** /**
@ -1292,11 +1318,7 @@ class ConfluenceChart {
this.observedPlotCanvas = canvas; this.observedPlotCanvas = canvas;
this.plotResizeObserver = new ResizeObserver(() => { this.plotResizeObserver = new ResizeObserver(() => {
this.syncOverlayLayer(); this.syncOverlayLayer();
this.renderAnchorHandles(); this.scheduleOverlays(true);
this.renderLineBridges();
this.renderProjectionDiagnostic();
this.renderComments();
this.renderContextLabels();
if (this.armedTool) this.renderGesture(); if (this.armedTool) this.renderGesture();
}); });
this.plotResizeObserver.observe(canvas); this.plotResizeObserver.observe(canvas);
@ -1377,7 +1399,10 @@ class ConfluenceChart {
const point = this.eventPoint(event); const point = this.eventPoint(event);
if (!point) return; if (!point) return;
if (!this.armedTool) { if (!this.armedTool) {
if (this.withinPlot(point)) this.mousePan = { start: point }; if (this.withinPlot(point)) {
this.mousePan = { start: point };
this.attachPointers('tool');
}
return; return;
} }
event.preventDefault(); event.preventDefault();
@ -1546,6 +1571,7 @@ class ConfluenceChart {
finishToolGesture(event) { finishToolGesture(event) {
this.mousePan = null; this.mousePan = null;
if (!this.armedTool) this.detachPointers('tool');
if (!this.armedTool || !this.gesture) return; if (!this.armedTool || !this.gesture) return;
const tool = this.armedTool; const tool = this.armedTool;
const { start, end } = this.gesture; const { start, end } = this.gesture;
@ -2242,6 +2268,7 @@ class ConfluenceChart {
original: { ...level }, original: { ...level },
moved: false, moved: false,
}; };
this.attachPointers('line');
event.currentTarget.setPointerCapture?.(event.pointerId); event.currentTarget.setPointerCapture?.(event.pointerId);
} }
@ -2291,6 +2318,7 @@ class ConfluenceChart {
const level = this.levels.find(value => value.id === this.draggingLine.id); const level = this.levels.find(value => value.id === this.draggingLine.id);
const moved = this.draggingLine.moved; const moved = this.draggingLine.moved;
this.draggingLine = null; this.draggingLine = null;
this.detachPointers('line');
if (moved && level && this.onLineChange) this.onLineChange({ ...level }); if (moved && level && this.onLineChange) this.onLineChange({ ...level });
} }
@ -2303,6 +2331,7 @@ class ConfluenceChart {
anchor: level && ConfluenceChart.isFlat(level) ? 'price' : anchor, anchor: level && ConfluenceChart.isFlat(level) ? 'price' : anchor,
}; };
event.currentTarget.setPointerCapture?.(event.pointerId); event.currentTarget.setPointerCapture?.(event.pointerId);
this.attachPointers('anchor');
} }
moveAnchor(event) { moveAnchor(event) {
@ -2350,6 +2379,7 @@ class ConfluenceChart {
event.preventDefault(); event.preventDefault();
const level = this.levels.find(value => value.id === this.draggingAnchor.id); const level = this.levels.find(value => value.id === this.draggingAnchor.id);
this.draggingAnchor = null; this.draggingAnchor = null;
this.detachPointers('anchor');
this.hideSnapDot(); this.hideSnapDot();
if (level && this.onLineChange) this.onLineChange({ ...level }); if (level && this.onLineChange) this.onLineChange({ ...level });
} }
@ -2425,7 +2455,10 @@ class ConfluenceChart {
destroy() { destroy() {
if (this.resizeObserver) this.resizeObserver.disconnect(); if (this.resizeObserver) this.resizeObserver.disconnect();
if (this.plotResizeObserver) this.plotResizeObserver.disconnect(); if (this.plotResizeObserver) this.plotResizeObserver.disconnect();
if (this.contextLabelFrame != null) cancelAnimationFrame(this.contextLabelFrame); if (this.overlayFrame != null) cancelAnimationFrame(this.overlayFrame);
this.detachPointers('tool');
this.detachPointers('anchor');
this.detachPointers('line');
clearTimeout(this.lineFocusTimer); clearTimeout(this.lineFocusTimer);
for (const node of this.commentNodes.values()) clearTimeout(node.focusTimer); for (const node of this.commentNodes.values()) clearTimeout(node.focusTimer);
if (this.chartEl && this.clickListener) this.chartEl.removeEventListener('click', this.clickListener); if (this.chartEl && this.clickListener) this.chartEl.removeEventListener('click', this.clickListener);