Skip to content

Record CPU per request for the fcvm VM arm and the host-container control - #1047

Open
ejc3 wants to merge 2 commits into
bench/hostcdp-in-processfrom
bench/request-cpu
Open

ejc3 wants to merge 2 commits into
bench/hostcdp-in-processfrom
bench/request-cpu

Conversation

@ejc3

@ejc3 ejc3 commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Stacked on: bench/hostcdp-in-process (#1046)

The render benchmark page says CPU per request is not measured. The fast-teardown arm already read each clone child's utime+stime at the kill, and the CPU each child spent being reaped after it. Nothing reduced those readings, and nothing attributed the shared memory server's CPU to a request. The host-container control recorded no CPU at all.

Contract:

  • Serial reqbench records carry the memory server's CPU for each request.
  • reqanalyze publishes CPU per request for the fast-teardown arm.
  • The host control records its container's CPU counters for each rep and summarises them.
  • Concurrent reqscale records carry no per-request server CPU.

Downstream impact: new fields in records and summaries, and the host control now refuses a container without a CPU counter. The latency fields are unchanged.

Changes

reqbench.py

  • serve_cpu() takes the memory server's utime+stime between a clone's launch and the end of its teardown. Requests run one at a time, so that delta is the request's share.
  • A server whose start time changed, or that can't be read, is recorded as an error, not as zero. A file-backed restore has no server.

reqanalyze.py

  • arms[<fast arm>].request_cpu_ms gives bootstrap medians of the total, which is the children at the kill plus their reaping plus the memory server.
  • It also gives a median for each of those parts, and for each child at the kill.
  • It counts the totals that include a reaping the reaper raced, which makes them lower bounds.

reqscale.py

  • Its requests overlap, so it drops serve_cpu from its records.

hostcdp.sh

  • Each rep records the container cgroup's cpu.stat usage_usec before and after drive().
  • The summary adds container_cpu_ms: the median and mean over each drive() window, and the run average.
  • The run average is everything the container used from the first measured rep's start to the last one's end, divided by the measured count. A clone's CPU is its whole one-request life, so the run average is the figure comparable to it.
  • A container without the counter is refused. CGROUP_ROOT locates the cgroup, and tests point it at a fixture.

Evidence

Each new test was observed failing on the parent:

  • ServeCpu (4 tests), and serve_cpu on run_cdp_request's record.
  • AnalyzerAvailability.test_request_cpu_sums_children_reaping_and_the_memory_server
  • HostCdpContainerCpu (the counters and summary, and the refusal).
  • ConcurrentRequestRecords.test_the_memory_server_cpu_is_not_charged_to_overlapping_requests
$ python3 -m unittest discover -s bench/chromium -p 'test_*.py'
Ran 1060 tests in 253.652s
OK

That run preceded the reqscale change; test_reqscale passes after it.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 2ea20cad-40d5-4a71-ba95-df95a532d9fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T03:13:39.977031Z 3dd8c2e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3dd8c2e180

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bench/chromium/hostcdp.sh Outdated
Comment on lines +1089 to +1090
CPU_STAT="${CGROUP_ROOT:-/sys/fs/cgroup}${container_cgroup}/cpu.stat"
grep -q '^usage_usec ' "$CPU_STAT" 2>/dev/null \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reject empty or root container cgroup paths

When Podman reports an empty or / State.CgroupPath, such as when no per-container cgroup is available, this resolves to the root cgroup's cpu.stat. That file normally contains usage_usec, so the guard passes and every request is charged CPU from the entire shared host rather than from the container, corrupting the host-control comparison. Validate that the path is absolute, nonempty, and not / before reading it.

AGENTS.md reference: bench/chromium/AGENTS.md:L336-L338

Useful? React with 👍 / 👎.

Comment thread bench/chromium/reqanalyze.py Outdated
Comment on lines +2347 to +2351
block = {
"total": metric_summary(by[a], total),
"children_at_kill": metric_summary(by[a], parts("children")),
"reclaim": metric_summary(by[a], parts("reclaim")),
"memory_server": metric_summary(by[a], parts("serve")),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Carry tick uncertainty into CPU totals

On normal CLK_TCK=100 runs, the child-at-kill, reclaim, and memory-server values are all derived from tick-quantized /proc/<pid>/stat readings, but this reduction sums their point values and publishes only a bootstrap CI. It even discards the existing reclaim *_lo/*_hi bounds, so request_cpu_ms.total can imply sub-tick precision and support differences smaller than the measurement resolution. Propagate counter bounds into the total or explicitly publish the accumulated quantization interval.

AGENTS.md reference: bench/chromium/AGENTS.md:L729-L732

Useful? React with 👍 / 👎.

ejc3 added 2 commits October 2, 2026 03:31
…trol

The corpus runs published no CPU figure. The fast-teardown arm already read
each clone child's utime+stime at the kill and its reaping CPU after it, but
nothing reduced them, the shared memory server's CPU was attributed to no
request, and the host-container control recorded no CPU at all.

bench/chromium/reqbench.py:
- serve_cpu(): the memory server's utime+stime between a clone's launch and
  the end of its teardown. Requests run one at a time, so that delta is the
  request's share. A server whose start time changed, or that cannot be
  read, is an error, not zero; a file-backed restore has no server.
- run_cdp_request records it as serve_cpu on every completed request.

bench/chromium/reqanalyze.py:
- arms[<fast arm>].request_cpu_ms: bootstrap medians of the total (children
  at the kill + their reaping + the memory server), of each part, and of
  each child at the kill, plus a count of totals that include a reaping the
  reaper raced (a lower bound). Records missing any part are left out.

bench/chromium/reqscale.py:
- Its requests overlap, so the memory server's CPU over one request's window
  is not that request's; reqscale drops serve_cpu from its records.

bench/chromium/hostcdp.sh:
- Reads the container cgroup's cpu.stat usage_usec before and after each
  drive() and records both counters on the rep. A container without the
  counter is refused (exit 5), as is a rep whose counter cannot be read.
- summary.json container_cpu_ms: the median and mean per drive() window,
  and the run average, everything from the first measured rep's start to the
  last one's end divided by the measured count. A clone's CPU is its whole
  one-request life, so the run average is the comparable figure.
- CGROUP_ROOT (default /sys/fs/cgroup) locates the cgroup; tests point it at
  a fixture.

Tests, each observed failing on the parent:
- ServeCpu (4 tests) and serve_cpu on run_cdp_request's record.
- AnalyzerAvailability.test_request_cpu_sums_children_reaping_and_the_memory_server
- HostCdpContainerCpu: the per-rep counters and summary, and the refusal.
- ConcurrentRequestRecords: reqscale records carry no serve_cpu.
- The hostcdp harnesses' podman stubs answer .State.CgroupPath and their
  stub drivers advance a fixture cpu.stat by 2.5 ms per call.

Tested: python3 -m unittest discover -s bench/chromium -p 'test_*.py'
(1060 tests, OK, before the reqscale change); test_reqscale (OK) after it.
… the run, fail closed

From a Codex review of the previous commit. Per-request readings of the
memory server and of utime+stime missed work in two ways, and the reducer
published what it could read instead of refusing what it could not.

bench/chromium/reqbench.py:
- proc_cpu_ticks() adds cutime and cstime. The fast reap records them per
  pinned process as reaped_children_cpu_ms_by_child: fcvm's cp --reflink,
  nsenter and ip setup helpers have exited by the kill and were counted
  nowhere.
- The memory server is sampled cumulatively (its own and its reaped
  children's CPU, with its start time) before each request's launch and
  after its teardown, as serve_cpu_before / serve_cpu_after. The per-request
  delta is gone: the server finishes working-set publication and copy-mode
  warming after a clone exits, outside that clone's window, and CLK_TCK
  steps made a 4 ms/request server read 0 most of the time.

bench/chromium/reqanalyze.py:
- request_cpu_ms reports means with a bootstrap CI (mean_ci), the same
  statistic as the host control's run average: the clone's tree (children
  at the kill + their reaped children + reaping), its parts, each child, and
  the count of totals that include a lower-bound reaping.
- memory_server_average(): the server's cumulative growth from the first
  measured request to the last, divided by the count. Only when the run had
  one arm; a server shared with other arms, restarted, or missing a sample
  is reported as not attributable, and the total is then not published.
- One measured fast request without a complete reading withholds every CPU
  figure (complete: false, missing_records) instead of publishing the rest
  under a smaller n.

bench/chromium/hostcdp.sh: the container's cgroup path must be below the
root, canonical, and contain the container's main pid (podman inspect
.State.Pid) in its subtree. An empty path or "/" made the counter the root
cgroup's, every process on the machine.

bench/chromium/reqscale.py: drops the two server samples from its
overlapping requests' records.

Tests, each failing on the previous commit:
- ServeCpuSample (3), MemoryServerAverage (5)
- AnalyzerAvailability: test_request_cpu_counts_reaped_helpers_and_measured_lower_bounds
  (the lower-bound row is now a measured one) and
  test_one_incomplete_request_withholds_every_cpu_figure
- TeardownFastReapGuard.test_a_reaped_helpers_cpu_is_recorded: a child
  burns 0.3 s in a helper it reaps, then execs sleep
- CdpFailureIsLabelledOnTheRecord: the record's two server samples
- HostCdpContainerCgroup (3): root/empty, climbing, and pid-less cgroups
- HostCdpCpuSummary: the run average counts CPU between requests (a
  stronger check of an unchanged formula)

Tested: python3 -m unittest discover -s bench/chromium -p 'test_*.py'
(1073 tests, OK).
@ejc3
ejc3 force-pushed the bench/request-cpu branch from 3dd8c2e to 5404b32 Compare October 2, 2026 03:52

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant