Skip to content

perf: one filler instead of thirteen, and eight bytes per draw instead of one - #56

Merged
donislawdev merged 2 commits into
mainfrom
perf/one-filler-instead-of-thirteen
Sep 5, 2026
Merged

perf: one filler instead of thirteen, and eight bytes per draw instead of one#56
donislawdev merged 2 commits into
mainfrom
perf/one-filler-instead-of-thirteen

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Third chunk of the 2026-09-05 performance report: P2 and P12a.

The report named four call sites. There are thirteen, in four shapes.

Measured over 64 MiB through a 32 KiB buffer, variants interleaved and the order reversed between repetitions:

shape packages MB/s bytes identical after?
one draw per byte zip, targz, wav 182 no - this is the breaking change
eight via a temporary array (P12a) bmp, gif, ico, opc, png, tiff 1468 yes
eight via a hand rolled shift loop avif, jpg, jxl, webp 845 yes
one store straight into the buffer all thirteen now 2499

P12a was reported for png alone - it is six packages. And the fourth shape was nobody's finding: byte(v >> (8*j)) with ascending j is little endian, so those four collapse to a single LittleEndian.PutUint64, byte for byte identical, for free.

Breaking: zip, targz and wav bytes move

Padding is where those formats spend almost the whole file. Size, structure and readability are untouched - only the padding content differs, so recorded hashes will not match.

End to end on 64 MB, order reversed, drift canary on every run, ranges disjoint: zip 2.81x, targz 2.74-3.32x.

wav gains nothing and is here by your decision after the measurement. Its padding is audio % frameSize, so exactly two bytes of a WAV differ and the time is unchanged - the report grouped it with the other two because the loop looks identical, but the call site is not.

Blast radius checked rather than assumed: 24 formats x 5 sizes x 2 seeds, exit code verified on every run. Three moved, twenty identical.

A blind spot in the golden set, found by probe

Forcing the filler to emit a constant moved 33 of the 54 pinned cases and not one WAV. wav_32kib lands on a size the audio fills exactly, so it walks past the filler untouched - that path had no pinned witness at all while its bytes were being changed. wav_with_the_padding_chunk is that witness. Four cases repinned, one added, 55 total.

"Format has a golden value" is not "this path of the format has a golden value", and the list of case names does not show the difference.

Guard

TestBulkRandomBytesComeFromOnePlace - two mutations, both caught. It asks about Uint64 because that is what all thirteen reached for, names the one honest UintN caller as an exception, and fails if that exception outlives its code. Its comment states what it does not see (a filler built on IntN, Uint32 or Float64, two of which are deliberately left alone in internal/format/archive).

A third mutation proves the golden set covers the shared filler's short tail.

Verification

  • full go test -tags "$(cat .github/build-tags)" ./... - green, 82 packages
  • python tools/preflight.py --quick - all 12 checks pass
  • staleness.py - 797 mutations, every pattern occurs exactly once
  • try-named.py on both affected guards - 12 mutations, all caught

Net: the tree lost 47 lines.

🤖 Generated with Claude Code

donislawdev and others added 2 commits September 5, 2026 22:59
… instead of two

Three findings from the performance report, and two of them came back with the
report's own suggestion measured and refused.

preflight asked the filesystem twice for every planned file. It reads the
output directory once. A run of 100 000 files with --dry-run goes from
15.7-19.7 s to 0.49-0.56 s, order alternated - the check was 97% of it. Ten
thousand names measured on their own: 1.937 s of stat calls against 8.9 ms for
one listing.

That is also a fix rather than only a speedup, and the guard for it says so.
os.Stat follows a link, so a link pointing at nothing answered "no name here"
and the run replaced it without a word. A directory ENTRY is what a taken name
is, whatever it points at. With the old question put back, the new guard
reports that the run went ahead over a name somebody else's link was holding.

A directory that cannot be LISTED is still asked about file by file. Both
systems allow write permission without read, a run into such a directory has
always worked, and reading nothing there and calling it empty would let the run
write over what is inside. That fallback has its own guard, which skips on
Windows because denying a listing there needs an ACL.

The plan ceiling forced a collection to take every reading, so a run of one
kilobyte paid for two of them - measured with GODEBUG=gctrace=1, exactly two on
every run however small. It asks /gc/heap/allocs:bytes first, at 251 ns against
519 us, and only collects when that says it might be over. The shortcut is
sound by an inequality rather than by an estimate: the live heap cannot have
grown by more than has been allocated.

The report asked for /gc/heap/live:bytes and that metric is WRONG here. It
reports the heap as of the last collection, and measured on 2026-09-05 all four
existing ceiling guards stay green with it, because 25 MB of allocation makes
the collector run on its own and the lagging reading catches up by luck. With
the collector switched off the luck goes: a plan six times the ceiling is
accepted. The new guard turns the collector off for exactly that reason.

hashFile reads in 256 KiB pieces. The report asked for a 1 MB buffer through
io.CopyBuffer and that does nothing at all: os.File implements io.WriterTo, so
CopyBuffer hands it the whole job and throws the buffer away - 128 KiB, 256 KiB
and 1 MiB with a plain file all take the same 167-172 ms that io.Copy takes.
Hidden behind a reader that offers only Read, 256 KiB takes 161 ms against 199.
The size is measured too: 64 KiB is 182 ms and nothing above a quarter of a
megabyte can be told apart, so sixteen workers cost four megabytes.

planChildren is sized up front, since the total is known from the groups.

TotalBytes being walked twice is NOT done, and that is a measurement rather
than an oversight: one walk over 100 000 planned files has a median of 0 s and
a maximum of 541 us, so two of them cost half a millisecond of a nineteen
second run. Widening a signature for that would be a change nothing can see.

engine.go went past the length ceiling, and the guard asks for a split by what
the parts do rather than for a bigger number - so preflight and the questions
it asks about names are their own file now. Two ceilings came down with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d of one

Thirteen packages each carried their own copy of the bulk random filler
loop, in four different shapes. Measured over 64 MiB through a 32 KiB
buffer, interleaved with the order reversed between repetitions:

  one draw per byte              182 MB/s   zip, targz, wav
  eight via a temporary array   1468 MB/s   bmp, gif, ico, opc, png, tiff
  eight via a shift loop         845 MB/s   avif, jpg, jxl, webp
  one store into the buffer     2499 MB/s   the shape they all use now

All thirteen now call core.FillRandomBE or core.FillRandomLE. Two
functions rather than one with a flag, because choosing the wrong byte
order is not a style mistake - it silently rewrites every file a format
has ever produced, and an argument would put that one typo away.

BREAKING: zip, targz and wav files have different bytes. Their padding
is where those formats spend almost the whole file, so almost every byte
changes. Size, structure and readability are untouched. End to end on
64 MB, ranges disjoint: zip 2.81x, targz 2.74-3.32x.

wav is in that list for uniformity and not for speed, by the owner's
decision after the measurement: its padding is audio modulo the frame
size, so exactly two bytes of a WAV differ and the time is unchanged.

The other ten packages moved with no byte change at all - the shift loop
IS little endian, which the performance report did not notice, so those
four collapse to one store for free. Checked across 24 formats at five
sizes and two seeds: three moved, twenty identical.

Also closes a blind spot the golden set had. Forcing the filler to emit a
constant moved 33 of the 54 pinned cases and not one WAV: wav_32kib lands
on a size the audio fills exactly and never reaches the filler, so that
path had no pinned witness. wav_with_the_padding_chunk is that witness.

Guard: TestBulkRandomBytesComeFromOnePlace, two mutations, both caught.
It names the one honest UintN caller as an exception and fails if that
exception outlives its code. A third mutation proves the golden set
covers the shared filler's short tail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit ee953f5 into main Sep 5, 2026
18 checks passed
@donislawdev
donislawdev deleted the perf/one-filler-instead-of-thirteen branch September 5, 2026 22:53
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