Skip to content

sched: a wake its poster parks or exits behind runs where it was posted - #1111

Merged
Japabu merged 5 commits into
mainfrom
sched-place
Nov 1, 2026
Merged

Japabu merged 5 commits into
mainfrom
sched-place

Conversation

@Japabu

@Japabu Japabu commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

SchedPass::begin drains the mailbox before the pass knows its disposition. A fair wake posted on the poster's own CPU was judged against the running task in the very pass that parks or ends that task, and was forwarded to an idle CPU: an IPI and a cold cache where a local switch a moment later was free. The same forward moves communicating pairs apart. In fs-rtt's r2 sweep on the AMD bench, a client that landed on the server's CPU had the server forwarded away (10→14, 14→8), and a pair split across the two CCXs pays about 1.9 µs per round trip (read_4k_at 16.1–16.4 µs in one LLC against 19.9–20.4 µs across).

Behaviour

  • The pass's opening holds every task its drain or its deadlines would queue behind the running task, in either band. The disposition then places each against the CPU as it leaves it (CpuSched::place_settled):
    • after a park, a stop or an exit, the first held task of each band runs here. The rest, which would queue behind it or behind a task already queued in that band, are forwarded to idle siblings as before. So a burst its poster parks behind, such as the soundserver waking two boosted clients, keeps one task here.
    • before a yield, or with the task kept, every held task is forwarded as before.
  • The held buffer is kept between passes, so a hold allocates nothing once warm. begin asserts the previous pass was disposed.
  • Step 2 is dropped (e938fe3e5). WakeCause::parks_next and its hand-off had no caller. sched: a woken partner follows its waker's LLC #1129's waker-LLC wake affinity carries the waker on every wake instead.
  • Issue: issues/a-wake-whose-waker-parks-next-is-forwarded-to-an-idle-cpu.md becomes issues/a-wake-held-for-its-posters-park-is-unpriced-on-metal.md (defect, open). It records what is left: no metal row prices the hold, and the T14 slowdown below is unattributed. Both citations move.

Checks

Evidence: pr-1111/r4 at 24a8a3d0; earlier rounds r1, r3.

check revision result
cargo run -- --ci host at the top of the stack (#1116, #1117, #1129) 1178599c4 108 steps, all green, exit 0
cargo test -p kernel --lib, cargo test -p kernel-sim the review tree before the test helper's clippy fix (fn parameter → bool) 369 passed; every simulator suite green; exit 0 each
libc_children ×6 on QEMU x86-64 (2 CPUs) at this head and ×6 at its base 24a538c88 042b2b299, 24a538c88 12 of 12 PASS, exit 0, no stall

Metal A/B (base 0c7e62ca7, head 28a55585c = this PR's step 1 with #1116 and #1117, before review r1; 4 boots per image per side per bench, evidence)

Every boot returned rc=0. A row resolves only when all 4 of one side's runs beat all 4 of the other's (p = 0.029).

  • The T14, one LLC. The ipc rows are unchanged (ratios 0.997–1.015, none resolved). Eight disk rows and four memfs rows resolved slower, by 1–3%. They are fstat, seek, stat, pread_4k, read_4k_at, write_4k_at and tree.100. The head also carried sched: a thread runs only on the CPUs its process's mask names #1116 and sched: a spawn wakes the shallowest of its equal free-core sleepers #1117, and each side was one image, so nothing attributes these rows to the hold. issues/a-wake-held-for-its-posters-park-is-unpriced-on-metal.md records them. A T14 A/B of this PR alone against its base is staged (below).
  • The AMD bench, two LLCs. This PR targeted the cross-LLC split, and here head read slower. The diagnostic below shows that came from where the pairs landed, not from the change.
    • fs.disk.read_4k_at: base boots ran 16.7, 16.9, 17.0 and 21.3 µs; head boots ran 21.1–22.0 µs. ipc.reqrep.8: base 6.5, 6.5, 6.7 and 8.2 µs; head 8.2–8.5 µs. The other disk rows and the other ipc reqrep rows split the same way, base fast in 3 boots of 4, head slow in all 4.
    • Neither row resolves (p 0.057–0.2), because one base boot sits in the slow mode too.
  • The AMD diagnostic explains it (pr-1111/r2).
  • tty_interrupt stall (libc_children, QEMU x86-64, 2 CPUs). Every run so far:
    • main 24a538c88: 1 stall in 10 (4 earlier runs, then 6 this round with none);
    • step 1 alone, f5ae75f6b: 0 in 6;
    • this head, 042b2b299: 0 in 6;
    • dd9bf974d, step 1 with step 2's then-uncalled hand-off: 2 failures in 3. One was this stall and the other signal_quit: stalled driving the child that reloads, which main has not shown.
    • Pooled, the builds that behave as step 1 failed 2 times in 15 against main's 1 in 10. That separates nothing, in either direction. The stall reproduces on main, so it is not introduced here, but these runs cannot show that the hold leaves its rate unchanged. signal_quit's stall is unexplained. The issue is issues: libc_children's tty_interrupt can stall at the nested shell's line #1127.

T14 A/B of this PR alone (base 24a538c88, head 042b2b299, 4+4 interleaved, judgment)

Every boot EXIT=0. Judged in pinned check trees at each side's revision: osbench and both I219 rows PASS on all 8.

  • ipc.reqrep.* unchanged (ratios 1.002–1.007).
  • Seven disk rows resolve 1.2–2.3% slower: fs.disk.fstat, pread_4k, read_4k_at, write_4k_at and three direct.*. fs.mem.read_4k resolves 5% faster.
  • Not attributed. Timer rows the hold cannot reach resolve slower by the same amount: wake.periodic.idle +2.8% and tail.* +1.6%, timer wakes on idle CPUs, where nothing runs to hold behind. tail.* also reads slower on head in both AMD A/Bs of sched: a woken partner follows its waker's LLC #1129, a different change. In each of these A/Bs the base boots first and the two images differ, so they carry a head bias of 1–3% on rows nothing reaches.
  • issues/a-wake-held-for-its-posters-park-is-unpriced-on-metal.md records this (1b9e7c57d) and asks for an order-balanced A/B.

Risks

  • A held wake waits for the disposition within the same pass. That costs nothing measurable on the host, and the metal price is not yet read.
  • Only the first held task of each band takes the freed CPU. When no idle sibling is free, the rest queue here as before the hold.
  • The T14 disk rows read 1.2–2.3% slower in the A/B of this PR alone. That is the size of the A/Bs' head bias, so it is not attributed either way.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

A request for the waker-LLC wake-affinity A/B on the AMD (from #1121, IPC phase 2)

Please have that A/B's AMD rows include ipc.reqrep.65536 and ipc.reqrep.4096 from osbench's ipc group. The group already runs in the osbench row, so no change is needed to get them, only the reading.

Why:

The base for #1121 is #1108 18060b521; whole messages are 343a655b1. Measurement trees: ipc-messages-ab-base, ipc-messages-ab-h1.

@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up to my earlier note: #1121's AMD ipc.reqrep.65536 regression is not placement. A diary-counting batch put every pair inside one LLC on all four boots, with one park per side per round trip. Your wake-affinity A/B doesn't need to carry that row for #1121's sake; it is in #1121's copies. Evidence: https://github.com/ToyOSOrg/evidence/tree/87bf3a5ac862a2ac8ed2499fe51e2259f1353809/ipc-messages/round-3-ipcwake

Japabu added a commit that referenced this pull request Nov 1, 2026
…ts buffer

The pass's opening held every wake it found behind the running task, and
the disposition placed each against the CPU it left. When the poster
parked, every held wake saw an idle CPU and stayed, so a burst its poster
parked behind queued whole on the poster's CPU. Before the hold each had
gone to its own idle sibling. For RT wakes nothing could move the second,
because only fair tasks are stolen: the soundserver waking two boosted
clients and parking ran them in turn on one CPU.

place_settled now treats a held task as behind when its band already has a
queued task, an earlier held one included. The first held task of each band
takes the freed CPU and the rest are forwarded as before. Two host tests,
fair and RT, hold it; dropping the queued-ahead check reds both and nothing
else.

settle took the held vector by value, so every hold allocated and freed
under the kernel's global heap lock, twice per request/reply round trip. It
now drains the vector and puts the buffer back. begin asserts that the
previous pass was disposed, since an undisposed pass would now lose held
tasks rather than skip a pick.

The issue record said the branch decided the case and stayed assigned. It
is renamed for what remains: no metal row prices the hold, and the T14 rows
that read 1-3% slower in the first A/B, whose head also carried affinity and
the spawn tie-break, are not attributed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1111 at 042b2b299 (sched stack, focused re-review)

Architecture. The hold still sits in SchedPass::begin and is settled by each disposition, with no new mechanism. The r1 fix is a single queued_ahead input to the existing place decision (place_settled → place_behind). That is the right owner and the smallest change.

Round-1 required findings

  • A burst of held wakes: fixed. kernel/pure/sched/cpu.rs:1051-1059: a held task counts as queued behind once its band already has a queued task (has_rt() for RT, fair_len() > 0 for fair), and earlier held tasks count. I traced each disposition:

    • block, stop and exit: the first task of each band stays and the rest are forwarded.
    • none and yield: settle runs while running is still set, so behind_fair/behind_rt forwards every held task as before.

    The two burst tests (fair and RT, cpu.rs:4257-4291) are the right claim. The control r16/mut-1111-burst-r2.log reds exactly those two.

  • No allocation per hold: fixed. settle takes the buffer, drains it and puts it back (cpu.rs:1631-1638). While settling, holding is false, so nothing can push into the buffer that was taken out. The begin assert (:1617) fails loudly if a pass is left undisposed.

  • T14 rows of 1–3%: recorded. The issue and the body list them, say the head also carried sched: a thread runs only on the CPUs its process's mask names #1116 and sched: a spawn wakes the shallowest of its equal free-core sleepers #1117 with one image a side, and attribute nothing. A T14 A/B of this PR alone is staged.

  • tty evidence: fixed. r16/tty/exits.txt shows 6/6 exit 0 at 042b2b299 and 6/6 at 24a538c88. Each log reads 1 passed. The pooled statement is labelled as separating nothing, which is the honest reading.

  • Issue status: fixed. issues/a-wake-held-for-its-posters-park-is-unpriced-on-metal.md is open. Both citations moved, and no reference to the old slug remains at the stack top.

#1116's rebase. The begin conflict was resolved correctly. The assert comes before the outbound relocation and both come before holding = true, so relocate never holds (range-diff against b0a338fe2).

SUGGESTION (optional)

  • issues/a-wake-held-for-its-posters-park-is-unpriced-on-metal.md:3: the exit is a measurement that is owed, and issues/README.md files that under kind: tooling. defect means "real, reproducible", which the 1–3% rows are not yet. Leave it if the A/B is expected to name a cost.

No blocker. LAND, subject to the T14 A/B the orchestrator is running.

Japabu added a commit that referenced this pull request Nov 1, 2026
SYS_MESSAGE_CALL is SYS_MESSAGE_SEND and then a blocking SYS_MESSAGE_RECV on
the same connection, in one syscall. A refused send answers as a send does
with nothing sent; once the request is queued the receive answers as a
receive does, into the same two windows. A protocol whose peer answers each
request in turn makes the next message its answer. The request carries no
handles: an error could come from either half, so a caller could not know
whether they had moved.

The call no longer tells the scheduler that its caller parks next:
#1111 dropped WakeCause::parks_next (e938fe3), and #1129's waker-LLC
affinity carries the waker on every wake instead. What is left is one
syscall entry and exit per round trip.

Consumers: the file server's client (toyos::fs's request and reply) and
osbench's ipc.reqrep rows, whose request's last frame and answer's first
are one call. Connection::call takes the answer straight into the caller's
buffer, as recv_into does. messages_are_whole checks that a call takes the
answer its peer sends, with the answer's handle, split across both windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu and others added 5 commits November 1, 2026 15:42
SchedPass::begin drains the mailbox before the pass knows its
disposition, so a fair wake posted on the poster's own CPU was judged
behind the running task in the very pass that parks or ends that task,
and was forwarded to an idle CPU: an IPI and a cold cache where a local
switch a moment later was free. osbench's same-CPU pingpong rows held
their placement in none of 53 T14 boots, which is what this forward
predicts; a thread started and then joined meets it at the join.

The opening drain now holds what it finds behind the running task, and
each disposition places it against the CPU as it leaves it: after a
park, a stop or an exit it runs here; before a yield or with the task
kept it is forwarded as before. Only the opening drain holds, so the
CPU's own later drains are unchanged.

WakeCause gains `parker`, the CPU whose task posted the wake and parks
next in the same call. The home CPU hands such a fair wake to that CPU
inside one LLC, where the poster's park runs it and the pair's later
wakes are local. No caller sets it yet: it is the placement input the
whole-message IPC's combined call drives in its phase 2.

The simulator's turn_taking_pair holds the first half across seeds:
both threads start on cpu0 beside an idle cpu1 with balancing off, and
no turn is forwarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b24b37a added CpuSched::held and ::holding without entries in
tripwire_field, so a shadow mismatch inside either was reported as the
nearest field below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WakeCause::parks_next(cpu) named the CPU of a poster that parks next, and
the home CPU handed such a fair wake there inside one LLC. No caller ever
set it. The IPC call meant to set it is recommended for dropping, and the
waker-LLC wake affinity that follows this PR carries the waker on every
wake instead, so the field, the hand-off and their three tests go. Step 1
stays: a wake the opening drain finds behind a poster that parks, stops
or exits still runs where it was posted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts buffer

The pass's opening held every wake it found behind the running task, and
the disposition placed each against the CPU it left. When the poster
parked, every held wake saw an idle CPU and stayed, so a burst its poster
parked behind queued whole on the poster's CPU. Before the hold each had
gone to its own idle sibling. For RT wakes nothing could move the second,
because only fair tasks are stolen: the soundserver waking two boosted
clients and parking ran them in turn on one CPU.

place_settled now treats a held task as behind when its band already has a
queued task, an earlier held one included. The first held task of each band
takes the freed CPU and the rest are forwarded as before. Two host tests,
fair and RT, hold it; dropping the queued-ahead check reds both and nothing
else.

settle took the held vector by value, so every hold allocated and freed
under the kernel's global heap lock, twice per request/reply round trip. It
now drains the vector and puts the buffer back. begin asserts that the
previous pass was disposed, since an undisposed pass would now lose held
tasks rather than skip a pick.

The issue record said the branch decided the case and stayed assigned. It
is renamed for what remains: no metal row prices the hold, and the T14 rows
that read 1-3% slower in the first A/B, whose head also carried affinity and
the spawn tie-break, are not attributed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The A/B of #1111 alone on the T14 (base 24a538c against 042b2b2, 4+4
boots) reproduced seven disk rows 1.2-2.3% slower and left ipc unchanged.
Timer rows the hold cannot reach read slower by the same amount there and
in the AMD A/Bs of #1129, each with the base booting first and a different
image per side, so the rows are not attributed. The record says so and
asks for an order-balanced A/B.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu marked this pull request as ready for review November 1, 2026 14:59
@Japabu
Japabu added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit d057f2a Nov 1, 2026
10 checks passed
@Japabu
Japabu deleted the sched-place branch November 1, 2026 15:13
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