runner: measure benchmark memory and CPU from the process subtree - #14
Merged
Conversation
The resource monitor read the pod-wide cgroup (`memory.current` / `cpu.stat`), so the reported "Peak memory" and "CPU" for a benchmark also counted page cache, leftover build artifacts, and — for benchmarks whose `bench.sh run` step compiles (e.g. `cargo bench --bench sql`) — the compiler itself. On a live wide_schema run that meant a single rustc (~16 GB, 12 cores) was being attributed to the benchmark. Scope both memory and CPU to the spawned benchmark command's process subtree by walking /proc, excluding compiler/build-tool processes (rustc, cargo, cc, ld, sccache, build-script-*, …): - memory: sum VmRSS over the subtree each second (peak/avg). - CPU: accumulate per-process utime/stime deltas over the window, so process churn (the bench binary starting after the compile finishes) is handled and the compile's CPU is excluded. shell.rs splits spawn from wait so the monitor gets the child pid. When no pid is available (or no /proc, e.g. local macOS) it falls back to the cgroup figures as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The self-subtree test ran on a single-threaded tokio runtime and spun for 1.2s, so the background poll task was starved (and CPU is a delta needing >=2 samples at the 1s poll interval) — it observed zero CPU and failed on Linux. Use a multi-thread runtime, spin >2s so at least two polls land, and guard the CPU assertion on having enough samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refines benchmark resource reporting by scoping memory/CPU measurement to the spawned benchmark command’s process subtree, avoiding inflated numbers caused by compiler/toolchain activity during bench.sh run (e.g., cargo bench compilation). It also refactors command execution helpers so the resource monitor can observe the spawned child PID while the command runs.
Changes:
- Refactor
shell.rsto split “spawn with logging” from “wait + collect logs”, enabling PID-aware monitoring. - Update the resource monitor to sample RSS and CPU from
/procfor the benchmark’s process subtree (excluding build-tool processes), with intended fallback behavior to cgroup stats when needed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| controller/src/runner/shell.rs | Refactors command execution to expose the spawned child PID to the monitor while preserving existing logging/collection behavior. |
| controller/src/runner/monitor.rs | Implements /proc-based subtree sampling (RSS + CPU) and updates the monitor’s accounting logic and tests accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+12
to
+15
| //! CPU stats still come from cgroup v2 `cpu.stat`. When the per-process root | ||
| //! pid is unknown the sampler falls back to the cgroup memory figure. When | ||
| //! neither `/proc` nor the cgroup files are available (e.g. local macOS | ||
| //! development), reads return `None` and stats default to zero. |
Comment on lines
+187
to
+193
| } else if root_pid.is_none() { | ||
| // Fallback path: keep memory sampling via cgroup. | ||
| if let Some(current) = read_memory_current() { | ||
| peak.fetch_max(current, Ordering::Relaxed); | ||
| sum.fetch_add(current, Ordering::Relaxed); | ||
| count.fetch_add(1, Ordering::Relaxed); | ||
| } |
Comment on lines
+233
to
+239
| // CPU: per-process subtree accumulation when we have a root pid (so the | ||
| // compile is excluded), otherwise the pod-wide cgroup delta. | ||
| let (cpu_user, cpu_sys) = if self.root_pid.is_some() { | ||
| ( | ||
| self.cpu_user_usec.load(Ordering::Relaxed), | ||
| self.cpu_sys_usec.load(Ordering::Relaxed), | ||
| ) |
Comment on lines
+266
to
+270
| /// Clock ticks (USER_HZ) to microseconds. Linux USER_HZ is 100 on all common | ||
| /// configurations, so a tick is 10 ms = 10_000 µs. | ||
| fn ticks_to_usec(ticks: u64) -> u64 { | ||
| ticks.saturating_mul(10_000) | ||
| } |
Address review feedback on #14: - Fall back to the pod-wide cgroup figures whenever `/proc` subtree sampling fails, not only when the root pid is unknown. A `proc_ok` flag tracks whether any `/proc` sample succeeded; memory and CPU both fall back to cgroup when it stays false (root pid unknown, or `/proc` unreadable in a restricted container). `sample_memory(Some(pid))` now also falls back to `memory.current` instead of returning `None`/0. - Take a final memory+CPU sample once stop is signalled, so CPU is captured up to the end of the run rather than as of the last full poll interval (was undercounting by up to POLL_INTERVAL with no final /proc read). - Read `_SC_CLK_TCK` via `libc::sysconf` (cached) instead of hard-coding USER_HZ=100, so CPU time is correct on non-100Hz kernels. - Update module docs to describe the subtree-or-cgroup behavior for both memory and CPU (CPU no longer "still comes from cgroup cpu.stat"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Owner
Author
|
Addressed the review feedback in 3407d2e:
|
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.
Problem
The resource monitor reads the pod-wide cgroup (
/sys/fs/cgroup/memory.currentandcpu.stat) over the whole monitored window. For benchmarks whosebench.sh runstep compiles (the Criterion SQL harness —cargo bench --bench sql), that window includes the compiler, so the reported "Peak memory" and "CPU" are dominated byrustc/cargo, not the benchmark.Observed live on a
wide_schemarun: the comment reported Peak memory 62.2 GiB and CPU user ~2942 s — that's the parallel build + LTO compile, not the queries (which use ~1 GB). The numbers are effectively measuring the toolchain.Fix
Scope both memory and CPU to the spawned benchmark command's process subtree, excluding compiler/build-tool processes (
rustc,cargo,cc,ld,sccache,build-script-*, …):VmRSSacross the subtree each second (peak/avg) by walking/proc.utime/stimedeltas over the window, so process churn (the bench binary starting after the compile finishes) is handled and the compile's CPU is excluded.shell.rsis refactored to split spawn from wait so the monitor receives the child PID. When no PID is available — or/procis absent (e.g. local macOS) — it falls back to the pod-wide cgroup figures as before, so behavior is unchanged off-cluster.Background
This change was written earlier but never landed: it was pushed to the
builds-no-debuginfo-by-defaultbranch after that PR (#11) had already been merged at the debuginfo-only commit, so only the debuginfo change reachedmain. This PR brings just the metric-scoping change in, cleanly on top of currentmain.Testing
cargo fmt --check,cargo clippy --all-targets -- -D warnings, andcargo test(138 tests, incl. unit tests for the/procparsing, build-tool classification, and a self-subtree memory/CPU smoke test) all pass.🤖 Generated with Claude Code