Skip to content

[COD-2349] feat(callgrind): track subprocesses across fork and exec#25

Open
GuillaumeLagrange wants to merge 2 commits into
cod-3196-fix-valgrind-separate-threadsyes-behavior-to-represent-osfrom
cod-2349-support-subprocesses-in-valgrind
Open

[COD-2349] feat(callgrind): track subprocesses across fork and exec#25
GuillaumeLagrange wants to merge 2 commits into
cod-3196-fix-valgrind-separate-threadsyes-behavior-to-represent-osfrom
cod-2349-support-subprocesses-in-valgrind

Conversation

@GuillaumeLagrange

Copy link
Copy Markdown

Track subprocesses spawned by a benchmark so they can be measured and
attributed back to the benchmark that spawned them, across both fork and
exec.

Two independent pieces, one per commit:

1. Forward instrumentation state across a traced exec. A process spawned
via exec under --trace-children=yes gets a fresh valgrind whose
instrumentation state resets to --instr-atstart, so a benchmark reached
through an exec chain (e.g. cargo run) was measured from the wrong point. A
plain fork inherits the state with the address space, but an exec does not.
This adds a VG_(needs_child_exec_args) tool need: the core asks the tool for
extra valgrind arguments while building the child's argv at exec time and
appends them after VG_(args_for_valgrind) (later options win). Callgrind uses
it to forward --instr-atstart=<current state>. Wired into the exec argv
construction on Linux (generic), Darwin, and Solaris.

2. Record spawned subprocesses per dump part. When a benchmark spawns a
subprocess, the profile had no way to attribute that subprocess to the exact
benchmark (dump part) that spawned it. On fork the parent now records the new
child pid against the part currently being measured, and each part's header
lists the children spawned during it as desc: Spawned pid: lines (a desc
field so kcachegrind and callgrind_annotate ignore it). A fork child restarts
its part counter at 1 and drops the inherited edges so it only reports children
it spawns itself, and zeroes cost so work before the fork stays attributed to
the parent.

To carry the child pid to the fork hook, VG_(atfork)'s parent callback now
takes the pid of the just-created child; the pre/child callbacks are unchanged.
All fork/clone sites (Linux, FreeBSD, Solaris, generic) pass it through. The
child hook also re-stamps the in-flight fork syscall's start time from the
child's clocks, since the thread CPU clock restarts in the child and the exit
delta would otherwise underflow.

The spawn edge is recorded parent→child rather than child→parent: at fork time
the parent already knows both the child pid and its own live part, so no spawn
identity needs to cross the exec boundary — only the instrumentation state
does. This is a change from the earlier squashed design that emitted
desc: Spawned by: <pid>:<part> from the child; see the COD-2349 discussion for
the full rationale.

Stacked on top of #24 (COD-3196, per-thread dump support), which this builds
on for the retired-thread handling in the fork/exec paths.

Behavior verified with fork/exec integration tests in the valgrind-helpers repo
(subprocess measured and recorded across fork, across a traced exec, through a
deep fork tree, and one child recorded per part across a two-part run).

Closes COD-2349

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR tracks benchmark subprocesses across forks and traced execs. The main changes are:

  • Forwards Callgrind instrumentation state to traced exec children.
  • Records spawned child PIDs against the active dump part.
  • Resets inherited counters, spawn records, and costs in forked children.
  • Preserves retired thread data for per-thread dumps.
  • Passes child PIDs through the parent at-fork callback.

Confidence Score: 5/5

This looks safe to merge.

  • Spawn records are removed only after all thread sections for the part are written.
  • Empty parts force an output section before their spawn records are removed.
  • Output-open failures terminate before cleanup can discard unwritten records.
  • No blocking issue was found in the updated cleanup path.

Important Files Changed

Filename Overview
callgrind/dump.c Adds per-part child records, retired-thread output, empty-part handling, and cleanup after successful part processing.
callgrind/main.c Registers fork and exec hooks, records child PIDs, and resets inherited child profiling state.
callgrind/threads.c Adds stable kernel thread identities and retains exited-thread data until it is dumped.
coregrind/m_libcproc.c Extends parent at-fork callbacks to receive the new child PID.
coregrind/m_syswrap/syswrap-generic.c Appends tool-provided arguments when constructing traced exec command lines.

Sequence Diagram

sequenceDiagram
    participant Parent as Callgrind parent
    participant Core as Valgrind core
    participant Child as Fork or exec child
    participant Dump as Profile dump

    Parent->>Core: fork or clone
    Core-->>Parent: parent hook with child PID
    Parent->>Parent: Record PID for next dump part
    Core-->>Child: child hook
    Child->>Child: Reset part counter, spawn records, and costs
    alt traced exec
        Core->>Parent: Request child exec arguments
        Parent-->>Core: Current instr-atstart state
        Core->>Child: Exec with forwarded state
    end
    Parent->>Dump: Write dump part
    Dump->>Dump: Emit spawned PID metadata
    Dump->>Dump: Remove emitted records
Loading

Reviews (3): Last reviewed commit: "feat(callgrind): record spawned subproce..." | Re-trigger Greptile

Comment thread callgrind/dump.c
@codspeed-hq

codspeed-hq Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 84 untouched benchmarks
⏩ 60 skipped benchmarks1


Comparing cod-2349-support-subprocesses-in-valgrind (ce72315) with cod-3196-fix-valgrind-separate-threadsyes-behavior-to-represent-os (5cae7e2)

Open in CodSpeed

Footnotes

  1. 60 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@GuillaumeLagrange
GuillaumeLagrange force-pushed the cod-2349-support-subprocesses-in-valgrind branch from ce72315 to 69b355e Compare July 22, 2026 21:23
@GuillaumeLagrange
GuillaumeLagrange force-pushed the cod-3196-fix-valgrind-separate-threadsyes-behavior-to-represent-os branch from 5cae7e2 to bf37129 Compare July 22, 2026 21:23
GuillaumeLagrange and others added 2 commits July 22, 2026 23:56
A process spawned via exec under --trace-children=yes gets a fresh
valgrind whose instrumentation state resets to --instr-atstart, so a
benchmark reached through an exec chain (e.g. `cargo run`) was measured
from the wrong point. A plain fork inherits the state with the address
space, but an exec does not.

Add a VG_(needs_child_exec_args) tool need: the core asks the tool for
extra valgrind arguments while building the child's argv at exec time
and appends them after VG_(args_for_valgrind), where later options
override earlier ones. Callgrind uses it to forward
--instr-atstart=<current state> so the exec'd image is instrumented from
its first instruction. Wired into the exec argv construction on Linux
(generic), Darwin, and Solaris.

Closes COD-2349
Co-Authored-By: Claude <noreply@anthropic.com>
When a benchmark spawns a subprocess, the resulting profile had no way to
attribute that subprocess back to the exact benchmark (dump part) that
spawned it, so the spawn tree could not be rebuilt.

Track fork edges and emit them in the dump: on fork the parent records
the new child pid against the part currently being measured, and each
part's header lists the children spawned during it as "desc: Spawned
pid:" lines (a desc field so kcachegrind and callgrind_annotate ignore
it). A fork child restarts its part counter at 1 and drops the inherited
edges so it only reports children it spawns itself, and zeroes cost so
work before the fork stays attributed to the parent.

To carry the child pid to the fork hook, VG_(atfork)'s parent callback
now takes the pid of the just-created child; the pre/child callbacks are
unchanged. All fork/clone sites (Linux, FreeBSD, Solaris, generic) pass
it through. The child hook also re-stamps the in-flight fork syscall's
start time from the child's clocks, since the thread CPU clock restarts
in the child and the exit delta would otherwise underflow.

Closes COD-2349
Co-Authored-By: Claude <noreply@anthropic.com>
@GuillaumeLagrange
GuillaumeLagrange force-pushed the cod-2349-support-subprocesses-in-valgrind branch from 69b355e to 002990c Compare July 22, 2026 21:56
@GuillaumeLagrange
GuillaumeLagrange force-pushed the cod-3196-fix-valgrind-separate-threadsyes-behavior-to-represent-os branch from bf37129 to bb4fb76 Compare July 22, 2026 21:56
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