Skip to content

perf: round separator drag pane heights to prevent permanent full repaints - #853

Merged
liihuu merged 1 commit into
klinecharts:mainfrom
NemeZZiZZ:perf/pane-height-rounding
Sep 15, 2026
Merged

liihuu merged 1 commit into
klinecharts:mainfrom
NemeZZiZZ:perf/pane-height-rounding

Conversation

@NemeZZiZZ

Copy link
Copy Markdown
Contributor

What

Round the pane heights computed during a separator drag to integers.

Why

SeparatorWidget._pressedTouchMouseMoveEvent derives the drag distance from event.pageY (src/widget/SeparatorWidget.ts:116). On browsers that report sub-pixel mouse/touch coordinates (Firefox/Safari, or any browser under non-100% zoom / fractional HiDPI scaling), pageY is fractional, so the computed reducedPaneHeight / increasedPaneHeight are fractional and get stored via setBounding / setOptions.

A fractional pane height never equals container.clientHeight, which is always an integer. The guard in DrawWidget.updateImp (src/widget/DrawWidget.ts:75-80) therefore fails on every subsequent update and keeps escalating to UpdateLevel.Drawer: from that moment on, every crosshair mousemove triggers a full main + overlay repaint of both adjacent panes, permanently, until reload.

Measured on the dev UMD bundle with a programmatic scenario (chart 800x500, candle pane + VOL pane, 500 bars; 50 crosshair mousemoves; canvas repaints counted via clearRect instrumentation, fractional pageY drag simulated through the real event path):

50 crosshair moves main canvas repaints overlay repaints pane heights after drag
before drag 0 125 373px / 100px
after fractional drag (before fix) 100 125 381.22px / 91.78px (stuck fractional)
after fractional drag (with fix) 0 125 381px / 92px (integer)

How

Two Math.round() calls at the source, where the heights are computed:

  1. reducedPaneHeight = Math.round(Math.max(startDragReducedPaneHeight - Math.abs(dragDistance), reducedPaneMinHeight))
  2. increasedPaneHeight = Math.round(startDragIncreasedPaneHeight + diffHeight)

This is safe because:

  • The layout itself only produces integer pane heights (Chart._chartBounding is Math.floor'd), so rounding can only converge with the layout, never diverge from it.
  • Rounding both panes independently can drift their sum by at most 1px; the flexible (candle) pane absorbs any remainder on the very next layout({ measureHeight: true }) call, which the drag handler already performs on every move.
  • Integer heights make bounding.height === container.clientHeight hold again, so normal updates stay at UpdateLevel.Overlay.

Verification

  • pnpm type-check — 0 errors
  • pnpm code-lint (biome) — 0 errors
  • Benchmark scenario above re-run on the patched bundle: main canvas repaints per 50 crosshair moves drop from 100 to 0 (identical to the pre-drag baseline), and pane heights are integers after the drag.

Notes

  • Chrome currently reports integer pageY for mouse events, so the fractional coordinate path mainly affects Firefox/Safari and zoomed/HiDPI environments; the benchmark simulates a fractional pageY to exercise the same code path.
  • A user can still set a fractional pane height explicitly through setPaneOptions; that API path is intentionally out of scope for this PR.

@liihuu
liihuu merged commit 4050964 into klinecharts:main Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants