Split out of the #764 round-2 review
(docs/planning/green-program/sockets/764-RCA-2026-09-03.md), which measured
but did not explain a sub-mechanism inside #766's dispatch-latency mechanism:
a thread that has already been woken and dispatched sometimes returns the CPU
without consuming its own wake, several times in a row, before a later
dispatch finally does. This is not the #766 round-robin wait itself -- it is
extra turns inside that wait that #766's "one round robin after a tail
re-enqueue" model does not predict.
The two specimens
repro-boot-057 (docs/planning/green-program/sockets/serials/764-rca/repro-boot-057/serial_kernel.txt):
the reader (thread 30) is woken at line 2291 (unblock(30): Added to per_cpu_queues[0]). It is then restored -- a full dispatch, not merely
enqueued -- six times before it observes the wake:
2313, 2349, 2407, 2490, 2558, 2620 <- "Restored kernel context for thread 30: RIP=0x80002f7912 RSP=0xffffc90000282978"
2622 <- "TCP_BLOCK: Thread 30 woken from recv blocking"
Five of those six restores (2313/2349/2407/2490/2558) are immediately
followed by a CR3 switch and a "Saved kernel context for blocked thread 30"
at an identical RIP/RSP, with zero intervening log lines -- e.g. 2313 (restore)
/ 2314 (CR3) / 2315 (save). Only the sixth (2620) reaches the wake check at
2622 and makes progress.
Re-derived in this slot:
grep -n 'unblock(30)' repro-boot-057/serial_kernel.txt
-> 2123, 2291
grep -n 'Restored kernel context for thread 30' repro-boot-057/serial_kernel.txt
-> 2203(pre-wake), 2313, 2349, 2407, 2490, 2558, 2620
grep -n 'woken from recv blocking' repro-boot-057/serial_kernel.txt
-> 2622
707/boot01 (docs/planning/green-program/sockets/serials/707-2026-09-02/x86-battery-r2/boot01/serial_kernel.txt,
the issue's own #764 specimen): the reader (also thread 30) is woken at
2303, then restored seven times before it observes the wake:
2363, 2489, 2545, 2656, 2705, 2749, 2755 <- restores
2757 <- "TCP_BLOCK: Thread 30 woken from recv blocking"
Six of those seven restores are wasted; the seventh (2755) reaches the check
at 2757 and the read completes.
Re-derived in this slot:
grep -n 'unblock(30)' boot01/serial_kernel.txt
-> 2210, 2303
grep -n 'Restored kernel context for thread 30' boot01/serial_kernel.txt
-> 2287(pre-wake), 2363, 2489, 2545, 2656, 2705, 2749, 2755
grep -n 'woken from recv blocking' boot01/serial_kernel.txt
-> 2757
The 707-battery turn census
docs/planning/green-program/sockets/serials/764-rca/summarize.py, run over
all 25 boots of 707-2026-09-02/x86-battery-r2/, counts a thread's restores
between its wake and the line where its sys_read wait loop observes it
(resumes column). 24 of 25 boots have a countable value (boot14 has no
accept-block line for the port and is excluded):
| turns |
boots |
data_latency_ms |
| 1 |
20 |
318 - 1480 |
| 2 |
1 |
2258 |
| 3 |
1 |
2768 |
| 4 |
1 |
3155 |
| 7 |
1 |
4740 -- the battery's exit 13 |
Re-derived in this slot:
python3 docs/planning/green-program/sockets/serials/764-rca/summarize.py \
docs/planning/green-program/sockets/serials/707-2026-09-02/x86-battery-r2/boot* \
| awk -F'\t' 'NR>1 && $NF!="" {print $NF}' | sort | uniq -c
-> 20 x "1", 1 x "2", 1 x "3", 1 x "4", 1 x "7"
data_latency_ms tracks this turn count, not queue length or a uniform
per-pass cost -- 39 of 46 boots in the separate 76-boot battery's census
consume the wake on turn one, as expected if a dispatched reader normally
exits its wait loop immediately. The multi-turn boots are the outliers this
issue is about.
Why this isn't already explained
A dispatch of the reader is a full context-switch cycle: a restore
(kernel/src/interrupts/context_switch.rs:962), set_running()
(kernel/src/task/scheduler.rs:2103), and a fresh quantum
(kernel/src/interrupts/context_switch.rs:436). sys_read's wait loop
breaks the moment thread.state != Blocked
(kernel/src/syscall/handlers.rs:1403-1410):
crate::task::scheduler::yield_current();
crate::arch_halt_with_interrupts();
let still_blocked = crate::task::scheduler::with_scheduler(|sched| {
if let Some(thread) = sched.current_thread_mut() {
thread.state == crate::task::thread::ThreadState::Blocked
} else {
false
}
})
.unwrap_or(false);
if !still_blocked {
// ... consumes the wake
So once unblock() has run (set_ready() at scheduler.rs:2897, the
Added to per_cpu_queues log at :2951), a subsequent restore of the same
thread should read Blocked as false and take the !still_blocked arm on
its very first dispatch. It does not, five and six times respectively on the
two specimens above -- and a restore followed immediately by a save at the
same RIP/RSP looks identical in the serial log whether still_blocked ran
and (correctly or incorrectly) read Blocked, or the thread executed no
instruction from still_blocked at all before yielding again. The serials
alone cannot distinguish those two cases.
Candidate reading
Not concluded, just the shape worth checking: set_ready() at
scheduler.rs:2897 and set_running() at scheduler.rs:2103 are two
separate writes to thread state, on two different sides of a dispatch. If
there is a window in which a thread is dispatched (restored, set_running()
called) but its state field has not yet settled to a value distinguishable
from Blocked at the point still_blocked reads it -- or conversely, if
still_blocked can read a state that flips back to Blocked between the
read and the yield_current()/halt on some other iteration -- either would
produce exactly this shape: N restores that make no progress, then one that
does.
Proposed experiment (per CLAUDE.md: tracing framework, not log lines)
A trace_count! (or trace_event!) on the still_blocked == true branch of
the loop at handlers.rs:1403-1410, read back through the lock-free tracing
framework (kernel/src/tracing/, trace_dump() / trace_dump_counters()
via GDB or /proc/trace/*) rather than through a log line, so the hot
context-switch/syscall-wait path is not disturbed. If the counter increments
on the wasted restores, still_blocked is reading Blocked correctly on
each of them, and the question becomes why the scheduler keeps
re-dispatching a thread whose own wait loop has no work to do until turn N;
if it does not increment on some of the wasted restores, the thread is
failing to reach or execute that check at all on those restores, which
points at the dispatch path instead.
Related
Split out of the #764 round-2 review
(
docs/planning/green-program/sockets/764-RCA-2026-09-03.md), which measuredbut did not explain a sub-mechanism inside #766's dispatch-latency mechanism:
a thread that has already been woken and dispatched sometimes returns the CPU
without consuming its own wake, several times in a row, before a later
dispatch finally does. This is not the #766 round-robin wait itself -- it is
extra turns inside that wait that #766's "one round robin after a tail
re-enqueue" model does not predict.
The two specimens
repro-boot-057(docs/planning/green-program/sockets/serials/764-rca/repro-boot-057/serial_kernel.txt):the reader (thread 30) is woken at line 2291 (
unblock(30): Added to per_cpu_queues[0]). It is then restored -- a full dispatch, not merelyenqueued -- six times before it observes the wake:
Five of those six restores (2313/2349/2407/2490/2558) are immediately
followed by a CR3 switch and a "Saved kernel context for blocked thread 30"
at an identical RIP/RSP, with zero intervening log lines -- e.g. 2313 (restore)
/ 2314 (CR3) / 2315 (save). Only the sixth (2620) reaches the wake check at
2622 and makes progress.
Re-derived in this slot:
707/boot01(docs/planning/green-program/sockets/serials/707-2026-09-02/x86-battery-r2/boot01/serial_kernel.txt,the issue's own #764 specimen): the reader (also thread 30) is woken at
2303, then restored seven times before it observes the wake:
Six of those seven restores are wasted; the seventh (2755) reaches the check
at 2757 and the read completes.
Re-derived in this slot:
The 707-battery turn census
docs/planning/green-program/sockets/serials/764-rca/summarize.py, run overall 25 boots of
707-2026-09-02/x86-battery-r2/, counts a thread's restoresbetween its wake and the line where its
sys_readwait loop observes it(
resumescolumn). 24 of 25 boots have a countable value (boot14 has noaccept-block line for the port and is excluded):
data_latency_msRe-derived in this slot:
data_latency_mstracks this turn count, not queue length or a uniformper-pass cost -- 39 of 46 boots in the separate 76-boot battery's census
consume the wake on turn one, as expected if a dispatched reader normally
exits its wait loop immediately. The multi-turn boots are the outliers this
issue is about.
Why this isn't already explained
A dispatch of the reader is a full context-switch cycle: a restore
(
kernel/src/interrupts/context_switch.rs:962),set_running()(
kernel/src/task/scheduler.rs:2103), and a fresh quantum(
kernel/src/interrupts/context_switch.rs:436).sys_read's wait loopbreaks the moment
thread.state != Blocked(
kernel/src/syscall/handlers.rs:1403-1410):So once
unblock()has run (set_ready()atscheduler.rs:2897, theAdded to per_cpu_queueslog at:2951), a subsequent restore of the samethread should read
Blockedas false and take the!still_blockedarm onits very first dispatch. It does not, five and six times respectively on the
two specimens above -- and a restore followed immediately by a save at the
same RIP/RSP looks identical in the serial log whether
still_blockedranand (correctly or incorrectly) read
Blocked, or the thread executed noinstruction from
still_blockedat all before yielding again. The serialsalone cannot distinguish those two cases.
Candidate reading
Not concluded, just the shape worth checking:
set_ready()atscheduler.rs:2897andset_running()atscheduler.rs:2103are twoseparate writes to thread state, on two different sides of a dispatch. If
there is a window in which a thread is dispatched (restored,
set_running()called) but its
statefield has not yet settled to a value distinguishablefrom
Blockedat the pointstill_blockedreads it -- or conversely, ifstill_blockedcan read a state that flips back toBlockedbetween theread and the
yield_current()/halt on some other iteration -- either wouldproduce exactly this shape: N restores that make no progress, then one that
does.
Proposed experiment (per CLAUDE.md: tracing framework, not log lines)
A
trace_count!(ortrace_event!) on thestill_blocked == truebranch ofthe loop at
handlers.rs:1403-1410, read back through the lock-free tracingframework (
kernel/src/tracing/,trace_dump()/trace_dump_counters()via GDB or
/proc/trace/*) rather than through a log line, so the hotcontext-switch/syscall-wait path is not disturbed. If the counter increments
on the wasted restores,
still_blockedis readingBlockedcorrectly oneach of them, and the question becomes why the scheduler keeps
re-dispatching a thread whose own wait loop has no work to do until turn N;
if it does not increment on some of the wasted restores, the thread is
failing to reach or execute that check at all on those restores, which
points at the dispatch path instead.
Related
repro-boot-057, andthe issue's own
707/boot01specimen); re-attributed to the scheduler rowas one presentation of x86: a timer wake dispatches only after a full round robin - measured sleep overrun p90 2592 ms, max 10318 ms over 324 trials #766's dispatch-latency mechanism, but the wasted
turns inside that wait are a distinct, unexplained sub-mechanism.
turns are inside: x86: a timer wake dispatches only after a full round robin - measured sleep overrun p90 2592 ms, max 10318 ms over 324 trials #766 predicts one round robin (one turn) after a tail
re-enqueue (
scheduler.rs:3731); the two specimens above take six andseven turns respectively, which x86: a timer wake dispatches only after a full round robin - measured sleep overrun p90 2592 ms, max 10318 ms over 324 trials #766's stated mechanism does not predict.