Skip to content

docs(perf): the RSP's idle steps are already free — measured, and it moves the plan - #235

Merged
doublegate merged 3 commits into
mainfrom
docs/rsp-idle-step-measurement
Jul 31, 2026
Merged

docs(perf): the RSP's idle steps are already free — measured, and it moves the plan#235
doublegate merged 3 commits into
mainfrom
docs/rsp-idle-step-measurement

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Motivation

The fast-exec profile (#234) ended on a question it could not answer: how much of the RSP bucket is a step that had nothing to do? That is what the deficit-counter scheduler has to be sized against — its value is removing visits, and a visit is only worth removing if it costs something.

Answer: the idle steps are ~38% of render-phase steps, and they are already free.

Docs-only. All instrumentation was scratch: bus.rs snapshotted before, restored after, and the tree verified clean against the snapshot between every leg.

How many steps are idle

Counted halted versus executing steps in Bus::rsp_tick over the 900-frame gameplay_phase_probe run.

The cumulative share is misleading, which is why this is tabled per interval rather than quoted as one number — it starts above 80% and falls throughout, because boot is mostly a halted RSP and the render phase is not:

steps (millions) idle in interval share
0–100 82,856,525 82.86%
100–200 70,561,600 70.56%
200–300 90,565,572 90.57%
300–400 64,101,175 64.10%
400–500 46,854,407 46.85%
500–600 40,118,503 40.12%
600–700 40,948,739 40.95%
700–800 38,661,898 38.66%
800–900 37,707,875 37.71%

The last four intervals sit at 37.7–41.0% and are flat, which is what says that is the steady state rather than a point on a curve. Quoting the cumulative 56.9% would have overstated it by half; quoting the first interval's 82.9% by more than double.

Whether removing them is worth anything: no

Rsp::su_step already returns immediately when halted. What a caller could still skip is the wrapper — Bus::rsp_tick's call, the StepResult::default() it constructs, the three Option tests on the result, and the step counter.

A-B-A, gameplay_phase_probe, 900 frames, --features fast-exec:

leg ms/frame
A (unmodified) 63.6, 63.5
B (early-out) 65.8, 64.3
A (restored) 63.5, 65.1

The legs overlap — B's 64.3 sits inside A's 63.5–65.1 — so this is neutral, and B's apparent regression is drift. Without the return leg I would have reported a 2–3% regression.

This harness is noisier than frame_bench (2.5% spread within a leg against ~1%), which is why the result is stated as "neutral" rather than as a number: precise enough to rule out a win of the size that would matter, not precise enough for more.

The reading: LLVM already elides the wrapper's work. su_step returning a default StepResult inlines into rsp_tick, and the dead stores and three None tests fold away. There was nothing there to remove.

What this does to the deficit-counter scheduler's case

It had two justifications, and both are now measured:

  1. scheduler.rs's per-edge dispatch arithmetic — 5.05% (docs(perf): profile the fast-exec frame — and it undercuts the plan's next phase #234). Eliminating it entirely buys 1.05×.
  2. Per-edge chip visits, whose cost is charged to the chip buckets — measured here as ~nothing for the RSP, the largest of them.

That is the second justification failing on its strongest single case. It is not proof for the RDP, AI, PI or VI, whose visits were not measured this way, and the document says so — but the RSP was where the argument was strongest, since it is 21.4% of the frame and 38% of its steps do nothing.

So the RSP's 21.4% is real microcode execution, not dispatch overhead, and the lever that reaches it is a faster interpreter (the plan's pre-decoded threaded design), not fewer visits.

Ruled out

Do not re-try the rsp_tick halted early-out. The analogous change was a win for the RDP and the AI (#219/#221) — but those avoid a core::mem::take of a large struct, and the RSP's wrapper has no such payload, so the pattern does not transfer. Matching the shape of a past win is not evidence.

Note on the branch commit message

One backtick-quoted identifier was eaten by the shell when I wrote the commit (WHETHER REMOVING THEM HELPS: no. already early-outs… should read no. Rsp::su_step already early-outs…). Fixing it would have meant amending an already-pushed commit, which this repo's own rules say to stop and ask about rather than do — so it is left alone and the correct text is here, in what the squash actually lands.

Verification

bash scripts/check_en_us.sh; pre-commit run markdownlint --all-files. Docs-only — no code touched.

🤖 Generated with Claude Code

…moves the plan

Answers the question the fast-exec profile ended on: how much of the RSP bucket
is a step that had nothing to do? That is what the deficit-counter scheduler has
to be sized against, since its value is removing VISITS and a visit is only worth
removing if it costs something.

HOW MANY. ~38% of render-phase steps are halted. The CUMULATIVE share is
misleading and is tabled per interval because of it: it starts above 80% and
falls throughout, since boot is mostly a halted RSP and the render phase is not.
The last four intervals sit at 37.7-41.0% and are flat, which is what says that
is steady state. Quoting the cumulative 56.9% would have overstated it by half;
quoting the first interval's 82.9% by more than double.

WHETHER REMOVING THEM HELPS: no.  already early-outs on halted; what a
caller could still skip is the wrapper — the call, the StepResult::default(), the
three Option tests, the counter. A-B-A over 900 frames:

  A 63.6 / 63.5   B 65.8 / 64.3   A 63.5 / 65.1

The legs OVERLAP — B's 64.3 sits inside A's 63.5-65.1 — so this is neutral and
B's apparent regression is drift. LLVM already elides the wrapper's work: su_step
returning a default StepResult inlines into rsp_tick and the dead stores fold
away. There was nothing there to remove.

WHAT IT DOES TO THE PLAN. The deficit-counter scheduler had two justifications
and both are now measured: scheduler.rs's dispatch arithmetic at 5.05% (1.05x if
eliminated entirely), and per-edge chip visits whose cost lands in the chip
buckets — measured here as ~nothing for the RSP, the largest of them. That is the
second justification failing on its strongest case. Not proof for the RDP, AI, PI
or VI, whose visits were not measured this way, and the doc says so.

So the RSP's 21.4% is REAL MICROCODE EXECUTION, not dispatch overhead, and the
lever that reaches it is a faster interpreter, not fewer visits.

Recorded under Ruled out: do not re-try the halted early-out. The analogous
change WAS a win for the RDP and AI (#219/#221) because those avoid a
core::mem::take of a large struct; the RSP's wrapper has no such payload, so the
pattern does not transfer. Matching the shape of a past win is not evidence.

Instrumentation was scratch-only: bus.rs snapshotted before, restored after, and
the tree verified clean against the snapshot between every leg.

Gates: check_en_us.sh, markdownlint. Docs-only.

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

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 26 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6bb7de3c-e88f-4f70-9c98-3c493d5e892b

📥 Commits

Reviewing files that changed from the base of the PR and between f0b9eca and 1ab5e36.

📒 Files selected for processing (1)
  • docs/performance.md
📝 Walkthrough

Summary by CodeRabbit

  • Documentation
    • Added performance measurements covering idle processing during RSP operation.
    • Documented the limited benefit of skipping halted processing steps, with improvements of less than 0.5% during early startup and lower expected gains during rendering.
    • Recorded that future performance work should prioritise faster RSP interpretation over scheduling changes based on avoiding chip visits.

Walkthrough

The performance documentation adds render-phase RSP idle-step measurements, reports the limited result of a halted-step early-out, and records revised conclusions about deficit-counter scheduling and interpreter performance.

Changes

RSP performance analysis

Layer / File(s) Summary
RSP idle-step measurements
docs/performance.md
The document records render-phase halted-step frequency at approximately 38% and separates it from cumulative boot-phase measurements.
Optimisation and scheduling findings
docs/performance.md
The document reports an early-out improvement of at most 0.45%, excludes a contaminated sample, and records the decision not to land the change. It also redirects scheduling analysis towards interpreter performance and lists the early-out as a ruled-out optimisation.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 9 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes the documented RSP measurements but exceeds 72 characters and uses a non-imperative subject, so it fails the Conventional Commits requirement. Use a concise imperative subject of 72 characters or fewer, such as "docs(perf): document measured RSP idle-step cost".
✅ Passed checks (9 passed)
Check name Status Explanation
Description check ✅ Passed The description directly explains the RSP measurements, benchmark results, and documentation-only scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Oracle Number Is Stated ✅ Passed The PR changes only docs/performance.md; it is a docs-only change, which the custom check explicitly exempts from the n64-systemtest oracle-number requirement.
Docs-As-Spec Sync ✅ Passed The PR changes only docs/performance.md; no rustyn64 chip-crate paths changed, and the PR body states it is docs-only with no code change.
Changelog Entry For User-Visible Changes ✅ Passed Only docs/performance.md changed; it records measurements and states the optimisation was not landed. Under CONTRIBUTING.md, no user-visible change requires an [Unreleased] entry.
Measured, Never Tuned ✅ Passed PR changes only docs/performance.md. The new figures are host-performance measurements with recorded methods; no hardware constant, timing-model value, or executable behaviour was added. The ledger...
Unsafe Stays Out Of The Chip Crates ✅ Passed The PR changes only docs/performance.md; no unsafe or forbid attribute changes exist. Chip roots and rustyn64-core retain forbid(unsafe_code), and Rust AST scans find no unsafe constructs.

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

…fact

Review pointed out that `gameplay_phase_probe` has a ~2.5% within-leg spread,
wider than the effect being looked for, and that `examples/frame_bench.rs` has a
~1% floor. Re-running there REVERSED the result.

The first measurement gave overlapping legs and was recorded as NEUTRAL. On
frame_bench, five readings per leg:

  A  64.231  64.392  64.262  64.401  65.407
  B  63.818  63.896  63.927  63.941  [71.507 excluded]

One reading is excluded and named: B's 71.507 is 11.8% above the rest of its own
leg, which is contamination rather than variance. Every other reading is kept,
including A's 65.407, which is only 1.8% high and has no such excuse.

On that data the legs DO NOT OVERLAP — A's minimum sits above B's maximum — so
the early-out is worth 64.231 -> 63.941 ms = 0.45%.

It is a real effect and it is small enough to change nothing, for two reasons
that are now stated as bounds rather than glossed:

- frame_bench's window is EARLY BOOT, where 80%+ of RSP steps are halted. The
  render phase runs at 38%, so expect roughly half of 0.45% there.
- It is within a factor of two of the harness's own noise floor, which is why
  five readings per leg were needed to see it at all.

Still not landed: 0.45% on the most favorable window does not justify a change
that also breaks `rcp_steps_for_test`'s count, and the reason to reach for it —
that idle visits are expensive — is exactly what it disproves. The value is the
bound, not the patch.

The conclusion about the deficit-counter scheduler is unchanged in substance:
its second justification still fails on its strongest case. What changed is that
the number is 0.45% rather than zero, and a document that says "neutral" when it
means "0.45%, measured on a harness that could not resolve it" is the kind of
claim this project keeps having to retract.

Also adopted: the step-to-frame ratio (~1.0 M RCP steps per frame) is stated so
the step-interval table connects to the 900-frame run, and the illustrative
snippet now shows the three Option tests it skips rather than eliding them.

Gates: check_en_us.sh, markdownlint. Docs-only.

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

Copy link
Copy Markdown
Owner Author

Adjudication of the Antigravity (Gemini) review. The first suggestion was right and it overturned this PR's result (f0b9eca).

Suggestion 1 — re-run the A-B-A on frame_bench rather than gameplay_phase_probe. Adopted, and it reversed the finding.

You are correct that the probe's ~2.5% within-leg spread is wider than the effect being looked for, which means the overlapping legs I reported could not distinguish "no effect" from "an effect this harness cannot see". On frame_bench, five readings per leg:

leg ms/frame
A (unmodified) 64.231, 64.392, 64.262, 64.401, 65.407
B (early-out) 63.818, 63.896, 63.927, 63.941, 71.507

One reading is excluded and named: B's 71.507 is 11.8% above the rest of its own leg — contamination, not variance. Every other reading is kept, including A's 65.407, which is only 1.8% high and has no such excuse.

The legs then do not overlap — A's minimum (64.231) sits above B's maximum (63.941) — so the early-out is worth 0.45%, not zero.

Two bounds now stated rather than glossed: frame_bench's window is early boot, where 80%+ of RSP steps are halted, so the render phase (38%) should see about half of that; and 0.45% is within a factor of two of the harness's own noise floor, which is why five readings per leg were needed to see it.

Still not landed — 0.45% on the most favorable window does not justify a change that also breaks rcp_steps_for_test, and the reason to reach for it (idle visits being expensive) is exactly what it disproves. The conclusion about the deficit-counter scheduler is unchanged in substance; what changed is that the number is 0.45% rather than "neutral", and a document that says neutral when it means 0.45%, measured on a harness that could not resolve it is the class of claim this project keeps having to retract.

Suggestion 2 — state the step-to-frame ratio. Adopted: ~1.0 M RCP steps per frame (900 M over 900 frames), so the 100 M-step intervals read as ~100 frames each.

Nitpick — the truncated snippet. Adopted; it now shows the three Option tests being skipped rather than eliding them behind ..., since what is skipped is the entire subject.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/performance.md`:
- Around line 1357-1364: Revise the benchmark conclusion in the surrounding
performance documentation to state that the measured legs differ by 0.45% but
the result is near the harness noise floor, rather than calling it a real
effect. Clarify that the halted-step experiment primarily represents microcode
execution and shows small measured halted-visit overhead, without claiming it
proves the full 21.4% RSP bucket is microcode execution. Keep the conclusion
limited to RSP and avoid generalizing it to RDP, AI, PI, or VI.
- Around line 1366-1368: Remove the claim that the experiment breaks
rcp_steps_for_test's count from the performance discussion. Keep the remaining
performance rationale unchanged, and only mention a tested patch or failing test
if it actually bypasses Bus::rsp_tick's counter increment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a51f9c54-0d9c-4432-9a82-91c8ef307f60

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc3ab4 and f0b9eca.

📒 Files selected for processing (1)
  • docs/performance.md

Comment thread docs/performance.md Outdated
Comment thread docs/performance.md Outdated
…as invented

Two review findings, both errors of mine.

"SO IT IS A REAL EFFECT" OVERSTATED WHAT FIVE READINGS A SIDE CAN SHOW. The legs
differ by 0.45% and do not overlap; that is an OBSERVATION, not a demonstrated
causal improvement, with an effect within a factor of two of the harness floor
and one excluded sample. What it establishes is an UPPER BOUND: whatever removing
idle visits is worth, it is not more than this. Reworded to say that.

The same over-reach ran into the conclusion. "The RSP's 21.4% is real microcode
execution, not dispatch overhead" generalises from ONE wrapper path. What the
experiment supports is narrower: the halted-visit overhead inside the bucket is
small, so the bucket is predominantly work done while the RSP is running. It does
NOT decompose the running 62% into dispatch versus arithmetic versus register
access, and it says nothing about the RDP, AI, PI or VI. The threaded interpreter
is now "likely" the lever rather than established as one, with the decomposition
named as the next measurement.

I CLAIMED THE CHANGE BREAKS `rcp_steps_for_test`. IT DOES NOT. The experimental
patch increments `rcp_steps` on the halted return, so the count is preserved and
no test is affected. That was a reason invented to reinforce a conclusion already
reached on other grounds — the worst kind of supporting argument, because it
reads as evidence. Removed, and the removal is recorded in the document rather
than quietly dropped, since a reader of the earlier text deserves to know it was
wrong.

The not-landed decision stands on the size of the effect alone, which was always
the real reason.

Gates: check_en_us.sh, markdownlint. Docs-only.

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

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This documentation-only PR adds profiling data and analysis to docs/performance.md regarding RSP idle step overhead, concluding that eliminating idle steps yields at most a ~0.45% improvement and ruling out early-out optimizations for rsp_tick.

Blocking issues

None found.

Suggestions

  • docs/performance.md (lines 1335–1355): The benchmarking conclusion relies on $N=5$ samples per variant and manually discards 1 sample (20% of variant B's data) to claim non-overlapping ranges (64.231 ms vs 63.941 ms). When measuring a 0.45% delta against a ~1% harness noise floor, pruning outliers from small sample sizes to force non-overlapping bounds is statistically unsound. Increase the sample count (e.g., $N \ge 30$) and report the mean with standard deviation or confidence intervals rather than manual exclusion and min/max bounds.

Nitpicks

  • docs/performance.md (lines 1320–1330): The pseudo-code snippet duplicates self.rcp_steps = self.rcp_steps.wrapping_add(1); in both the early-return branch and the fallthrough path. Placing the increment before the if check or using defer/scopeguard logic would make the snippet cleaner if meant for reference.

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

@doublegate
doublegate merged commit 8747b6a into main Jul 31, 2026
12 checks passed
@doublegate
doublegate deleted the docs/rsp-idle-step-measurement branch July 31, 2026 18:45
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