Skip to content

perf: a recipe is parsed once instead of twice, and the verdict on P4 - #60

Merged
donislawdev merged 1 commit into
mainfrom
perf/recipe-parsed-once
Sep 6, 2026
Merged

perf: a recipe is parsed once instead of twice, and the verdict on P4#60
donislawdev merged 1 commit into
mainfrom
perf/recipe-parsed-once

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Seventh chunk: P5 done, and P4 closed with a measurement instead of code. These were the last two open items in the report.

⚠️ Stacked on #59 for the same reason it is stacked on #58 - docs/, tools/ and CLAUDE.md have no branches. Merge #58 and #59 first.

P5 - and deliberately not the fix the report asked for

Reading a recipe walked the document three times: the lexer for flow depth, a full parse to check for a stray second document, and the decoder, which parsed the whole file again.

🔴 The report's fix would have reintroduced a defect this project already fixed. It proposed dropping the parse and counting documents from the token stream. But recipesIn counts documents with a body, and a comment before a leading --- is a document without one. Counting raw separators refused files written in ordinary YAML house style - which is exactly why TestOneRecipeIsAcceptedWhateverSeparatorsSurroundIt exists, with a comment describing that bug.

So the parse stays and the decoder's one goes: it now works from the tree already built. The document-counting semantics are untouched.

report's claim 36% of the read
the discarded parse, measured 13% (107 ms of 841 ms)
what the change delivers 10% (839 → 754 ms), ranges disjoint

Measured on the largest recipe the size limit allows: 900 kB, 20 000 targets, nine repetitions interleaved. A few-kilobyte recipe was already instant; where this shows is the batch screen, which re-reads on every keystroke.

The evidence a green suite could not give

This moves the words of a refusal, not the bytes of a file, and no byte guard can see that. Your condition was the corpus before and after, so tools/probes/refusalcorpus.py now exists: 48 malformed and awkward recipes (syntax errors, unknown keys, two BOMs, tabs, unclosed brackets, tags with nothing after them, seven separator layouts) through validate and generate --dry-run, comparing stdout, stderr and the exit code.

96 of 96 identical. Plus five fuzz targets at 20s each, clean.

Two stale mutation entries, one of them mine

The existing strictness entry quoted the call that was replaced, so it reported SKIP and had stopped proving anything. And the entry I added beside it duplicated it while naming a guard about declared keys rather than typos - it came back NOT CAUGHT, which was a statement about my entry, not about the code. Duplicate removed, original repaired, three mutations caught.

P4 - true as a proportion, below the threshold in effect. No code.

batches whole settle Hash Hash share
20 (the report's own case) 2.50 ms 0.98 ~39%
100 11.93 ms 4.52 38%
200 24.53 ms 9.36 38%

🟢 The report is right that the hash is 26-39% - it measures 38%.

🔴 And it does not matter. UX5 puts the reaction threshold at 100 ms. At the batch count the report itself names, a keystroke costs 2.5 ms - forty times under - and the curve is linear, so the threshold falls around eight hundred batches. Same class as P8.

Three reasons it stays without code, two of them older than this session (O145, your decision of 2026-08-27): the fix would be a defence nothing can redden; throttling invalidates the live-validation guards; and the only real remaining fix touches recipe_hash, which goes into somebody else's manifest - untouchable rule 10, not a performance question, for 9 ms at two hundred batches.

🟢 P5 already took part of it for free: O145 recorded 14.24 ms at 100 batches; it is now 11.93 ms, because Parse stopped parsing twice.

Verification

  • full go test -tags "$(cat .github/build-tags)" ./... - green
  • python tools/preflight.py --quick - all 12 checks pass
  • try-named.py on both recipe guards - 3 mutations, all caught
  • 5 fuzz targets, 20s each - clean

🤖 Generated with Claude Code

@donislawdev
donislawdev force-pushed the perf/recipe-parsed-once branch from 384158b to 3049aff Compare September 6, 2026 04:35
@donislawdev
donislawdev force-pushed the perf/recipe-parsed-once branch from 3049aff to 2a7a4ea Compare September 6, 2026 04:43
Base automatically changed from perf/archive-prefix-and-buffers-v2 to main September 6, 2026 04:57
Reading a recipe walked the document three times: the lexer for flow
depth, a full parse to check for a stray second document, and then the
decoder, which parsed the whole file again from scratch.

The decoder now works from the tree the document check already built.

NOT the fix the report asked for. That one removed the parse and counted
documents from the token stream, which would have reintroduced a defect
this project already fixed: recipesIn counts documents with a BODY, and a
comment before a leading "---" is a document without one. Counting raw
separators refused files written in ordinary YAML house style, which is
why TestOneRecipeIsAcceptedWhateverSeparatorsSurroundIt exists. The parse
stays and the decoder's second one goes instead, so the counting semantics
are untouched.

Measured on the largest recipe the size limit allows, 900 kB and 20 000
targets, nine repetitions interleaved: validate 839 -> 754 ms, ranges
disjoint. The report said 36% of the read; the discarded parse measured
13% and the change delivers 10%. On a few-kilobyte recipe none of this is
visible - where it shows is the batch screen, which re-reads on every
keystroke.

A green suite is not evidence here, because this moves the WORDS of a
refusal rather than the bytes of a file, and no byte guard can see that.
The owner's condition was the refusal corpus before and after:
tools/probes/refusalcorpus.py drives 48 malformed and awkward recipes
through validate and generate --dry-run and compares stdout, stderr and
the exit code. 96 of 96 identical. Five fuzz targets, 20s each, clean.

Two mutation entries were stale afterwards and one of them was mine: the
existing strictness entry quoted the call that was replaced, and the one
I added beside it duplicated it while naming a guard about declared keys
rather than about typos. It came back NOT CAUGHT, which was a statement
about my entry and not about the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev force-pushed the perf/recipe-parsed-once branch from 2a7a4ea to 11b5f96 Compare September 6, 2026 04:57
@donislawdev
donislawdev merged commit 3759e5e into main Sep 6, 2026
18 checks passed
@donislawdev
donislawdev deleted the perf/recipe-parsed-once branch September 6, 2026 05:10
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