Skip to content

perf: planning a png or a gif stops coding the picture twice - #58

Merged
donislawdev merged 2 commits into
mainfrom
perf/plan-without-encoding
Sep 6, 2026
Merged

perf: planning a png or a gif stops coding the picture twice#58
donislawdev merged 2 commits into
mainfrom
perf/plan-without-encoding

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Fifth chunk of the 2026-09-05 performance report: P7.

Done without moving a single byte, which neither the report nor section 3.5 of the verdicts expected - both assumed rung ceilings meant a breaking change, the way AVIF took it.

The trick

Planning drew the whole picture and compressed it just to learn its length, threw that away, and let the writer do it again. That was 38 to 53% of a PNG run.

The ladder is walked largest rung first. So for any request comfortably above what the top rung encodes to, that rung is already the answer and the encode only confirms it. ladderCeiling is what "comfortably above" means. Everything near a rung boundary still encodes and still gets the exact number, so the answer never changes - only where the exact length is learned does.

The cost is that padding is settled in the writer instead of the plan: one subtraction for PNG, a moved call for GIF.

Measured

Ceilings, via the refusal message which reports the exact minimum, at 640x480:

format smallest largest swept over
png 5456 B 5974 B 12 seeds, label on and off
gif 54518 B 64020 B + 1, 3, 10, 30, 60 frames

The PNG gradient compresses about 210:1, so a ceiling of 16384 leaves 2.7x of headroom - while a provable bound (an incompressible 640x480) would be 1229280 B, 75x too big, and the fast path would essentially never fire.

End to end, tools/probes/abcpu, order reversed, ranges disjoint:

png, 300 files of 200 kB   cpu 3797 -> 1859 ms   wall 4734 -> 2580 ms
gif, 200 files of 200 kB   cpu  797 ->  453 ms   wall 1232 ->  789 ms

PNG 2.04x less CPU and 1.83x wall, GIF 1.76x and 1.56x.

Bytes: 147 combinations of format, size, seed, label and frame count - including sizes either side of the fast-path threshold and either side of every rung - zero differences, refusals matching too.

jpg cannot have this, and that is a result rather than an omission

I implemented it for jpg too and reverted it after reading my own code. jpg writes its padding in front of the picture (Write emits SOI, then writeComments, then the encoder's stream with its SOI dropped). The comment length therefore has to be known before anything is encoded - which is exactly the number the fast path does not have.

The only way round is moving the padding behind the picture: a documented padding channel in MVP-FORMATS.md and a breaking change. Not without you.

The three picture formats look identical at the spot the report pointed at and differ forty lines later, in write order. Same class as wav under P2: identical code, different call site.

Two guards, because the fast path can fail two different ways

  • TestTheLadderCeilingIsAboveEveryPictureTheTopRungMakes - is the ceiling safe. Too high is merely slower; too low is the dangerous direction, because planning then takes a rung on trust whose picture does not leave room, and the writer refuses a size planning accepted - which is the "preview gives the same verdict as the run" row. It reads the constant out of the source rather than copying it.
  • TestPlanningAPictureDoesNotCodeIt - is the fast path actually taken. Without it the ceiling could be right while planning encoded anyway. 50 plans allocate 29 kB against 2.4 MB for one write.

Four mutations, all caught.

Verification

  • full go test -tags "$(cat .github/build-tags)" ./... - green, 83 packages
  • python tools/preflight.py --quick - all 12 checks pass
  • try-named.py on both new guards - 4 mutations, all caught

One staleness.py STALE report during this work was not a defect: the P3 entry names a file that exists only on perf/tone-computed-once, and tools/ is one repository with no branches.

🤖 Generated with Claude Code

Planning drew the whole picture and compressed it only to learn its
length, threw that away, and let the writer do it again. That was 38 to
53% of a PNG run.

The ladder is walked largest rung first, so for any request comfortably
above what the top rung encodes to, that rung is already the answer and
the encode only confirms it. ladderCeiling is what "comfortably above"
means, and planning takes the top rung without encoding when the request
clears it. Everything near a rung boundary still encodes and still gets
the exact number, so the answer never changes.

The bytes are identical, and that was checked rather than reasoned:
147 combinations of size, seed, label and frame count - including sizes
either side of the fast path threshold and either side of every rung -
came out identical, refusals included.

Measured with tools/probes/abcpu, order reversed, ranges disjoint:
  png, 300 files of 200 kB   cpu 3797 -> 1859 ms   wall 4734 -> 2580 ms
  gif, 200 files of 200 kB   cpu  797 ->  453 ms   wall 1232 ->  789 ms

jpg is deliberately left alone. It writes its padding in FRONT of the
picture, so the comment length has to be known before anything is
encoded - which is exactly the number the fast path does not have. The
only way round is moving the padding behind the picture, and that is a
documented padding channel and a breaking change.

Two guards, four mutations, all caught. One says the ceiling is safe -
too low is the dangerous direction, because planning would then accept a
size the writer has to refuse - and it reads the constant out of the
source rather than copying it. The other says the fast path is actually
taken: 50 plans allocate 29 kB against 2.4 MB for one write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit b2eca73 into main Sep 6, 2026
18 checks passed
@donislawdev
donislawdev deleted the perf/plan-without-encoding branch September 6, 2026 04:43
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