Repository navigation
perf: a commit draws the new stroke, not the whole layer - #362
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
force-pushed
the
perf/incremental-layer-bake
branch
from
October 1, 2026 06:28
d38c6dd to
1c4e46b
Compare
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
And the total cost of actually drawing 40 strokes, one at a time:
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/layerSigtake 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.
strokeSighas only ever looked at the stroke count and the last stroke's identity, so an edit that changes neither is invisible to it — andrevexists precisely to cover that.offsetLayerssays so in its own comment: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 beforesizeCanvas, 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 appenda signature claiming more strokes than the layer has is refuseda boiling layer signs per phase, so a different phase is not an appendAnd 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, becausedestination-outcomposes 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 checkgreen.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