Skip to content

perf(trap): look up trap table addresses without copying or scanning the protected-code ranges - #1419

Merged
benletchford merged 2 commits into
benletchford:masterfrom
rlanday:perf/trap-table-address-borrow
Sep 5, 2026
Merged

perf(trap): look up trap table addresses without copying or scanning the protected-code ranges#1419
benletchford merged 2 commits into
benletchford:masterfrom
rlanday:perf/trap-table-address-borrow

Conversation

@rlanday

@rlanday rlanday commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Two fixes to the per-dispatch trap-table lookup that #1364 introduced, keeping its provenance check intact:

  1. trap_table_address snapshotted the bus's protected-code ownership on every trap dispatch, cloning the readonly_code_ranges Vec so the check closure could outlive an immutable borrow it did not need (the whole lookup holds the bus immutably). It now answers the check from the live ranges through a borrowed accessor; the snapshot stays for install_trap_address, whose memory closure needs the mutable bus.
  2. The protected-code range list is queried by is_protected_head on every hop of every trap-table chain walk, by readonly_code_contains on protected writes, and by readonly_code_overlaps on every guest write. Every materialized trap slot registers a range (trampoline, gateway, come-from head), so the list holds on the order of a thousand entries and each query was a linear scan. The list is now kept sorted by start with overlapping and touching entries merged on insert, and all three queries are one binary search. Answers are unchanged: coverage of a contiguous interval by the union of ranges is coverage by the single merged range containing its start, and the last range starting before an interval's end has the largest stop. A unit test checks merged insertion and compares both queries against a brute-force union over every interval in a synthetic layout.

Why

Re-baselining perf work against current master showed it executing several times the host work of a tree based on 3c63ca4. Bisect (3 in Three headless replay, 200 M guest instructions, single runs): 5d87791 = 105.9 B host instructions, eef89a3 (#1364) = 268.0 B, 305027d (#1362) = 308.6 B, 83af901 = 308.5 B. A sample profile of master (with #1220 rebased on it, so the single-step effect is excluded) put trap_table_address at 22% of self time with another 6% in the memmove under it.

Measurements

Paired hardware counters on the headless replay (--max-instructions, scripted input where the game needs it), both arms interleaved, identical guest instructions, ticks and framebuffer hash on every pair; the medians of the pair ratios:

step 3 in Three, 200 M: instructions / CPU time SimCity 2000, 400 M: instructions / CPU time
no clone (fix 1) vs rebased #1220 −3.9% (5/5) / per-step CPU time to follow in a comment −1.8% (5/5) / to follow
sorted ranges (fix 2 here) on top of the chrome and routing fixes −47.7% (5/5) / to follow −20.3% (5/5) / to follow
all four fixes together vs rebased #1220 −60.7% instructions, −53.0% CPU time (35.4 s → 16.6 s), 5/5 −39.1% instructions, −30.8% CPU time (18.6 s → 12.9 s), 5/5

Absolute, 3 in Three: 246.7 B → 128.9 B host instructions for the second step. Cycles and CPU time moved the same direction but the machine was loaded during these runs, so the instruction counts are the numbers to trust.

Together with the routing fast path and the themed-chrome cache (separate PRs), 3 in Three goes from 308.5 B on master to 128.9 B; the pre-#1364 tree is 105.9 B.

Measured on builds based on c40588f, the master of the day; the branch is rebased onto d30234c, whose seven commits do not touch these paths.

The per-step instruction counts above were measured while the machine was loaded (cycles and CPU time were noise); the combined row was re-measured with the machine idle, and per-step CPU times from the same idle rerun follow in a comment.

Related fixes for the same regression: #1419 (trap-table lookup), #1420 (routing fast path), #1421 (themed chrome cache); #1220 removes the single-step mode they were measured on top of.

Tests

cargo test: the new unit test plus the existing suite; one failure, step_frame_forces_render_after_same_tick_foreground_progress, fails identically on untouched master (c40588f) here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UqCuD9vsGeeij5DdYt3vcK

…d-code ranges

trap_table_address snapshotted the bus's protected-code ownership on every
trap dispatch, cloning the readonly range list into a fresh Vec so the check
closure could outlive an immutable borrow it did not need: the whole lookup
holds the bus immutably. On a 3 in Three headless replay the lookup was 22%
of host self time with another 6% in the memmove under it. Answer the check
from the live ranges through a borrowed accessor; the snapshot stays for
install_trap_address, whose memory closure needs the mutable bus.
…rship by binary search

Every materialized trap slot registers a protected range (trampoline,
gateway, come-from head), so the list grows to about a thousand entries,
and since the unified runtime every trap dispatch asks whether each chain
hop is a Systemless-owned head: a linear scan over the whole list per hop.
Keep the list sorted by start with overlapping and touching entries merged,
so coverage and overlap questions are one binary search each. Answers are
unchanged: coverage of a contiguous interval by the union of ranges is
coverage by the single merged range that contains its start.
@rlanday

rlanday commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Per-step CPU time, re-measured with the machine idle (3 in Three headless replay, 200 M guest instructions, 3 pairs each, identical ticks and framebuffer hash on every pair; each step against the previous one in the order the fixes were built):

step host instructions CPU time
fix 1: no per-dispatch clone (#1419, first commit) −3.9% (3/3) +9.4% (0/3; cycles +11%)
routing fast path + dialog-frame cache (#1420 + #1421 first commit) −17.8% (3/3) −36.4% (3/3, 33.0 s → 21.1 s)
sorted, merged ranges (#1419, second commit) −47.7% (3/3) −33.8% (3/3, 20.4 s → 13.6 s)
menu bar + titles cache (#1421, second commit) −4.9% (3/3) −1.7% (2/3)

All four together vs rebased #1220 (5 pairs, both workloads): 3 in Three −60.7% instructions / −53.0% CPU time; SimCity 2000 −39.1% / −30.8%.

Note on the first row: removing the per-dispatch clone alone lowers the instruction count but costs CPU time (the borrowed check still walks the whole range list, now without the copy that had just pulled it into cache). The sorted-ranges commit removes that walk, so #1419 is measured as the pair; a run of #1419's two commits on their own is queued and will be posted here.

@benletchford benletchford left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed the range insertion and binary-search invariants, all range writers, 24-bit translation, and protected longword boundaries. The immutable borrow removes the dispatch-time copy without changing ownership checks. Tested this together with #1420 and #1220 on current master: 4,987 library tests passed, three ignored; CI is green.

@benletchford
benletchford merged commit eabcd5a into benletchford:master Sep 5, 2026
4 checks passed
@rlanday

rlanday commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

This PR's two commits on their own (on rebased #1220, against rebased #1220), 5 pairs, identical ticks and framebuffer hash on every pair:

workload host instructions cycles CPU time
3 in Three, 200 M −41.6% (5/5) −34.8% (5/5) −35.0% (5/5, 29.7 s → 20.1 s)
SimCity 2000, 400 M −18.8% (5/5) −15.6% (5/5) −15.7% (5/5, 19.9 s → 16.8 s)

The 3 in Three pairs ran on an idle machine; during the SimCity 2000 pairs macOS PDF indexing was using about 1.6 cores, so treat that CPU-time figure as slightly pessimistic.

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