Skip to content

The boot CPU waits for an AP's echo as long as a dead CPU is: 5 s, not 100 ms - #670

Merged
Japabu merged 2 commits into
mainfrom
wt/toyos-loadfree2
Oct 1, 2026
Merged

Japabu merged 2 commits into
mainfrom
wt/toyos-loadfree2

Conversation

@Japabu

@Japabu Japabu commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

time::AP_START is how long the boot CPU waits for an AP it started to echo its token. It is now DEAF_CPU's 5 s, up from 100 ms. The issue it closes is deleted with it. The harness is main's, byte for byte.

This supersedes #658 (wt/toyos-loadfree, f1ad5d1ed).

What changed, by decision

AP_START is DEAF_CPU's span (kernel/src/time.rs: one declaration, read by both architectures' bring-up loops).

  • When it expires, the machine boots without that CPU and without every CPU after it, for good.
  • A vCPU its host has not run yet is slow, not dead. This kernel already gives a live CPU DEAF_CPU before it calls that CPU deaf.

issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md is deleted. Its exit has two halves:

  • this bound;
  • virt_smp green in a whole-suite run at a load above the dev host's 14 cores, which is the guest run owed below.

What it costs. A CPU that never echoes costs 5 s of boot, once, because the loop stops at the first one.

  • Among the guest tests, only virt_failed_ap_leaves_no_hole (smp-skip-ap) meets it.
    • Its ready marker is the BSP's control-register line. after_console prints it (kernel/src/main.rs:281) before start_other_cpus runs (:438).
    • So the 5 s does not fall in the boot wait. It falls in judge_virt_job's await_marker, as 5 s of silence against GUEST_QUIET's 15 s.
  • Metal, from the round-1 review: the TCO watchdog (9.6 s) is fed only from a scheduler pass (sched/driver.rs:512). The T14 logs put Boot: complete at 1195–1276 ms, so a dead first AP reaches the first pass near 6.2 s.

Dropped, and why

The whole harness half of round 1 and of #658 is dropped:

Gates

  • cargo run -- --ci host at 8b94f8438: EXIT=0, "Host: 56 step(s), all green".
  • git diff origin/main at this head names only kernel/src/time.rs and the deleted issue.
    • The kernel delta is byte-identical to 3f7081381's, where cargo run -- --build-only exited 0.

High risk: SMP bring-up

  • Independent oracle. Linux waits 5000 ms for the same echo on arm64: arch/arm64/kernel/smp.c, __cpu_up, wait_for_completion_timeout(&cpu_running, msecs_to_jiffies(5000)), read at torvalds/linux 551c722f4080.

  • Recorded real failure at 100 ms. virt_smp went red at 8f142ba9e on a loaded host, with SMP: cpu7 mpidr=0x7 did not echo within 100ms … and SMP: 7 of 8 MADT CPUs online.

  • Negative control. No host test can reach the decision:

    • time.rs and both arch/*/smp.rs compile only for the kernel's targets.
    • kernel-loom carries smp_roster.rs to the host, but its await_echo takes the expiry as a predicate and never sees the bound.
    • A host assertion on the constant would only restate it.

    The control is therefore the guest arm: virt_smp under load with AP_START at 100 ms. It goes red only when the host leaves a vCPU unrun for more than 100 ms after its CPU_ON. 636r2-virt.log recorded that once. Round 1's negative-control run, at load 86 → 98, did not: it went 21/21.

Unsure

  • 5 s is Linux's number, not one measured here. A CI runner whose hypervisor withholds a vCPU for longer still boots one CPU short, and virt_smp names that red.
  • time::DEAF_CPU itself can still panic a TCG guest whose host starves it for 5 s. issues/kernel/a-shootdown-panicked-on-a-cpu-the-host-starved.md owns that.

Owed

  • CI at this head.
  • One whole-suite guest run under the 28-spinner load, with virt_smp and virt_failed_ap_leaves_no_hole green on main's wall-clock waits.

🤖 Generated with Claude Code

https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L

…r as long as a dead CPU is

The 21-test guest suite is meant to become a required check on GitHub's
shared Linux runners, so the host's speed and load may decide none of its
verdicts. On main every wait the harness puts on a guest read the host's
wall clock: the boot wait, `Liveness`'s silence and backstop (every
`await_guest`), `drain_until`, `screendump_while`, `run_test_paced`'s
ceiling and the fatal-halt test's wait for QMP to show the other vCPUs
halted. A loaded host stretches a TCG guest's work by however long QEMU's
threads sat runnable and unserved, and a wall-clock wait reads that stretch
as a hang.

`tests/common/steal.rs` is the guest's clock, carried from #658
(`wt/toyos-loadfree`, `f1ad5d1ed`): the wall clock less what the host
withheld from the guest's QEMU. Demand under one thread's worth loses the
moments it waited; demand over it has the share the host served. A guest
that wants nothing gets the wall's time, so a stopped guest is still called
stopped on time. The clock never runs faster than the wall.

Two corrections to #658's reading:
- macOS: `ri_runnable_time` counts a thread running as well as waiting to.
  Measured on this host: one spinner alone for 500 ms ran 500.05 ms and was
  runnable 503.24 ms; 28 on 14 cores ran 6.93 s and were runnable 14.00 s.
  #658 read the whole of it as the wait, so a busy guest on a quiet host had
  half the wall. The wait is now what it holds past the run.
- Linux: `/proc/<pid>/task/*/schedstat` is read per tid, and a span counts
  each thread's growth since the last reading. A thread that exits now loses
  only its last span, where #658's sum lost the thread's whole life and read
  the span as no demand.

`WIDTH` and `set_width` go: the width was a stand-in for contention, and
contention is now the clock's to take out, so keeping both double-counts it.
`host_scale` times boots on the guest's clock, so it measures the host's
speed and not its load. The boot wait is 20 s of the guest's clock, the
number a run of one guest always had. The suite's summary says what share
of the wall its guests had.

`time::AP_START` becomes `DEAF_CPU`'s 5 s, up from 100 ms. Its expiry boots
the machine one CPU short for good, and a vCPU its host has not run yet is
slow, not dead. Linux waits 5000 ms for the same echo on arm64
(`arch/arm64/kernel/smp.c`, `__cpu_up`). `virt_smp` was booted 7 of 8 on a
loaded host for this (`636r2-virt.log`), so
`issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md`
goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu
Japabu marked this pull request as ready for review October 1, 2026 10:08
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator guest runs at 3f7081381 (-LOAD = 28 CPU spinners on the 14-core host for the whole job; logs orch/logs/loadfree2-*.log):

job load avg at start → end expected exit result
whole, LOAD 28.0 → 86.3 0 0 21/21 in 176.9 s; "the guests had 35% of the 176s their waits read across"
negative control (change reverted), LOAD 86.3 → 98.3 1 0 21/21 in 42.9 s
whole, quiet — 0 0 21/21 in 26.3 s; "the guests had 83%"

The negative control is green under a heavier load than the arm it controls: on the 21-test suite, main's wall-clock waits, WIDTH and the 100 ms AP_START did not red at load ~98. Under the same load the branch's suite took 4× longer (176.9 s vs 42.9 s).

@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1 at 3f7081381.

Gate. CI: host passed at this head (run 36847219923, macos-latest). Guest: the orchestrator's three runs at this head all exited 0.

Size. The branch adds 486 lines and removes 134, net +352:

  • kernel: +4 −2;
  • issues: −30;
  • harness and host checks: +482 −102.

After the cut below, it is kernel +4 −2 and issues −30, net −28.

BLOCKER

  • tests/common/steal.rs:1 (with tests/checks/steal.rs, tests/checks.rs:107-121, and the steal/Moment/clock hunks of tests/common/qemu.rs and tests/toyos.rs). Delete the guest clock from this PR. It fixes no measured failure:

    • The negative control is 21/21 green at load ~98 (loadfree2-negctl-whole-LOAD.log). That control has main's wall-clock waits, WIDTH and the 100 ms AP_START reverted in.
    • Main's last completed nightly (run 36763711317, 6e0d7da82) ran main's wall-clock waits on GitHub's ubuntu runners at --jobs 1. All twelve guest shards and tcg were green. That suite held 20 of these 21 tests by name.
    • The PR body itself says every sighting of the starved-guest defect is in a cut test. It also says the clock cannot see a runner's hypervisor steal, which is the one contention its stated target has.
    • No wait among the 21 has a recorded red. The hard ceilings a starved guest could still trip are the boot wait and the screendump_* ceilings, and on CI the clock reads those as the wall.

    It is also unready on its own terms:

    • demand for Linux (steal.rs:173) is the reading for that target, and it has never run. Both host gates run on macos-latest, and it was only type-checked.
    • steal.rs:209's unimplemented! makes every guest wait panic on any host but macOS and Linux. Main's Instant ran on all of them.
  • tests/common/qemu.rs:177 (with :2275 and the set_width call in tests/toyos.rs run_tasks). Keep WIDTH, set_width and main's boot wait. The deletion's only stated reason is that the clock already takes contention out. Without the clock, the deletion is a guess:

NOTE

  • kernel/src/time.rs:248: AP_START at DEAF_CPU's 5 s lands on its own.
    • It is one declaration, and it matches Linux arm64's wait_for_completion_timeout(..., msecs_to_jiffies(5000)).
    • Its red arm is 636r2-virt.log: 100 ms at 8f142ba9e, where cpu7 did not echo and 7 of 8 CPUs came up under load.
    • Guest run 2 is not its negative control: it was green.
    • Metal cost, checked. The TCO is fed only from a scheduler pass (sched/driver.rs:512), and APs reach one only after smp::set_ready. The orchestrator's T14 logs put kernel to Boot: complete at 1195–1276 ms, so a dead first AP reaches the first pass near 6.2 s, inside the 9.6 s bound. The boot deadline (120 s) and the hard-lockup bound (60 s) are wider.
  • The re-cut head (AP_START and the issue deletion, with the harness as main has it) is untested. It owes:
    • CI;
    • one whole-suite guest run under the 28-spinner load, with virt_smp and virt_failed_ap_leaves_no_hole green on main's wall-clock waits. The second test's boot now spends 5 s of CI's 20 s boot wait.
  • The run table's "4× slower" is not the clock's cost.
    • The branch arm opened with external deps changed: cleaning …/tests/toyos-rust-tests, and its artifact-staging waits ran up to 102.9 s before its first PASS. The control's longest staging wait was 11.8 s.
    • Per-test times add up to 214 s on the branch and 169 s on the control. The gaps are virt_timer_preempts (31 s vs 10 s), iommu_virtio_platform (34 s vs 26 s) and virt_failed_ap_leaves_no_hole (12 s vs 6 s; that one is AP_START's 5 s). No other test differs by more than 2 s.
    • Guest-clock minimum waits do stretch. drain_for (qemu.rs:1427) runs until dur of guest time has passed, so every drain_serial pace and await_guest's 200 ms poll costs dur/share of wall time. At the run's 35% share that is about 570 ms a poll. But that is one poll per await, under a second per test.
    • The two arms ran on unequal builds. The control's green result stands either way.
  • issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md: deleting it is right, and the deletion goes with the AP_START half. Its exit is that bound plus virt_smp green in a whole-suite run at a load above 14 (run 1, 28→86).
  • The host's load decides no verdict: every wait on a guest reads the guest's own clock, and a wait on another CPU is a bound on a dead one #658 is still open, and this PR supersedes it.

REMOVE

SEND BACK

The harness goes back to main's, byte for byte: `tests/common/steal.rs`,
`tests/checks/steal.rs` and `steal_clock` are deleted, and every wait that
read the guest's clock reads the wall again. `WIDTH`, `set_width`,
`host_scale`'s wall-clock boots and the boot wait's `10 s x max(width, 2)`
stand as main has them.

The clock fixed no measured failure. With main's wall-clock waits, `WIDTH`
and the 100 ms `AP_START` reverted in, the 21-test suite went 21/21 under 28
spinners on the 14-core dev host at a load average of 86 to 98
(`loadfree2-negctl-whole-LOAD.log`). Its Linux reading had never run, and its
`unimplemented!` would have panicked every wait on any other host. Deleting
`WIDTH` was justified only by the clock taking contention out; without the
clock it is unmeasured, and #638 deletes it on its own terms.

What stays is `time::AP_START` at `DEAF_CPU`'s 5 s, from 100 ms, and the
deletion of the issue it closes. Linux arm64 waits 5000 ms for the same echo
(`arch/arm64/kernel/smp.c`, `__cpu_up`). `virt_smp` booted 7 of 8 with
"cpu7 mpidr=0x7 did not echo within 100ms" on a loaded host
(`636r2-virt.log`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
@Japabu Japabu changed the title A wait on a guest reads the guest's own clock, and an AP is waited for as long as a dead CPU is The boot CPU waits for an AP's echo as long as a dead CPU is: 5 s, not 100 ms Oct 1, 2026
@Japabu

Japabu commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator guest run at 8b94f8438 under the 28-spinner load (load average 14.4 → 47.1): the whole suite, exit 0.

test result: ok. 21 passed, 21 total (31.0s)

@Japabu
Japabu added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit 5905282 Oct 1, 2026
1 check passed
@Japabu
Japabu deleted the wt/toyos-loadfree2 branch October 1, 2026 10:50
Japabu added a commit that referenced this pull request Oct 1, 2026
Japabu added a commit that referenced this pull request Oct 2, 2026
…illisecond

`off` gave every other CPU 100 ms to take `SGI_OFF` and be off by
`AFFINITY_INFO`, on a private `Budget` whose number was a choice. A vCPU its
host serves late is slow and not dead: #670 moved `time::AP_START` off the
same 100 ms on a recorded red. The wait now ends at `time::DEAF_CPU`'s span,
the bound the tree already has for a CPU that does not take an interrupt, and
declares nothing of its own. Its end stays a degraded answer and no panic:
what keeps a CPU on may be firmware's refusal of `CPU_OFF`, and the machine
was asked to end.

The poll was a bare spin of firmware calls. At 100 ms
`virt_off_names_the_cpus_left_on`'s trace was 6199064 bytes, 50812 calls; at
five seconds that loop is fifty times it. `AFFINITY_INFO` is now asked again
once per `ASK_AGAIN`, a 1 ms `Cadence`, as the xHCI port poll is paced: the
same row's trace is 610488 bytes, 5004 calls, and the row says its count.

Review of 17e6363, the rest:
- `psci::Error::code` wrote `Error::of`'s table a second time for callers
  that print the number. `cpu_off`, `system_off`, `system_reset` and
  `affinity_info`'s refusal answer the `i32` the call returned.
- `psci::init` runs first in `interrupts`, as x86-64's `init_reset` does, so
  a panic in the per-CPU block or the GIC's bring-up has a reset.
- `virt_fatal_halts_the_others_first` is held to the armed line as a machine
  with a reset and no panic key says it, `panic: rebooting in 60 s, timed
  by`, where it took the held line too. The issue that records what stays
  unheld says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
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