Skip to content

docs: close APU Workstream D — one adoption, five closed levers - #386

Merged
doublegate merged 5 commits into
mainfrom
docs/v2.3.6-apu-d1-d6-rejection
Aug 17, 2026
Merged

docs: close APU Workstream D — one adoption, five closed levers#386
doublegate merged 5 commits into
mainfrom
docs/v2.3.6-apu-d1-d6-rejection

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Records v2.3.6 workstream D1 + D6 as measured and rejected. Documentation only — no code ships, and the implementations are reverted.

What was measured

D1Apu::dmc_tick_end runs every CPU cycle at 1.789 MHz and was the largest untouched component of the APU's 18.7% of frame time (~23% of per-cycle cost). Exactly two things in it are unconditional hardware — the byte-timer clock and the get/put parity flip; everything else is DMA corner-case bookkeeping gated on some piece of DMC state being non-idle. D1 added an idle guard between the two.

Deliberately not the maintained summary flag the v2.3.4 note proposed: that would need updating at each of ~30 mutation sites across apu.rs and dmc.rs, and one missed site is a silent accuracy regression in the least testable corner of the emulator. The guard instead reads the same fields the skipped blocks branch on, so byte-identity was structural rather than asserted.

D6LengthCounter::reload, called four times per cycle, was doing an unconditional store of new_halt over an already-identical halt. Four redundant stores per cycle, each writing a byte back onto itself.

Why rejected

Two independent runs of scripts/perf/ab_check.sh --base origin/main, on a host verified quiet first (self-hosted review runner idle, load 2.06/20 cores, no cargo running).

workload run 1 candidate run 1 control run 2 candidate run 2 control
nes_run_frame_nestest −0.24% (p = 0.50) −4.27% +0.99% +1.37%
nes_run_frame_flowing_palette −2.39% −3.37% +1.12% +0.53%
nes_run_frame_nestest_fast −2.91% −3.72% +1.26% +1.13%
nes_run_frame_flowing_palette_fast −3.81% −3.73% +0.58% +0.91%

Run 1 looked like a textbook win and was entirely an artifact. Its order-bias control re-benches the reference against itself, and drifted −3.4% to −4.3% on all four workloads — the apparent −3.81% is accounted for almost exactly by a −3.73% drift measured with no code change at all.

The mechanism is recorded because it will recur: the script builds the reference in a throwaway worktree and benches it immediately after, so on this workspace the reference measures on a machine still hot from a 44.9 s fat-LTO compile across all cores. Run 2 removed the confound (cached build, AB_MEASUREMENT_TIME=25) and the drift fell from ~4% to ~1%. In that better-conditioned run the candidate tracks the control to within a few tenths of a percent on every workload, and the raw sign has flipped positive.

Three independent grounds, any one sufficient: the sign flips between runs; neither control was clean and run 1's failed outright; and in the well-conditioned run the candidate is indistinguishable from the drift.

Why a null was the expected result

Recorded because a null with no mechanism is just a failed measurement. Under lto = "fat" with codegen-units = 1, dmc_tick_end is already inlined into cpu_clock and LLVM can CSE the field loads across the blocks the guard skips. The branches D1 elides were always-not-taken and perfectly predicted, so trading ~9 predictable not-taken branches for 9 loads, an OR-reduction and one branch is arithmetically a wash.

"Inert on almost every cycle" predicts a large win only if the work is actually executed — under fat LTO with perfect prediction it largely is not. That applies to D2, D4 and D5 too, and is written down so it tempers them rather than being rediscovered three more times.

Correctness is not what failed

Byte-identity was established and is not in question: dmc_dma 1/1, dma_timing_pin 11/11, APU unit suite 143/143, AccuracyCoin 141/141 on the authoritative RAM decoder, nestest 0-diff, and a full --features test-roms sweep across 127 test binaries. The rejection is on evidence of benefit.

D1 is reverted rather than kept as a simplification: its correctness argument is a nine-term case analysis sited in the DMA timing the $500/$520/$540 battery exists to pin, and carrying that for a zero effect is the trade v2.2.3 P3 and v2.3.6 D3 already declined.

Also recorded

D5 declined without measurement, on inspection: add_sample cannot know whether expansion audio is live, so keying its finite-check on the caller's external argument swaps one per-cycle branch for another rather than removing one. D2 and D4 remain unmeasured; D4 would add derived state to Pulse, incurring the same snapshot_schema_audit and recompute-on-restore obligations that counted against D3. D3's now-stale "still open" list is corrected to point here.

Workstream D of the v2.3.6 APU round, adjudicated and rejected. Recorded
with its numbers per the standing convention — the reason this campaign
can skip so many settled dead ends is that the rejections were written
down as carefully as the adoptions.

D1 gated the DMC end-of-cycle tick behind an idle predicate; D6 replaced
`LengthCounter::reload`'s unconditional halt store with a compare. Both
were byte-identical and both are reverted.

Measured as one adoption unit across two independent runs. The headline
is that run 1 looked like a textbook win — up to -3.81% at p = 0.00 on a
shipped `_fast` workload — and was entirely an artifact. Its order-bias
control, which re-benches the reference against ITSELF, drifted -3.4% to
-4.3% on all four workloads. The apparent -3.81% is accounted for almost
exactly by a -3.73% drift measured with no code change at all.

The mechanism is recorded because it will recur: `ab_check.sh` builds the
reference in a throwaway worktree and benches it immediately afterwards,
so on this workspace the reference measures on a machine still hot from a
44.9-second fat-LTO compile across all cores, while the candidate runs
once thermals have settled. Run 2 removed the confound — cached reference
build, `AB_MEASUREMENT_TIME=25` — and the control's drift fell from ~4% to
~1%. In that better-conditioned run the candidate tracks the control to
within a few tenths of a percent on every workload, and the raw sign has
flipped positive. Two runs, drift-corrected, both say zero.

Also recorded: why a null was the *expected* result, which matters because
a null with no mechanism is just a failed measurement. Under `lto = "fat"`
with `codegen-units = 1`, `dmc_tick_end` is already inlined into
`cpu_clock` and LLVM can common-subexpression the loads across the blocks
the guard skips; the branches D1 elides were always-not-taken and so
perfectly predicted. Trading nine predictable not-taken branches for nine
loads, an OR-reduction and one branch is arithmetically a wash. "Inert on
almost every cycle" predicts a large win only if the work is actually
executed, and under fat LTO with perfect prediction it largely is not.
That reasoning applies to D2, D4 and D5 as well, and is written down so it
tempers them rather than being rediscovered three more times.

Reverted rather than kept as a simplification. D1's correctness argument
is a nine-term case analysis sited in the DMA timing that the
`$500`/`$520`/`$540` implicit-abort battery exists to pin; carrying that
in the least testable part of the emulator for an effect indistinguishable
from zero is the same bad trade v2.2.3 P3 and v2.3.6 D3 declined.

D5 is additionally recorded as declined WITHOUT measurement, on inspection:
`add_sample` cannot know whether expansion audio is live, so keying its
finite-check on the caller's `external` argument swaps one per-cycle branch
for another rather than removing one. D2 and D4 remain unmeasured, and D3's
now-stale "still open" list is corrected to point here.

Byte-identity is separately established and not in question: `dmc_dma` 1/1,
`dma_timing_pin` 11/11, the APU unit suite 143/143, AccuracyCoin 141/141 on
the authoritative RAM decoder, nestest 0-diff, and a full `--features
test-roms` sweep across 127 test binaries. The rejection is on evidence of
benefit, not on correctness.
Copilot AI lite review requested due to automatic review settings August 17, 2026 03:36
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 43253408-945a-41f9-a5de-ec393f3ef001


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the performance documentation to record the v2.3.6 APU workstream items D1 and D6 as measured and rejected, preserving the measurement context (including order-bias control failure mechanism) so the same pitfalls aren’t rediscovered in later campaigns.

Changes:

  • Added a new “v2.3.6 D1 + D6” section documenting the attempted optimizations, measurement results, and rejection rationale.
  • Documented the observed order-bias/control drift mechanism tied to benchmarking immediately after a hot fat-LTO build.
  • Updated the “v2.3.6 D3” section to reflect that D1/D6 were later measured and rejected (and D5 declined on inspection), leaving D2/D4 as the remaining unmeasured items.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/performance.md
Workstream C/D — "the APU is 18.7% of frame time, recover some of it" —
is closed. Recorded in `docs/performance.md` as a scoped decision rather
than left to lapse, and `docs/STATUS.md`'s v2.3.1 entry, which still
listed the APU as an unmeasured lead, now points at the outcome.

The 18.7% figure itself stands and is not in dispute. It is a correct
subsystem attribution from v2.3.1, recoverable only because that pass
attributed by source file — fat LTO inlines the APU into `cpu_clock` and
hides it from a symbol profile entirely. What is settled is narrower and
more useful: the figure is not recoverable by GATING PER-CYCLE
BOOKKEEPING, which is the only strategy this workstream ever tried.

The tally is one adoption and five closed items. C1, the
default-configuration mix specialization, shipped in v2.3.5 at -3.3% to
-4.2%. D1, D3 and D6 were each measured on a quiet host across two
independent runs and each rejected. D5 was declined on inspection without
measurement. D2 and D4 are left unmeasured deliberately.

The reason to close rather than continue is that the three rejections
share one mechanical cause, which generalises to the two remaining levers.
Under `lto = "fat"` with `codegen-units = 1` the code these levers guard
is already inlined into `cpu_clock`, its loads are already
common-subexpressioned, and the branches being elided are always-not-taken
and so perfectly predicted. Swapping predictable not-taken branches for an
equivalent count of loads and a predicate is arithmetically a wash. Stated
as the rule worth keeping: "this work is inert on almost every cycle"
predicts a win only if the work is actually EXECUTED, and under fat LTO
with perfect prediction it largely is not.

D2 and D4 are structurally the same shape as the three that failed, so
their prior is now a null rather than an unknown. Measuring them would
spend two more quiet-host A/B pairs confirming what three data points
already indicate, and D4 additionally carries a `snapshot_schema_audit`
registration plus a recompute-on-restore obligation — the same two
standing costs that counted against D3. Recorded as a decision so a later
reader does not mistake them for oversights.

Three specific conditions that would justify reopening are written down,
and none is a variation on per-cycle gating: a structural change to how
the APU is clocked or synthesized (C1, the one lever that paid, worked by
specializing an entire code path rather than skipping bookkeeping);
a profile on hardware or a codegen configuration where the fat-LTO and
perfect-prediction premises do not hold; or an instrument better than the
+/-1-2% this host resolves, which is the floor that made three of these
calls "not demonstrated" rather than "demonstrably zero".
@doublegate

Copy link
Copy Markdown
Owner Author

Scope extended: this PR now also closes APU Workstream D.

Recording the D1+D6 rejection made the workstream-level conclusion unavoidable, so it is written down as a scoped decision rather than left to lapse. docs/STATUS.md's v2.3.1 entry — which still listed the APU as an unmeasured lead — now points at the outcome.

The tally: one adoption, five closed items.

lever target outcome
C1 default-configuration mix specialization ADOPTED v2.3.5, −3.3% to −4.2%
D1 DMC end-of-cycle tick, ~23% of per-cycle cost REJECTED — no measurable effect
D3 cached C1 gain predicate REJECTED — sign flipped between runs
D6 four unconditional length.reload() stores REJECTED — no measurable effect
D5 add_sample finite-check hoist DECLINED on inspection
D2 FrameCounter::tick countdown not measured; ceiling ~2.1% of frame
D4 Pulse::muted() caching not measured; would add derived state

The 18.7% figure is not in dispute. It is a correct subsystem attribution from v2.3.1, visible only because that pass attributed by source file — fat LTO inlines the APU into cpu_clock and hides it from a symbol profile entirely. What is settled is narrower: the figure is not recoverable by gating per-cycle bookkeeping, which is the only strategy this workstream ever tried.

Why close rather than continue. The three rejections share one mechanical cause that generalises to the two remaining levers. Under lto = "fat" with codegen-units = 1 the guarded code is already inlined into cpu_clock, its loads are already common-subexpressioned, and the elided branches are always-not-taken and perfectly predicted. Swapping predictable not-taken branches for an equivalent count of loads plus a predicate is arithmetically a wash.

The rule worth keeping: "this work is inert on almost every cycle" predicts a win only if the work is actually executed — under fat LTO with perfect prediction it largely is not.

D2 and D4 are structurally the same shape as the three that failed, so their prior is a null rather than an unknown. They are left unmeasured deliberately, not by oversight, and that is recorded so a later reader does not spend two quiet-host A/B pairs confirming it — D4 additionally carrying the same snapshot_schema_audit and recompute-on-restore obligations that counted against D3.

Three conditions that would justify reopening are written into docs/performance.md, none a variation on per-cycle gating: a structural change to how the APU is clocked or synthesized (C1, the one lever that paid, specialized a whole code path rather than skipping bookkeeping); a profile on hardware or a codegen configuration where the fat-LTO premise does not hold; or an instrument better than the ±1-2% this host resolves — the floor that made three of these calls "not demonstrated" rather than "demonstrably zero".

@doublegate doublegate changed the title docs(performance): record v2.3.6 D1 + D6 as measured and rejected docs: close APU Workstream D — record D1 + D6 as measured and rejected Aug 17, 2026
Two review points on the Workstream D write-up, both fair.

A missing comma after the subordinate clause in the D1 description
("...on a cartridge not running a DMC sample all of it is inert...") made
the sentence garden-path on first read.

"common-subexpression" was used as a verb in two places. It is a noun
phrase naming an optimization pass; the sentences now say that
common-subexpression elimination merged the loads, and that LLVM can
apply it across the blocks the guard skips.

Neither changes the technical content, but this section exists to be read
later by someone deciding whether to reopen the workstream, and a
statement about compiler behaviour is worth stating in the compiler's own
vocabulary.
@doublegate

Copy link
Copy Markdown
Owner Author

Both points taken — fixed in 28491903.

Nitpick, "common-subexpression" as a verb: correct, and worth fixing rather than waving through. It is a noun phrase naming an optimization pass. The two sentences now read "common-subexpression elimination has already merged its repeated loads" and "LLVM can apply common-subexpression elimination to the field loads across the blocks the guard skips". This section exists to be read later by someone deciding whether to reopen the workstream, and a claim about compiler behaviour should be stated in the compiler's own vocabulary or it invites doubt about whether the claim was understood.

Suggestion, communicating the closed status to anyone still referencing the v2.3.4 Workstream C list: already handled, in the two places that pointed at it. The §D3 section carried a "Still open from the v2.3.4 Workstream C list" enumeration naming D1, D2, D4, D5 and D6; it is now marked superseded and redirects here. And docs/STATUS.md's v2.3.1 entry still described the APU at 18.7% as an unmeasured lead; it now records the closure and the reason, leaving range.rs inside Ppu::tick at 1.52% as the one genuinely-open lead from that campaign.

The remaining mentions in VERSION-PLAN.md are historical statements about what v2.3.4 deferred and v2.3.5 delivered, which stay accurate as written and would be wrong to edit.

Self-inflicted, and caught in review. The previous commit rewrote two
sentences in place without re-wrapping the paragraphs around them, leaving
one 120-character line and one sentence hard-wrapped mid-clause. Both are
reflowed to the surrounding width.

Long lines that remain in this section are tables and headings, which
`.markdownlint.json` exempts by design — MD013 is disabled precisely so
the wide measurement tables can stay readable as tables.
@doublegate

Copy link
Copy Markdown
Owner Author

Both wrapping issues were self-inflicted by the previous commit and are fixed in the latest push — thanks for catching them. The prose fix rewrote two sentences in place without re-wrapping the paragraphs around them, leaving one 120-character line and one sentence hard-wrapped mid-clause. Both are reflowed to the surrounding width; the only long lines left in the section are tables and headings, which .markdownlint.json exempts by design (MD013 is disabled so the wide measurement tables stay readable as tables).

On the minus sign: keeping U+2212, because it is the established convention here. Checked rather than assumed — docs/performance.md contains 63 occurrences of U+2212 in sections predating this PR, in exactly this role: −34.6%, −5.13%, −11.3%, −10.1% and so on. Switching the new sections to ASCII hyphens would make this PR the inconsistent one.

Worth stating why the convention exists, since "default to ASCII" is reasonable advice in general: this file is dense with signed percentages where the sign carries the entire meaning of the sentence, and U+2212 renders at the same width as the digits while an ASCII hyphen renders narrow and reads as a dash. In a column of −3.81% / +0.58% the alignment is doing real work.

The substantive one: "Three levers were measured and three produced
nothing" was ambiguous, because C1 was also measured — and adopted. A
reader arriving at the closure to decide whether to reopen it could
reasonably have read that sentence as covering every measurement in the
workstream, which would misstate the record in the one direction that
matters. The sentence now names D1, D3 and D6 and says in the same breath
that C1 was measured and is the one that paid.

The rest is prose. "**The changes**, measured together as one adoption
unit because..." was a sentence fragment; it now follows the "**The
change.**" label-then-sentence pattern the D3 section already uses. A
run-on parenthetical in `docs/STATUS.md` is split into three sentences,
which also lets the second lead — `range.rs` inside `Ppu::tick` at 1.52%,
still unmeasured — stand on its own rather than trailing a clause about
the APU. And a comma pair around "separately" that interrupted the flow
of the byte-identity sentence is dropped.

Paragraphs reflowed after each edit rather than left ragged, which is the
mistake the previous two commits each made in turn.
@doublegate

Copy link
Copy Markdown
Owner Author

Four of five applied — thanks, the second one was a real ambiguity rather than a style point.

Naming the three levers: fixed, and it mattered. "Three levers were measured and three produced nothing" could reasonably be read as covering every measurement in the workstream, when C1 was also measured — and adopted at −3.3% to −4.2%. Since this section exists to be read by someone deciding whether to reopen the workstream, that misstates the record in the one direction that matters. It now names D1, D3 and D6 and says in the same breath that C1 was measured and is the one that paid.

Sentence fragment: fixed. It now follows the "The change." label-then-sentence pattern the §D3 section already uses, so the two rejection write-ups read the same way.

STATUS.md run-on: fixed, split into three sentences. A side benefit — the second lead (range.rs inside Ppu::tick at 1.52%, still unmeasured) now stands on its own instead of trailing a clause about the APU, which is where a genuinely-open item belongs.

Comma after "separately": taken.

Minus sign: declining, same as the previous pass — this was already checked. docs/performance.md contains 63 occurrences of U+2212 predating this PR, in exactly this role (−34.6%, −5.13%, −11.3%, −10.1%). Converting only the new sections would make this PR the inconsistent one, and converting the whole file is a separate change with its own rationale. On searchability specifically: the counter-argument is real but narrow here, since these figures are read in aligned tables where U+2212 matches digit width and an ASCII hyphen renders narrow enough to read as a dash.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR updates the project's status and performance documentation to formally close APU Workstream D and record the rejection of several per-cycle gating optimizations.

Blocking issues

None found.

Suggestions

  • The PR title states "record D1 + D6 as measured and rejected", but the documentation additions formally detail the disposition of D2, D4, and D5 as well to close out the entire workstream. Consider updating the title to accurately reflect the full scope of the change.

Nitpicks

  • This is a trivial documentation-only change; no technical risk.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

@doublegate doublegate changed the title docs: close APU Workstream D — record D1 + D6 as measured and rejected docs: close APU Workstream D — one adoption, five closed levers Aug 17, 2026
@doublegate

Copy link
Copy Markdown
Owner Author

Taken — title updated to docs: close APU Workstream D — one adoption, five closed levers.

You are right that the old trailing clause narrowed it: the PR dispositions all six levers, not the two that were measured last. D1, D3 and D6 measured and rejected; D5 declined on inspection; D2 and D4 deliberately left unmeasured with the reasoning recorded; C1 noted as the one adoption. The new title carries that shape without enumerating it.

Worth more than tidiness here — this repo squash-merges, so the PR title becomes the commit subject and is what git log shows to anyone later asking why the workstream stopped.

Merging. Three consecutive passes with no blocking issues, CI green, all threads resolved.

@doublegate
doublegate merged commit 09651c2 into main Aug 17, 2026
24 checks passed
@doublegate
doublegate deleted the docs/v2.3.6-apu-d1-d6-rejection branch August 17, 2026 04:54
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.

2 participants