Repository navigation
Split off-CPU intervals into sleeping and run-queue time - #3
Merged
Merged
Conversation
A blocked interval runs from switch-out to switch-in, so it held two waits: the sleep until the wakeup, and the run-queue delay until the next turn on a CPU. Each interval now records how long it waited on a run queue, read from the scheduler's own per-task accounting: the switch-out hook saves task_struct.sched_info.run_delay and the switch-in hook reads it again. That is two field reads in hooks that already run; nothing new fires system-wide, as a sched_wakeup hook would. - New timeSplit block beside sampling, resolved, echoed and compared the same way: source schedInfo (default) or off. schedInfo fails closed at prepare on a kernel whose BTF lacks the field; off is the explicit mode for such a kernel. - Observations carry runqueue_nanos (capture proto field 21) and the control schema moves to version 4; versions 2 and 3 are still read, as unsplit. The kernel drops a reading only when the counter went backwards and counts runqueueInversions. - The consumers apply one rule: a blocked interval's run-queue part is its tail, a runnable or preempted interval is run-queue time throughout, and an interval without a usable reading is unsplit, never guessed or clamped. Each part is clipped to the analysis window, so the parts add up to the duration. - The report gives sleeping, run-queue and unsplit time per reason and a timeSplit object. The stack profile fills its reserved split fields and adds the unsplit ones, with estimated parts floored cumulatively so they add up. stacks gains --time total|sleeping|runqueue|split, and export the split columns. Default outputs are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A
blockedinterval runs from switch-out to switch-in, so it holds two different waits: the thread's sleep until its wakeup, and then its wait on a run queue until it gets a CPU. PR #2 recorded why each interval began and left this split as future work, with capture proto field 21 and profile entry fields 20–23 reserved for it. This PR fills them in:How it is measured
The scheduler already keeps each task's total run-queue wait in
task_struct.sched_info.run_delay, the second field of/proc/<pid>/schedstat. The switch-out hook saves it and the switch-in hook reads it again; the difference is that interval's run-queue time.CONFIG_SCHED_INFO, whether or not delay accounting or schedstats are switched on at runtime. This was checked in the kernel source for v6.13 and master:enqueue_task(the same activation that precedessched_wakeup), or atsched_info_departfor a task that leaves the CPU still runnable;prepare_task_switch, before our switch-in hook;sched_wakeuphook would fire for every wakeup on the host; it is left for a future waker-attribution source, and it would have to besched_wakeup, notsched_waking, which can fire before the switch-out.__schedulekeep a sleepingprev_statewithout dequeuing the task.sched_infocounts that interval as run-queue time throughout, which is what actually happened.What changes
Configuration and capture
timeSplitblock besidesampling:source: schedInfo(the default) oroff. It is resolved, echoed by the native side, written into the manifest,captureStartandanalysisInputs, and compared structurally, likesampling. It is a separate block because it changes what is measured, not which intervals are kept.schedInfofails at prepare on a kernel whose BTF lackssched_info.run_delay, and the error namestimeSplit.source: off. There is no silent fallback.time-split=schedInfo|off.bpf_core_field_exists, so it still loads on a kernel without them.runqueue_nanos(field 21), and the control schema moves to version 4. The kernel drops a reading only when the counter went backwards, and counts it inrunqueueInversions. The ring record grows from 128 to 136 bytes.off.The split rule, applied by the consumers from the raw value:
[end − runqueue, end], and the rest is sleeping.off;Correlator
offCpuReasons.matched.<reason>gainssleepingNanos,runqueueNanosandunsplitNanos. A newoffCpuReasons.timeSplitgives the source, whether the split is available, the rule, unsplit intervals by cause, andrunqueueInversions.unsplit_nanosandestimated_unsplit_nanos(24–25) plus provenancetime_split_json. The change is additive and the profile schema stays 1.estimated_nanos.mergejust sums.stacks --time total|sleeping|runqueue|split:splitkeeps the whole time and ends each line in a[sleeping],[runqueue]or[unsplit]frame, so one flame graph shows both waits.totalrefuse a profile without the split.--summaryaddstimeandunsplitNanos.exportadds the six split columns.Docs: the README ("Why the thread left the CPU", the agent options, the overhead table, "Slice and filter"), OFFLINE.md (a new "Sleeping and run-queue time" section, the profile, retention), the AGENTS.md contract, and the agent and native READMEs.
A finding worth knowing
A runnable or preempted interval's reading can exceed its duration by a few microseconds, because
rq_clockcan be stale when a running task leaves the CPU (sched_yield, for example, updates it before callingschedule()). In the proof this happened to about 3 % of such intervals, by at most 26 µs; no blocked interval overshot. The first version dropped these readings in the kernel (82 of 6,830). Now the kernel passes the raw value through, and the rule treats runnable and preempted intervals as run-queue time regardless.Verification
Gate: the kernel proof.
run-offcpu-reason-proof.shis extended and passes on the 16-CPU 7.1.5 host, withtask_delayacct=0andsched_schedstats=0:/proc/<pid>/task/<tid>/schedstatrun delay; for the uncontended sleeper they match to the nanosecond (251,114 ns).Overhead. A pipe ping-pong in the target, pinned to one CPU, with admission set to zero so that only the hooks run:
schedInfoagainstoffmeasured +18 ns and −17 ns per round trip in two runs, on about 3,000–3,400 ns with the hooks attached. That is within noise.Tests
./gradlew spotlessCheck :jonoffcpu-agent:check :jonoffcpu-correlator:check,cargo fmt --check, and the native unit tests pass (including struct layout,timeSplitparsing and record encoding).--timeslices that add up to the total, a canonical profile and exact estimated parts;offrows with readings rejected;timeSplitmismatches rejected.run-packaged-agent-smoke.py --libc muslpasses on x86-64 with new assertions:timeSplitis{"source":"schedInfo"};--time splitrenders back to the total.is_switch=falsecallbacks) and passed twice on rerun; that check is untouched by this change.Pulsar broker capture (version 2),
mainagainst this branch on the same machine:--time runqueueis refused with a clear message.Compatibility
timeSplitresolves toschedInfo. On a kernel withoutCONFIG_SCHED_INFO, a configuration that worked before now fails with a message namingtimeSplit.source: off. Mainstream distribution kernels enable it throughTASK_DELAY_ACCTorSCHEDSTATS.