Skip to content

perf: a commit draws the new stroke, not the whole layer - #362

Merged
difficcd merged 1 commit into
mainfrom
perf/incremental-layer-bake
Oct 6, 2026
Merged

difficcd merged 1 commit into
mainfrom
perf/incremental-layer-bake

Conversation

@difficcd

@difficcd difficcd commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Continuing the overhead work (오버헤드 더는것을 최우선목표로). This is the one behind "선이 30획 이상이 되었을 때".

Cause

Committing a stroke re-bakes the layer's cache canvas, and bake() drew every stroke the layer holds from a cleared canvas. So the Nth stroke of a drawing costs N times the first, and a session's baking grows with the square of the strokes drawn.

Measured on a 1920×1080 layer, one full re-bake:

strokes 1 10 20 30 40 60
brush 23ms 179ms 1031ms 1603ms 2068ms 2985ms
pencil 38ms 203ms 310ms 325ms 613ms 840ms

And the total cost of actually drawing 40 strokes, one at a time:

before after
brush 37160ms 2113ms 17.6×
pencil 15436ms 742ms 20.8×

Skia on the CPU, so the absolute figures sit far above a browser's — the ratio is the point, and it is inherent: drawing N strokes costs N times drawing one, on any renderer.

Change

When a layer's strokes are the ones already on its canvas plus more on the end, only the new ones need drawing. They paint on top, which is the order they would have been drawn in anyway.

appendedAfter(stored, layer, rough) recognises that case by asking what the signature would have been when the layer was shorter (strokeSig/layerSig take an optional count). Everything else — a stroke edited or removed, the layer dragged, a merge, a canvas resize, a boiling phase — still bakes in full.

Why this is safe

It leans on the invariant the cache already needs, and no more. strokeSig has only ever looked at the stroke count and the last stroke's identity, so an edit that changes neither is invisible to it — and rev exists precisely to cover that. offsetLayers says so in its own comment:

Only coordinates change, so the cached canvas signature — built from stroke count and the last stroke's identity — does not notice, and the layer would keep drawing at its old position. Bumping rev is what invalidates it.

A prefix that changed under a cache that did not notice is already a stale cache today. This reuses that assumption rather than widening it.

Two further guards in bake: the stored signature is read before sizeCanvas, because resizing clears the canvas and then nothing is drawn however much the signature claims; and boiling layers are excluded outright, since each phase keeps its own canvas and an append would have to reach all of them.

Tests

test/core/appendedAfter.test.js — 11 cases, and nearly all of them are refusals, because saying "appended" when something else happened leaves stale pixels with no symptom:

  • a bumped rev refuses the append even though the strokes only grew — the safety argument, pinned.
  • the last stroke growing points - the eraser still drawing - is not an append
  • a signature claiming more strokes than the layer has is refused
  • a boiling layer signs per phase, so a different phase is not an append

And the claim itself, checked on bytes rather than on reasoning, in test/canvas/pixels.test.js:

  • drawing only the new strokes gives the same pixels as redrawing the layer — every split point of a four-stroke layer, with an eraser among the appended part, because destination-out composes against what is already on the canvas and is the case most likely to come apart. Byte-identical at all three splits.

1292 tests pass; npm run check green.

To try

Draw thirty or forty strokes on one layer and keep going. The pen should stay as responsive at the fortieth stroke as at the first — before, each commit got steadily heavier. Nothing should look different; if a drawing ever renders wrong after an undo, a layer move, or a merge, that is this change and worth reporting.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
easy-mv-maker Ready Ready Preview Oct 1, 2026 6:28am UTC

Committing a stroke re-baked the layer's cache canvas from nothing, drawing every stroke it
held. So the Nth stroke of a drawing cost N times the first, and a session's baking grew with
the square of the strokes drawn - which is the "it gets heavy past thirty strokes" report.

Drawing 40 strokes one at a time on a 1920x1080 layer, total time spent baking:

    brush    37160ms -> 2113ms   (17.6x)
    pencil   15436ms -> 742ms    (20.8x)

(Skia on the CPU, so the absolute figures are far above a browser's; the ratio is the point.)

When a layer's strokes are the ones already on its canvas plus more on the end, only the new
ones need drawing - they paint on top, which is the order they would have been drawn in
anyway. appendedAfter recognises that case by asking what the signature would have been when
the layer was shorter, and everything else - a stroke edited or removed, the layer dragged, a
merge, a resize, a boiling phase - still bakes in full.

The safety argument is that this leans on the invariant the cache already needs and no more:
an edit that changes strokes without changing the count or the last stroke's identity must
bump `rev`. offsetLayers exists to do exactly that and says so. A prefix that changed under a
cache that did not notice is already a stale cache today.

Checked on pixels rather than on reasoning: appending from every split point of a four-stroke
layer - with an eraser among the appended, because destination-out composes against what is
already there - is byte-identical to redrawing the whole layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@difficcd
difficcd force-pushed the perf/incremental-layer-bake branch from d38c6dd to 1c4e46b Compare October 1, 2026 06:28
@difficcd
difficcd merged commit 1b392b1 into main Oct 6, 2026
6 checks passed
@difficcd
difficcd deleted the perf/incremental-layer-bake branch October 6, 2026 10:27

This branch was successfully deployed

1 active deployment
Preview — 1c4e46b1 Deployed Oct 1, 2026 by vercel[bot]
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.

1 participant