Repository navigation
A thread's request follows its bursts, so a brief sleeper is due before a hog - #1266
Conversation
Review of #1266 at 578c9dfArchitecture. The policy sits where it belongs. The estimator is one pure function in the run queue's module ( Size.
What I was asked to judgeNo share change: holds by construction.
The estimator.
The second commit: verified neutral in the kernel.
The manual text. It names no code. It agrees with the implementation and with the bullet above it. The observation (a third of the CPU). This is expected EEVDF behaviour and not a scheduler defect. The PR's reason is wrong, though: the cause is the model, not a property of a job that naps every millisecond.
BLOCKER
SUGGESTION
PR description, for the orchestrator
Landing preconditions
SEND BACK: blockers 1 and 2. Both are cheap and neither touches production code. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8f2d2db to
338d134
Compare
Review of #1266, round 2, at 338d134 (code-identical to 8f2d2db)Scope: round 1's findings, the run-out recovery the orchestrator added, and the commits since 578c9df ( Architecture. The recovery sits where the estimator already lives:
Requests only grow on this path, so Round-1 blockers
The orchestrator's run-out decision: implemented as asked. Manual: it matches the decision. A request follows recent runs within the minimum and the full slice, a group takes its shortest thread's request, and a request changes order, never share. That is the EEVDF latency knob. Reading "its recent runs" to include the current one covers the run-out recovery, so no change is needed. Owed on metal: recorded in
Checks I ran at 338d134 (detached worktree, host toolchain):
BLOCKER
SUGGESTION
PR description, for the orchestratorNothing further. The mutation table's rows match Landing preconditions
SEND BACK: blockers 1 and 2. Each is a few lines. |
Review of #1266, round 3, at 9a1a013Scope: 9a1a013, the one commit since 338d134, which is its parent. It touches three files, +8/−7. The only production change is the condition in Architecture. Unchanged. The task still computes its request and the queue still applies it. The fix only stops telling the queue about a request that did not move. Round-2 blockers
The The run-out recovery test still covers both halves. I mutated each half in my scratch worktree, ran the test alone, and restored the tree:
The fix itself needs no control. It changes no decision, which the inspection above establishes, and its effect is only work the pass no longer does. Nothing else changed. Checks I ran at 9a1a013 (detached worktree, host toolchain):
Guest run 43097843657:
So this is a GitHub-side failure, not this branch. But no guest has yet run the run-out recovery: BLOCKERNone. SUGGESTION
PR description, for the orchestratorThe guest-rows row reads "queued". 43097843657 has ended in Landing preconditions
LAND, once the guest rows at 9a1a013 are green. No code change is required. |
47091b4 to
817938d
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
9a1a013 to
4534ce3
Compare
…re a hog The manual promises that a thread that sleeps and runs briefly gets the next slice without more total time. Every thread and group asked for the same 4 ms request, so a sleeper that ran a quarter millisecond while the hogs waited came back owing for it, was neither eligible nor due before the hogs' freshly reissued request, and waited that whole request out. In 400 seeds of `kernel-sim`'s interactive workload at four hogs, 12 runs had such a second wake. A thread's request now follows its bursts, the CPU time from a wake to its next park: each burst, bounded by the minimum and the full slice, moves the request half way to it. A thread that never sleeps keeps the full slice. A group asks for its shortest thread's request. Requests order EEVDF's queue and do not weigh in it, so shares do not move: a new simulator workload, a job of one or four bursty threads against a hog and beside a real-time job or not, finishes as it does on main, and a mutation that lets the shorter request weigh more reds it. `an_interactive_wake_waits_out_at_most_one_request` now sweeps 400 seeds, which finds the case on main; the fix has none. The core tests hold both wakes the runs showed, and the manual says how a request's length is chosen. The run queue's frozen reference and its differential test go, as their header said they would: this change decides differently by design. The division test stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With shorter requests a running thread's decision falls sooner, and in `kernel-sim`'s model of the old preemptible registration window a pass now reached a thread mid-commit through the decision arm. `preempt`'s assertion then fired with the task in hand, its drop guard aborted the unwind, and the gate that must catch the window took the whole test binary down. The kill and affinity arms already waited for the running word, `preempt`'s precondition; every arm now does, and the window is caught as before by the invariant walk's word check. In the kernel the running task's word reads `Running` at every pass, so nothing changes there. The request's mean is `u64::midpoint`, and the core wake tests drive their passes at the decision instants rather than the armed one-shot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s held from the first A thread whose bursts brought its request down and which then stopped parking kept that request for ever: up to sixteen times the decisions and timer interrupts of a full slice. At every pass a running thread's burst so far that has outrun its request now moves it as a park would, and its group's request follows; such a thread is back within 1% of the full slice a dozen requests on. `a_brief_sleeper_owing_its_last_burst_runs_within_the_minimum_run` skipped the first two wakes, and the first is the one owing for its burst that main gets wrong; it asserts every wake now. The request-share comments say what the model's nap is, a whole run chunk, and `RUN_CHUNK_NS` records that the model resolves nothing shorter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s comment is true A thread that never parks outruns its request at every pass once it has run a whole slice, and the request then stays where it is: the pass still set the thread's and its group's slice and walked the group's threads for nothing, on the path a yield loop measures. The request now moves, and the queue is told, only when `next_request` changes it. The request-share workload's comment no longer says the bursty job's finish is its work plus its time parked; it waits while runnable too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4534ce3 to
e24deb8
Compare
|
Orchestrator: landing under the benches-down rule. Review r3 LAND (9a1a013). Head e24deb8 is the same five commits rebased onto main after #1260 ( |
Draft, on main now that #1260 has landed. #1263 is rebased onto it. Head e24deb8, the commits of 9a1a013 rebased onto main after #1260 unchanged. The metal A/B is owed (below), the benches being down.
Problem
The manual promises that "a thread that sleeps and runs briefly gets the next slice without more total time". Every thread and every group asked for the same 4 ms request, and
kernel-sim's interactive workload (a sleeper running a quarter millisecond every 3 ms beside hogs) showed the cost. In 400 seeds at four hogs, 12 runs had a second wake wait out a whole hog request, 4 to 5 ms against the test's 2.25 ms contract. The test's 40 seeds had not reached the case. Every breach has the same shape:The first wake of each run had the same tie with no debt. Evidence:
policy/in https://github.com/ToyOSOrg/evidence/tree/444e752b2d490116c1d0d9f9144261c63fe39f79, recorded on #1263's branch asissues/a-wake-landing-as-the-running-request-ends-waits-a-whole-request.md. That record is closed here: #1263's rebase drops it.What changes
The owner's decision on this defect, option (b): a thread's request follows its recent run, never below the minimum slice, at both levels.
queue::next_request). A burst is the CPU time from a wake to the next park, taken off the task's own accounting (acct.cpu_nsat park, less its value at the wake). Each burst moves the request half way to it, the burst bounded to the minimum and the full slice.RunningTask::outrun_request,RunQueue::set_running_request). A thread that slept briefly and then turns CPU-bound is back within 1% of the full slice a dozen requests on, instead of keeping a 250 µs request, and up to sixteen times the timer interrupts, for ever.RunQueue::resize, at each thread's entry and departure): it is due as soon as its most latency-sensitive thread.eevdf's owna_shorter_request_changes_order_not_shareholds it.Manual: the scheduling page now says how a request's length is chosen and that it changes order, not share.
Removed:
queue/reference.rsandqueue/differential.rs, as their header said: this change decides differently by design.eevdf's division test stays.Proof
Latency:
an_interactive_wake_waits_out_at_most_one_requestnow sweeps 400 seeds, about 10 s.kernel-sim run interactive_mix:1:4: 12 runs breach at The run queue's operations cost less and decide as before #1260's head 9e31612 (main's scheduling, the control), 0 here.a_brief_sleeper_waking_as_the_running_request_ends_runs_at_once: a sleeper with no lag runs in the pass that reissues the hog's request.a_brief_sleeper_owing_its_last_burst_runs_within_the_minimum_run: a sleeper owing for its burst runs within the minimum run of every wake, the first included. Under main's policy its waits read[4000000, 0, 0, …]: the first, owing wake waits out the hog's whole request.a_thread_that_stops_sleeping_gets_back_to_the_full_slice.a_wake_with_no_claim_ahead_waits_for_the_running_requeststated the old rule. It is nowa_brief_sleepers_wake_runs_after_the_minimum_run.Share (
a_short_request_buys_no_share, new simulator workloadrequest_share):Why the one-thread bursty job gets about a third of the CPU, on main and on this head alike: not the scheduler. The model runs a
Runin 1 ms chunks and fires a timer due inside a chunk at its end (RUN_CHUNK_NS), so each 50 µs nap lasts a whole chunk of the hog's: the thread is runnable two thirds of the time and gets half of that. On hardware the nap is 50 µs plus the 10 µs one-shot floor.RUN_CHUNK_NS's doc now records that the model resolves nothing shorter than a chunk.Negative controls, each a mutation applied to the head 8f2d2db, run against the named tests alone (a core test that panics aborts its binary, so each core test runs by itself) and restored:
a_brief_sleeper_owing…, waits[4000000, 0, …]; the 400-seed interactive testa_brief_sleeper_owing…, waits[4000000, 0, …]; the 400-seed interactive testa_thread_that_stops_sleeping…, stuck at 264,648 nsa_short_request_buys_no_shareChecks
Logs and the proofs' readings: https://github.com/ToyOSOrg/evidence/tree/ff32bf2c3f600cff86bed03b8088c72e3a4ca1d0.
cargo test -p kernel --lib, and withsched-checkbuild/clippy.rs, andkernel-sim --all-targetscargo run -- --ci host modelskernel-sim's policy and scenarios andkernel-loomdrive this queuestd_threads_count_the_cpus_and_give_back_ended_threads,affinity_pins_hold_on_every_cpu,vector_state_survives_a_switchA second commit: a pass preempts only a task whose word says it runs
With shorter requests a running thread's decision falls sooner. In
kernel-sim's model of the old preemptible registration window, a pass then reached a thread mid-commit through the decision arm, andpreempt's assertion fired with the task in hand. Its drop guard turned the unwind into an abort, so the gate that must catch the window took the test binary down instead of reporting it.The kill and affinity arms already waited for the running word,
preempt's precondition. Every arm now does, and the window is caught as before by the invariant walk's word check (a_pass_inside_the_registration_window_is_caught, green). In the kernel the running task's word readsRunningat every pass, so nothing changes there.Owed on metal
Both metal benches are down, so metal is no longer a landing condition here. The speed-up is not yet measured on metal. Recorded as owed:
sched,fairandtime, with a Linux reference timed through the TSC (issues/a-wake-onto-a-busy-cpu-…): main must read thewake.*rows faster or unchanged, thefair.*share rows within 3%,time.yieldwithin 3%, and no scheduler, wake, timer or fairness row slower. The latency change is measured in the simulator only.Risks
🤖 Generated with Claude Code