Skip to content

cpu: recognise IRIX 6.5's idle loop and park it - #114

Merged
belikh merged 3 commits into
mainfrom
fix/idle-65
Oct 26, 2026
Merged

belikh merged 3 commits into
mainfrom
fix/idle-65

Conversation

@belikh

@belikh belikh commented Oct 26, 2026 •

Copy link
Copy Markdown

What changed

IRIX 6.5's idle loop is now recognised and parked. A settled 6.5 IP22 instance at a getty uses ~2 % host CPU per instance (1–2 % for two in parallel) instead of ~200 %, and the wall-clock idle-pause cold boot is back to 42–50 s (was >400 s). Related: #109 (the old detector could park the no-media PROM boot).

The change has two parts:

  1. CPU idle detector (src/cpu/idle_park.rs, mips_exec.rs): replace the exact PC+GPR hash with a PC-cluster sample; add a 60 s boot warm-up; add idle on|off|status and idleprof stats.
  2. seeq-enet docs: the code fix for the other ~100 % of an "idle" instance's CPU landed separately as seeq8003: stop the enet thread spinning after the first frame #84; this PR keeps the measurement note and the changelog paragraph, not a second copy of the patch.

Capture (the reason for the rewrite)

idleprof on a settled 6.5 IP22 getty (R4400, fresh disk, headless, serial console, xdm/amstart killed) shows the idle path is not a tight branch:

ffffffff880db080..880db0b4  ~1.1 % each, ie%=100   scheduler loop body
ffffffff880daedc..880daf..  ~1.1 % each, ie%=100   run-queue scan (89 instructions, ~470 bytes)
  • The loop calls helpers far away: spl at 0x8801_03xx, the ll/sc lock at 0x8801_ba90, and writes kernel globals.
  • Its registers evolve every iteration (the run-queue scan rotates state), so PC + GPRs never repeats exactly — the old detector could not fire. Confirmed by stepping: every revisit of the same PC has different GPRs.
  • The old hash did match a 3-instruction device-poll loop seen during boot (lbu/andi/bne -3, waiting on a SCSI bit at 0x8805604c), which must keep executing. That is why the wall-clock idle-pause boot stalled: parking a poll loop that needs instructions.
  • 5.3's 14-instruction loop (from clanker/perf/idle-pause-work.md) is the middle case.

The detector

The batch run loop calls the sampler after each ten-instruction group; with SAMPLE_EVERY = 32 that is ~320 guest instructions per record and ~10,000 per 32-sample ring. (The single-step path records every 32 calls.) update sorts the samples and groups them into clusters (consecutive PCs ≤256 bytes apart), then parks when:

  • one kernel cluster holds ≥12 of the 32 samples with ≥12 distinct PCs (the 6.5 loop qualifies; 2-instruction DELAY, 3-instruction device polls, and the 8-instruction ll/sc lock spin do not);
  • user-mode samples are at most a quarter of the ring (a syscall wake runs a few user instructions; a user process spinning fills the ring and is refused);
  • interrupts are enabled, a mask bit is set, nothing is pending, and Compare is armed;
  • any pending line on wake unparked it (masked-wake back-off retained);
  • 60 s have elapsed since process start (IRIS_IDLE_WARMUP_SECS overrides, 0 disables). Parking is a steady-state optimisation; boot has long instruction-counted waits that must not be sliced. The timer is process-wide so monitor stop/start does not restart it.

idle status reports park count, sleep time, wake reasons and the last window's largest cluster.

Evidence

Host: callisto, x86_64, 6 cores (i5-8500T @ 2.1 GHz). Guest: IRIX 6.5 IP22, R4400, fresh sparse copy of the rig disk per run, nvram-rig.bin, telnet forwarded, headless; killall amstart; killall xdm after boot, then settle.

measurement before after
host CPU at settled getty, idle-pause (3 × 10 s) 194 %, 194 %, 193 % (earlier build, detector never fired) 2 %, 2 %, 2 % (run 1); 1 %, 1 %, 2 % (run 2)
two parallel instances, each getty (3 × 10 s) — 1 %, 1 %, 1 % and 1 %, 1 %, 2 %; both answered date
wall-clock idle-pause cold boot to login >400 s 42 s, 50 s (two final-build runs)
ci_clock,idle-pause cold boot to login not measured on the final build 46 s
no-idle baseline with the seeq fix 200–202 % (CPU 100 % + enet 100 %) 83–100 % (CPU thread only)
guest date / sleep 5 across a park — correct; Compare/PIT tick continues
monitor cpu stop; cpu start — parking resumes, settled CPU 2 %, guest responsive

The "before" 194–202 % is CPU thread ~100 % plus seeq-enet ~100 %. top -H before the seeq fix: seeq-enet 98.9 %, MIPS-CPU 28 % (parked), hptimer 15.6 %. After: seeq-enet sleeping, total 1–2 %.

Tests on a quiet host:

build command result
default cargo test --workspace 1465 passed, 0 failed
jitv2 cargo test --workspace --features jitv2 1969 passed, 0 failed
ci_clock cargo test --workspace --features ci_clock 1468 passed, 0 failed
idle-pause cargo test -p iris --lib --features idle-pause 1408 passed, 0 failed
ci_clock,idle-pause cargo test -p iris --lib --features ci_clock,idle-pause 1411 passed, 0 failed

New tests: update_parks_a_long_stationary_kernel_window, update_parks_a_dominant_cluster_with_distant_helper_calls, update_declines_a_short_device_poll_loop, update_declines_a_two_instruction_delay_loop, update_declines_a_user_mode_loop, update_declines_a_window_that_keeps_moving, plus the existing wake/park/pace tests. The mips_exec_test masked-interrupt park test now samples as the run loop does and uses a loop length coprime with the batch.

Not verified / remaining risks

  • IRIX 5.3: no 5.3 image on the rig. Its 14-instruction loop fits the documented rules (≥12 distinct PCs, ≤256-byte gaps, matching the old target) and the park/wake mechanics are unchanged, but this is paper-only.
  • Snapshot restore in this guest still leaves the kernel unresponsive after save/restore (the pre-existing restore-fidelity issue in clanker/perf/idle-pause-work.md §4.2); the save itself works and the CPU parks correctly after restore in the cases where the guest survives. Not caused by this change.
  • Serial console: the CI serial console did not reach a shell (iris-ci run timed out waiting for its marker); telnet over the forwarded port was used for all guest commands. The serial getty is present and the boot serial log is readable.
  • The 60 s warm-up and cluster thresholds are heuristics. They are conservative (short loops and user code excluded), but a future guest with a >60 s boot or a very long hot kernel loop could still be throttled; IRIS_IDLE_WARMUP_SECS=0 and IRIS_NO_IDLE=1 are the escape hatches.
  • This PR is fork evidence only; no upstream PR until reviewed.

Relates to #109.

@belikh

belikh commented Oct 26, 2026

Copy link
Copy Markdown
Author

The commit messages say (#65) for the internal fix/idle-65 campaign ticket, not GitHub issue #65 (which is unrelated); this PR relates to #109. The rig is a 6-core i5-8500T, corrected in the body.

IRIX 6.5 never parked. `idleprof` at a settled IP22 getty shows why: the idle
path is a ~470-byte scheduler loop (0x880daedc..0x880db0b4, ~89 instructions)
with calls to spl and the ll/sc lock and stores to kernel globals. Its
registers evolve every iteration, so the old PC+GPR hash never repeated. The
same hash *did* match short device-poll loops (lbu/andi/bne -3 waiting on a
SCSI bit), which must keep executing, so the detector was wrong in both
directions.

Replace it with a PC-cluster sample. The run loop samples the PC every 32
instructions into a ring of 32 (~1000 instructions); samples are sorted into
clusters (consecutive PCs <=256 bytes apart), and the CPU parks when one
kernel cluster holds at least 12 samples with 12 distinct PCs. That rejects
2-instruction DELAY loops, 3-instruction device polls, the 8-instruction ll/sc
lock spin and mixed work, while accepting 5.3's 14-instruction loop and 6.5's
89-instruction scheduler path. User-mode samples are tolerated up to a quarter
of the ring (a syscall wake runs a few user instructions between kernel
stretches) but a user-dominated ring is never parked.

All earlier safety rules stay: no parking with IE=0 or every mask clear, wake
on any pending line and on running=false, the masked-wake back-off, and no
park before Compare is armed. A 60 s warm-up from process start
(IRIS_IDLE_WARMUP_SECS overrides) keeps boot at full speed: parking is a
steady-state optimisation and IRIX boot runs long instruction-counted waits.

The runtime switch and counters are the monitor's `idle on|off|status` and
`idleprof stats`.

Measured on callisto (x86_64, 16 cores), IRIX 6.5 IP22, R4400, fresh disk:
- settled getty: 2 %, 2 %, 2 % host CPU (was ~200 %).
- two parallel instances, each getty: 1-2 % each, both responsive.
- cold boot to login: 42 s (was >400 s).
- ci_clock,idle-pause cold boot to login: 46 s.

IRIX 5.3 was not verified: the rig has no 5.3 image. Its 14-instruction loop
fits the new rules and the park/wake mechanics are unchanged.
The seeq-enet loop sets `busy = tx_moved || rx_moved` at the end of a pass and
skips its wait while busy, so a burst is pumped without pacing. The "nothing
moved" fast path `continue`d before that assignment, so after the first frame
that was sent or delivered `busy` stayed true forever and the thread spun at
100 % for the rest of the run. Clear it before the `continue`.

This was the other half of an idle IRIX 6.5 instance's ~200 % host CPU. With
the CPU idle detector fixed and this, two parallel 6.5 instances at a getty
use 1-2 % each. `top -H` on callisto, 6.5 IP22 at a getty: seeq-enet ~99 % ->
~0 % (sleeping).
@belikh
belikh merged commit 2ebe1e6 into main Oct 26, 2026
15 of 16 checks passed
@belikh
belikh deleted the fix/idle-65 branch October 26, 2026 16:35
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