Files
scx_soryu/docs/cpu-release.md
T
2026-09-04 23:53:20 +03:00

3.8 KiB

cpu_release: a blind spot, an answer, and a knob that changed nothing

The interactive tail on an idle-ish desktop is consistently worse than during a full compile -- 0.29% past 1 ms against 0.15% -- which is backwards, and the one number that tracked it was cpu_release: about 130 a second on the desktop against 3 during a compile.

The instrument was blind to it

scx_bpf_reenqueue_local() throws the local DSQ back through ops.enqueue with SCX_ENQ_REENQ. ops.runnable is not part of that path, so wake_at is never stamped, and ops.running records a latency sample only when wake_at is set.

So every task bounced by a higher sched class spent that trip invisible. The tail that every decision on this branch was measured against excluded exactly the population under suspicion. bounce_hist was added to close that, stamped at SCX_ENQ_REENQ in ops.enqueue and read at ops.running, recorded alongside the wake sample rather than instead of it.

What takes the CPU

ops.cpu_release now splits by enum scx_cpu_preempt_reason.

releases rt stop dl
compile, 6 min 662 307 355 0
desktop, 5 min 38916 38775 141 0

Desktop is 99.6% RT, at roughly 130 releases a second, each bouncing 0.98 tasks. No deadline tasks at all.

Bounced tasks wait about twice as long

wake->run p99 bounce->run p99 bounce share
compile 655 us 786 us 0.1%
desktop 655 us 1.3 ms 1.8%

Real, and too small to matter. At 1.8% of samples with a p99 of 1.3 ms, bounces contribute roughly 18 events past 1 ms per interval against 193 from the wake population -- about 9% more tail, not the doubling that needed explaining.

So the answer is the boring one

The desktop's non-bounced population is already worse: wake->run p99.9 of 1.45 ms against 1.00 ms during a compile. RT preemption at 130 a second does not mainly hurt the task it bounces, it steals CPU from everyone and raises queueing delay across the board.

That is RT working as designed. The scheduler has no move here.

The knob, and its null

If the RT burst is short -- an audio callback is tens of microseconds -- then re-enqueueing is waste: the task pays a full trip through ops.enqueue, level recomputed and DSQ reinserted, to avoid a wait it would have outlasted. --reenqueue-on-release=false tests that.

intervals cpu_release/iv enq_reenq tail >1 ms p99 p99.9 max_wait
true 14 1138 15580 0.368% 655 us 1.60 ms 32 j
false 30 897 0 0.410% 655 us 1.60 ms 22 j

Paired: false better in 4 intervals, true in 10, sign test p = 0.18.

The knob works -- enq_reenq and the bounce histogram both go to zero -- and changes nothing measurable. p99 and p99.9 are bit-identical. This was predicted before the run, on the arithmetic above, and the prediction is recorded here because on this branch it is the third or fourth hypothesis to be tested and the first to survive.

The comparison is weak on its own terms: the two runs differ by 27% in release rate and by more than double in length, because desktop activity is not reproducible the way a compile is. It is not worth strengthening. The prior and the data agree, and they agree on "no effect".

Default

reenqueue_on_release = true. It is the conventional behaviour, it is what the paired direction leans toward, and it is the safe side of a risk this test did not exercise: with it off, tasks in the local DSQ of a CPU taken by a long RT or DL task cannot migrate away, and starve_ns walks the shared level DSQs, not local ones. Short audio bursts are fine. An RT hog would be a 5 s watchdog ejection.

bounce_hist and the reason split stay regardless. They cost nothing and they closed a hole in the instrument that had been open the whole time.