Skip to content

feat(perf): measure the two-acquisition race, and land the apt fix #408 missed - #409

Merged
doublegate merged 3 commits into
mainfrom
feat/v2.3.9-lock-race
Aug 19, 2026
Merged

feat(perf): measure the two-acquisition race, and land the apt fix #408 missed#409
doublegate merged 3 commits into
mainfrom
feat/v2.3.9-lock-race

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Two things, stacked on #408: the measurement for v2.3.9 item B, and the apt-ordering fix that #408 was supposed to carry and did not (see the correction note below).

The measurement

v2.3.9 item B recorded a hypothesis rather than a defect: the needs_nes render arm — taken exactly when a debugger or tool panel is open — acquires the emulator lock twice per redraw. Once to copy the framebuffer the user will see, and again for run_shell_ui, where panels read &mut Nes. The guard is dropped between them so composite work does not hold the emulator, which means the emulation thread can take the lock in that gap. If it does, the screen shows frame N while a panel describes N+1 — a confidently wrong answer in Pixel Provenance, whose whole purpose is explaining the pixel you are looking at.

It was written down as a hypothesis with the experiment attached, because this line has already retracted one conclusion drawn from reading rather than measuring. This is the experiment.

Nes::cycle() is read at both acquisitions and compared. The choice of quantity is load-bearing: it is cumulative and monotonic, and produce_one_frame holds the lock across a whole frame, so any difference at all means at least one complete frame landed in the gap — there is no partial-frame reading to misinterpret. No frame counter exists on Nes, and adding one would have been a second source of truth for something cycle() already answers.

Both counters are kept, not just the hits. "The race did not fire" and "nothing was observed" both read as zero hits, and only the denominator separates them — this release's own distinction, applied to its own instrument. The denominator counts redraws where the race could have fired (both readings present: a ROM loaded and the arm run twice). Counting ROM-less redraws would dilute the rate toward zero and manufacture the reassuring answer.

Surfaced in the Performance panel as "panel/screen frame skew — N of M redraws (X%)". The hover text says what a zero does and does not mean: it bounds the effect over that capture, it is not proof the race cannot happen. A measurement that reads as a verdict is how the next person stops looking.

debug-hooks-gated throughout, so the shipped default carries neither the two reads nor the counters. Two tests pin the counter's semantics.

It deliberately fixes nothing. The rate has to be observed on a real session before a fix is chosen, because the obvious fix — merging the two acquisitions — puts composite work back under the emulator lock, which is exactly the regression v2.3.0 fixed.

Correction: the apt-ordering fix missed #408

Review on #408 caught two real defects in apt-install-retry.sh, and I replied there saying they were "fixed in ab30c828". That commit was never on #408's branch. A compound shell command containing its git checkout was blocked by a privilege-escalation guard, so nothing in it ran — including the checkout — and I committed the fix onto this branch instead. The subsequent git push origin feat/v2.3.9-crucible pushed the unchanged local ref: a silent no-op.

So the fix rides here instead, unchanged:

  • Elevation outermost, timeout inside. With timeout outermost the signal goes to the elevation helper, which may not forward it, leaving apt-get orphaned while still holding the dpkg lock — so every retry then fails on the lock rather than the original problem. A retry loop that guarantees its own retries fail is worse than none.
  • DEBIAN_FRONTEND=noninteractive. A config prompt blocks on stdin that never arrives in CI, burning the whole timeout budget — the same failure the script bounds, through a door it had left open.

Re-verified by stubbing the elevation helper, timeout and sleep: fail-fail-succeed names the attempt it succeeded on and exits 0. shellcheck clean.

Verification

fmt, shellcheck, actionlint tree-wide, workspace clippy, frontend under debug-hooks (522 tests), both wasm32 invocations, rustdoc.

No emulation-core file is touched, and no accuracy path — so test-roms should be skipped here, A5's negative demonstration again.

doublegate and others added 2 commits August 19, 2026 09:27
…ut it

v2.3.9 item B recorded a hypothesis: the `needs_nes` render arm — taken exactly
when a debugger or tool panel is open — acquires the emulator lock TWICE per
redraw. Once to copy the framebuffer the user will see, and again sixty lines
later for `run_shell_ui`, where panels read `&mut Nes`. The guard is dropped
between them so the composite work does not hold the emulator, which means the
emulation thread can take the lock in that gap. If it does, the screen shows
frame N while a panel describes N+1 — a confidently wrong answer in Pixel
Provenance, whose whole purpose is explaining the pixel you are looking at.

That was written down as a hypothesis rather than a defect, with the experiment
attached, because this line has already retracted one conclusion drawn from
reading rather than measuring. This is the experiment.

`Nes::cycle()` is read at both acquisitions and the readings compared. The choice
of quantity matters: it is cumulative and monotonic, and `produce_one_frame`
holds the lock across a WHOLE frame, so any difference at all means at least one
complete frame landed in the gap — there is no partial-frame reading to
misinterpret. No frame counter exists on `Nes`, and adding one would have been a
second source of truth for something the cycle counter already answers.

Both counters are kept, not just the hits. "The race did not fire" and "nothing
was observed" both read as zero hits, and only the denominator separates them —
the same distinction this release keeps insisting on, applied to its own
instrument. The denominator counts redraws where the race COULD have fired: both
readings present, meaning a ROM is loaded and the arm ran twice. Counting
ROM-less redraws would dilute the rate toward zero and manufacture the reassuring
answer.

Surfaced in the Performance panel as a rate with the counts beside it, and the
hover text says what a zero does and does not mean: it BOUNDS the effect over
that capture, it is not proof the race cannot happen. A measurement that reads as
a verdict is how the next person stops looking.

`debug-hooks`-gated throughout, so the shipped default carries neither the two
reads nor the counters. Two tests pin the counter's semantics — that an
unobserved redraw is not a clean one, and that hits and observations move
independently.

What this does NOT do is fix anything. The rate has to be observed on a real
session with a panel open before a fix is chosen, because the obvious fix —
merging the two acquisitions — puts the composite work back under the emulator
lock, which is exactly the regression v2.3.0 fixed.

Verified: fmt, workspace clippy with the feature OFF, `debug-hooks` and `full`
frontend combos, both wasm32 invocations, rustdoc, and the frontend suite at 522.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review on #408 found two problems with `.github/scripts/apt-install-retry.sh`,
and both would have made it worse than nothing on the exact path it exists to
protect.

ELEVATION MUST BE OUTERMOST, WITH `timeout` INSIDE IT. The original had `timeout`
on the outside, which sends the signal to the elevation helper rather than to
`apt-get`. The helper may not forward it, leaving `apt-get` orphaned while still
holding the dpkg lock — so every subsequent retry fails on the lock rather than
on the original problem. A retry loop that guarantees its own retries fail is
worse than no retry loop.

`DEBIAN_FRONTEND=noninteractive`, for the same class of reason. A package that
prompts for configuration blocks on stdin that will never arrive in CI, burning
the whole timeout budget waiting for a human who is not there — the exact failure
this script bounds, arriving through a door it had left open. Passed explicitly
because the environment is scrubbed on elevation.

Re-verified with the same stubs: fail-fail-succeed still names the attempt it
succeeded on and exits 0. shellcheck clean.

Both are recorded in the plan beside the item rather than only here, because the
lesson generalises past this script: wrapping a command for reliability puts the
wrapper in the signal path, and the wrapper's own failure modes then belong to
the thing it was protecting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 19, 2026 13:28
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 411f25a1-ce95-448c-9be7-db8e635d7e7f


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

Adds a debug-hooks-gated measurement for potential “panel/screen frame skew” caused by the frontend’s two separate emulator-lock acquisitions in the needs_nes render path, and carries the missed apt-install-retry.sh ordering/CI-hardening fix into this branch.

Changes:

  • Instrument the needs_nes render path to compare Nes::cycle() at the framebuffer-copy acquisition vs the UI acquisition, tracking hits and observations and surfacing the rate in the Perf panel (debug-only).
  • Add RenderPerf/PerfView plumbing and tests for the new counters under debug-hooks.
  • Fix apt provisioning wrapper to put elevation outermost and to enforce noninteractive installs.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
to-dos/plans/v2.3.9-crucible-plan.md Documents the two apt-script ordering defects and rationale.
crates/rustynes-frontend/src/perf.rs Adds lock-gap counters, stats plumbing, and debug-only tests.
crates/rustynes-frontend/src/debugger/perf_panel.rs Displays the lock-gap skew rate (debug-only) in the Perf panel.
crates/rustynes-frontend/src/app.rs Captures Nes::cycle() at both lock acquisitions and records observations.
.github/scripts/apt-install-retry.sh Adjusts apt retry command structure (sudo ordering + noninteractive installs).

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

Comment thread crates/rustynes-frontend/src/app.rs Outdated
Comment thread crates/rustynes-frontend/src/perf.rs
Comment thread .github/scripts/apt-install-retry.sh Outdated
…rrections

Four findings from the #409 review, one of them a real defect in the instrument
this PR exists to add.

`RenderPerf::clear()` did not reset the new `lock_gap_*` counters. It is
documented as a regime-change reset, so a ROM change or pacing-regime change
would have cleared every sample ring and left the numerator and denominator of
the skew rate standing — mixing two populations into one percentage and
presenting it as a single measurement. That is exactly the defect the `wait`
series had before it, and the comment explaining that fix sits four lines above
the place the new counters were missing from. Adding to `stats()` and forgetting
`clear()` is evidently the shape of this mistake; the test now pins it and the
mutation fails without the reset.

The `cycle_at_fb` comment said the reading is taken "at the moment the
framebuffer is copied". It is taken on ACQUIRING the lock, a few lines earlier.
Equivalent — the emulator cannot advance while the guard is held, so every
reading inside that scope names the same frame — but "close enough to be
misleading" is how prose stops being checked, and a reviewer asking means the
next reader would have. The comment now says where it is read and why that is the
same thing.

`DEBIAN_FRONTEND` is now set with `env` rather than as a bare assignment to the
elevation helper. Both work on a standard runner, but the bare form additionally
requires SETENV in sudoers, and on a stricter host it fails by refusing to run at
all — breaking the wrapper rather than degrading it. Raised independently by both
reviewers, which is usually a sign the point is real.

And the counters use plain `+= 1`: one increment per redraw cannot overflow a
`u64` in any run that terminates, so `saturating_add` implied a bound worth
reasoning about where there is none.

Verified: shellcheck, the stub replay (fail-fail-succeed still names its attempt
and exits 0), fmt, workspace clippy, frontend under `debug-hooks` at 523 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@doublegate

Copy link
Copy Markdown
Owner Author

Both accepted, fixed in 6c99d5af

sudo env DEBIAN_FRONTEND=... rather than a bare assignment — adopted. You and Copilot raised this independently, which is usually a sign the point is real. Both forms work on a standard runner, but the bare assignment additionally requires SETENV in sudoers, and on a stricter host it fails by refusing to run at all — breaking the wrapper rather than degrading it, which is the wrong failure mode for a script whose only job is keeping a flaky step from failing.

+= 1 rather than saturating_add — adopted. One increment per redraw cannot overflow a u64 in any run that terminates, so saturating_add implied a bound worth reasoning about where there is none. Noted at the site so it does not get "hardened" back.

Also fixed from the Copilot threads on this PR, and worth flagging here because it is the substantive one: RenderPerf::clear() did not reset the new counters. It is a regime-change reset, so a ROM change would have cleared every sample ring and left the skew rate's numerator and denominator standing — two populations in one percentage. Now pinned by a test and mutation-checked.

Good to see this run complete, incidentally — the three previous runs on #404, #406 and #407 were all blocked by the datacloud_telemetry PreToolUse hook and could not read their diffs at all.

@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR introduces a performance metric to measure frame skew caused by a lock race during rendering and corrects a deadlock in the CI package installation script by restructuring the sudo and timeout commands.

Blocking issues

None found.

Suggestions

  • crates/rustynes-frontend/src/perf.rs (line 360): Using &mut self in a const fn (record_lock_gap) was only stabilized in Rust 1.83.0. If the project's Minimum Supported Rust Version (MSRV) is lower, this will break the build. Consider dropping const since the method is not invoked in a const context.
  • .github/scripts/apt-install-retry.sh (line 46): Security hardening: add -- before "$APT_PACKAGE" (apt-get install -yq -- "$APT_PACKAGE") to guarantee it is evaluated as a positional argument. This prevents option-flag injection if the environment variable happens to start with a hyphen.

Nitpicks

  • crates/rustynes-frontend/src/debugger/perf_panel.rs (line 267): The #[expect(...)] attribute requires Rust 1.81.0+. If supporting older compilers is a priority, use #[allow(...)] instead.

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

@doublegate
doublegate merged commit 243e81d into main Aug 19, 2026
27 checks passed
@doublegate
doublegate deleted the feat/v2.3.9-lock-race branch August 19, 2026 17:19
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
#410's per-game Latency Oracle persistence. The file that is supposed to
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
#410's per-game Latency Oracle persistence. The file that is supposed to
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.
doublegate added a commit that referenced this pull request Aug 19, 2026
Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.
doublegate added a commit that referenced this pull request Aug 19, 2026
…#414)

* docs(changelog): backfill the empty [Unreleased] section, and reject the gate for it

`[Unreleased]` was empty while three merged PRs carried user-visible
change: #407's Divergence Lens (a whole new tool panel and the v2.3.8
"Parallax" marquee), #409's two-acquisition lock-race measurement, and
be the single source of truth for user-visible change did not mention
any of them.

Backfilled now rather than at cut time. The reasons are recoverable from
the commit bodies today and would have to be reconstructed from diffs
later, which is how a release section ends up describing what changed
instead of why.

The obvious gate -- a PR touching `crates/*/src/**` must also touch
`CHANGELOG.md` -- was measured against the last 50 first-parent merges
before being proposed, per this release's own bar that a CI change is
demonstrated against real history rather than argued. It would have gone
red on 8 of them. Three are the genuine misses above. The other five are
this repo's normal workflow: features land bare and the release-cut PR
composes the whole section at once (the probe engine, the Latency
Oracle, the RAM Atlas, the throttle instrument), plus one docs PR that
touched only `//!` preambles.

A 62% false-positive rate against the project's own history. A gate that
fires on the normal workflow is suppressed within a week, and a
suppressed gate is worse than no gate because it still reads as
coverage. Rejected as specified, recorded with its numbers per the
convention `docs/performance.md` sets for rejected optimizations.

The measurement also relocates the defect. The failure mode is not "a
feature landed without an entry" -- at the moment each of those PRs
merged, nothing was wrong. It is that v2.3.8 landed its marquee and was
never cut, so the cut PR that writes the section never ran and v2.3.9
opened on top of an empty [Unreleased]. No per-PR gate addresses that.
What would is a release-ceremony check keyed on the cut, which is named
in the plan as a decision rather than built here.

* docs(changelog): the Divergence Lens entry described one tenth of #407

Review was right on both counts. The entry cited #407 as "v2.3.8
'Parallax' item A" and described only the pixel localisation, omitting
that the feature is reachable at all.

#407 is the whole of v2.3.8. Beyond item A it lands the frontend panel
under Tools -> Analysis, trial-scoped provenance capture, an AUDIO lens
resolving a divergence to the CPU cycle, and the pixel-cause explanation
that closes item B without bisection -- so a located difference comes
back as a cause rather than a coordinate. It also carries a real fix
found inside the work: the Lens left the emulator thirty frames ahead of
where it started, because a trial restores the anchor on the way IN and
not on the way out (deliberate -- it is what lets the Lens read the
trial's final frame off `nes`) and the outermost caller never put the
timeline back.

An entry that omits the panel describes a library, not a release. This
is the reason the backfill exists at all, reproduced at smaller scale
inside the backfill: the further a section is written from the work, the
more of it is missing.

* docs(changelog): drop the note about the changelog's own maintenance

Review is right that this belongs elsewhere. The paragraph explained
that #407, #409 and #410 merged without an entry -- a fact about how
this document is maintained, not about what changed for a user. The
CHANGELOG's own header already says it is the record of user-visible
change, so a section describing its gaps is the one thing in it that is
not.

It is not lost: the full account, including the measured rejection of
the per-PR gate and its 62% false-positive rate, is item D of the v2.3.9
plan, and the reasoning is in this branch's commit bodies.

The companion nitpick -- that the entries read like architecture
decision records rather than concise release notes -- is declined. That
is this project's CHANGELOG voice, not an accident of this PR: every
released section explains the mechanism and what was believed before the
measurement. Matching a generic house style would make these entries
inconsistent with the file they are joining.

* fix(frontend): omit the latency map when empty, so the claim about it is true

Review found that the entry claimed something `#[serde(default)]` does
not provide, and the claim was wrong rather than imprecise.

`serde(default)` covers LOADING a config that lacks the key. It says
nothing about SAVING, and the TOML serializer emits an empty table for an
empty map -- so a user who never opened the Latency Oracle would have had
their config rewritten with a bare `[input.latency_reports]` on the first
save after upgrading. Checked rather than reasoned about: serializing a
default `Config` and grepping the output puts the table at line 100.

Fixed by making the claim true rather than by weakening it. A
`skip_serializing_if` keeps the key out of the file until there is
something to store, and the CHANGELOG now separates the two guarantees
instead of attributing both to `default`.

The same false claim turns out to sit on two SHIPPED fields --
`graphics.hd_packs` (v1.5.0) and `graphics.shader_presets` (v1.2.0) --
both saying a pre-feature config "is byte-identical" when it is only
byte-identical until the first save. Those are corrected in PROSE only,
deliberately: adding `skip_serializing_if` there would change the file
two shipped features write, which is a separate decision with its own
risk, and the wrong half was the claim rather than the behaviour. The
distinction now stated at each site is that `serde(default)` is a LOAD
guarantee.

Two mutations, both directions. Removing `skip_serializing_if` fails the
test -- it is the original defect -- and so does an over-eager
`skip_serializing_if` that always returns true, which would silently
discard real measurements. A one-directional test here would have passed
against a field that never persists anything at all.

* docs(config): correct a direction word, and move the test below the imports

Two review points, both small and both real.

The `shader_presets` note said "same correction as `hd_packs` above".
`hd_packs` is defined eighteen lines BELOW it. A cross-reference that
sends the reader the wrong way is worse than none, and it is the kind of
error that survives because nobody checks a direction word.

The test had been inserted above the module's `use` statements. The
insertion point right after `mod tests {` is before the imports, not
after -- the same slip caught on #420, from the same habit of anchoring
on the `mod tests {` line.

* test(config): round-trip the latency map, not just its key

Review is right that the string checks were half a test. `contains(
"latency_reports")` verifies the KEY and says nothing about the VALUE, so
a `skip_serializing_if` on a field whose `Deserialize` had drifted would
still produce exactly the right text and load back as something else --
and it is the load side the documentation promises.

Both directions now round-trip: the empty config re-parses to an empty
map (so the omitted key is genuinely equivalent to an absent one, which
is the whole claim), and the populated one re-parses to the same
`RememberedLatency` it was given.
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