Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/memtrack/src/ebpf/c/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#define EVENT_TYPE_MMAP 6
#define EVENT_TYPE_MUNMAP 7
#define EVENT_TYPE_BRK 8
#define EVENT_TYPE_FORK 9

/* Common header shared by all event types */
struct event_header {
Expand Down Expand Up @@ -45,6 +46,13 @@ struct event {
uint64_t addr; /* address of mapping */
uint64_t size; /* size of mapping */
} mmap;

/* Process fork event - header.pid is the child, ppid the parent.
* Lets the parser rebuild the process tree and scope allocations to a
* benchmark process and its descendants. */
struct {
uint32_t ppid; /* parent pid that spawned header.pid */
} fork;
} data;
};

Expand Down
73 changes: 48 additions & 25 deletions crates/memtrack/src/ebpf/c/memtrack.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,31 @@ BPF_ARRAY_MAP(tracking_enabled, __u8, 1);
/* Counter for events that couldn't be added to the ring buffer */
BPF_ARRAY_MAP(dropped_events, __u64, 1);

/* Wake the consumer only once this much unconsumed data has accumulated.
* Per-event wakeups dominate submission cost at high event rates; batching
* them behind a data watermark amortizes the wakeup to ~1 per thousand
* events. The userspace poller's poll timeout flushes the tail that never
* reaches the watermark. */
#define WAKEUP_DATA_SIZE (64 * 1024)

static __always_inline long wake_flags(void) {
long avail = bpf_ringbuf_query(&events, BPF_RB_AVAIL_DATA);
return avail >= WAKEUP_DATA_SIZE ? BPF_RB_FORCE_WAKEUP : BPF_RB_NO_WAKEUP;
}

/* Helper to check if tracking is currently enabled */
static __always_inline int is_enabled(void) {
__u32 key = 0;
__u8* enabled = bpf_map_lookup_elem(&tracking_enabled, &key);

/* ARRAY-map lookups can't fail for a valid index; fail closed if one ever does. */
if (!enabled) {
return 0;
}

return *enabled;
}

/* == Code that tracks process forks and execs == */

/* Helper to check if a PID or any of its ancestors should be tracked */
Expand Down Expand Up @@ -87,6 +112,29 @@ int tracepoint_sched_fork(struct trace_event_raw_sched_process_fork* ctx) {
bpf_map_update_elem(&pids_ppid, &child_pid, &parent_pid, BPF_ANY);

// bpf_printk("auto-tracking child process: child_pid=%u", child_pid);

/* Emit a fork event so userspace can rebuild the process tree and scope
* allocations to a process plus its descendants. Gated on is_enabled()
* like allocation events, so only forks inside a measured region reach
* the ring buffer. header.pid carries the child: current here is the
* parent, so we cannot use the SUBMIT_EVENT macro. */
if (is_enabled()) {

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 Fork Edges Can Disappear

This gate can drop the process-tree edge that the new PID scoping depends on. The handler still adds the child to tracked_pids and pids_ppid before this branch, so a child forked while profiling is disabled can later emit allocation events after profiling starts, but the artifact never contains the matching Fork event. When the parser scopes events to the benchmark PID and descendants, that child has no recorded parent edge and its allocations can be treated as unrelated and dropped.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/memtrack/src/ebpf/c/memtrack.bpf.c
Line: 121

Comment:
**Fork Edges Can Disappear**

This gate can drop the process-tree edge that the new PID scoping depends on. The handler still adds the child to `tracked_pids` and `pids_ppid` before this branch, so a child forked while profiling is disabled can later emit allocation events after profiling starts, but the artifact never contains the matching `Fork` event. When the parser scopes events to the benchmark PID and descendants, that child has no recorded parent edge and its allocations can be treated as unrelated and dropped.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

struct event* e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) {
__u32 zero = 0;
__u64* drops = bpf_map_lookup_elem(&dropped_events, &zero);
if (drops) {
__sync_fetch_and_add(drops, 1);
}
return 0;
}
e->header.timestamp = bpf_ktime_get_ns();
e->header.pid = child_pid;
e->header.tid = child_pid;
e->header.event_type = EVENT_TYPE_FORK;
e->data.fork.ppid = parent_pid;
bpf_ringbuf_submit(e, wake_flags());
}
}

return 0;
Expand All @@ -99,31 +147,6 @@ int tracepoint_sched_fork(struct trace_event_raw_sched_process_fork* ctx) {

/* == Helper functions for the allocation tracking == */

/* Wake the consumer only once this much unconsumed data has accumulated.
* Per-event wakeups dominate submission cost at high event rates; batching
* them behind a data watermark amortizes the wakeup to ~1 per thousand
* events. The userspace poller's poll timeout flushes the tail that never
* reaches the watermark. */
#define WAKEUP_DATA_SIZE (64 * 1024)

static __always_inline long wake_flags(void) {
long avail = bpf_ringbuf_query(&events, BPF_RB_AVAIL_DATA);
return avail >= WAKEUP_DATA_SIZE ? BPF_RB_FORCE_WAKEUP : BPF_RB_NO_WAKEUP;
}

/* Helper to check if tracking is currently enabled */
static __always_inline int is_enabled(void) {
__u32 key = 0;
__u8* enabled = bpf_map_lookup_elem(&tracking_enabled, &key);

/* ARRAY-map lookups can't fail for a valid index; fail closed if one ever does. */
if (!enabled) {
return 0;
}

return *enabled;
}

/* Helper to store parameter value in map for tracking between entry and return
*/
static __always_inline int store_param(void* map, __u64 value) {
Expand Down
34 changes: 34 additions & 0 deletions crates/memtrack/src/ebpf/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ pub fn parse_event(data: &[u8]) -> Option<MemtrackEvent> {
size: event.data.mmap.size,
},
),
EVENT_TYPE_FORK => (
0,
MemtrackEventKind::Fork {
ppid: event.data.fork.ppid as i32,
},
),
unknown => {
panic!("Unknown event type: {unknown}");
}
Expand Down Expand Up @@ -185,4 +191,32 @@ mod tests {
_ => panic!("Expected Malloc event kind"),
}
}

#[test]
fn test_parse_fork_event() {
let mut event: bindings::event = unsafe { std::mem::zeroed() };
event.header.event_type = bindings::EVENT_TYPE_FORK as u8;
event.header.timestamp = 12345678;
event.header.pid = 2000; // child
event.header.tid = 2000;
event.data.fork.ppid = 1000; // parent

let bytes = unsafe {
std::slice::from_raw_parts(
&event as *const _ as *const u8,
std::mem::size_of_val(&event),
)
};

let parsed = parse_event(bytes).unwrap();
assert_eq!(parsed.pid, 2000);
assert_eq!(parsed.addr, 0);

match parsed.kind {
MemtrackEventKind::Fork { ppid } => {
assert_eq!(ppid, 1000);
}
_ => panic!("Expected Fork event kind"),
}
}
}
10 changes: 9 additions & 1 deletion crates/runner-shared/src/artifacts/execution_timestamps.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use libc::pid_t;
use serde::{Deserialize, Serialize};

use crate::fifo::MarkerType;
Expand All @@ -6,14 +7,21 @@ use crate::fifo::MarkerType;
pub struct ExecutionTimestamps {
pub uri_by_ts: Vec<(u64, String)>,
pub markers: Vec<MarkerType>,
#[serde(default)]
pub bench_pid_by_ts: Vec<(u64, pid_t)>,
}
impl super::ArtifactExt for ExecutionTimestamps {}

impl ExecutionTimestamps {
pub fn new(uri_by_ts: &[(u64, String)], markers: &[crate::fifo::MarkerType]) -> Self {
pub fn new(
uri_by_ts: &[(u64, String)],
markers: &[crate::fifo::MarkerType],
bench_pid_by_ts: &[(u64, pid_t)],
) -> Self {
Self {
uri_by_ts: uri_by_ts.to_vec(),
markers: markers.to_vec(),
bench_pid_by_ts: bench_pid_by_ts.to_vec(),
}
}
}
7 changes: 7 additions & 0 deletions crates/runner-shared/src/artifacts/memtrack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ pub enum MemtrackEventKind {
Brk {
size: u64,
},
/// A tracked process spawned a child. `pid` is the child, `ppid` the parent.
/// Carries no allocation; lets the parser rebuild the process tree and scope
/// allocations to a benchmark process plus its descendants.
Fork {
ppid: pid_t,
},
}

pub struct MemtrackEventStream<R: Read> {
Expand Down Expand Up @@ -159,6 +165,7 @@ mod tests {
MemtrackEventKind::Mmap { size: 9 },
MemtrackEventKind::Munmap { size: 9 },
MemtrackEventKind::Brk { size: 9 },
MemtrackEventKind::Fork { ppid: 1234 },
];

for kind in kinds {
Expand Down
12 changes: 9 additions & 3 deletions src/executor/shared/fifo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ impl RunnerFifo {
std::process::ExitStatus,
)> {
let mut bench_order_by_timestamp = Vec::<(u64, String)>::new();
let mut bench_pid_by_ts = Vec::<(u64, pid_t)>::new();
let mut bench_pids = HashSet::<pid_t>::new();
let mut markers = Vec::<MarkerType>::new();

Expand Down Expand Up @@ -206,7 +207,9 @@ impl RunnerFifo {
// Fall through to shared implementation for standard commands
match &cmd {
FifoCommand::CurrentBenchmark { pid, uri } => {
bench_order_by_timestamp.push((get_current_time(), uri.to_string()));
let ts = get_current_time();
Comment thread
linear-code[bot] marked this conversation as resolved.
bench_order_by_timestamp.push((ts, uri.to_string()));
bench_pid_by_ts.push((ts, *pid));
bench_pids.insert(*pid);
self.send_cmd(FifoCommand::Ack).await?;
}
Expand Down Expand Up @@ -273,8 +276,11 @@ impl RunnerFifo {
debug!(
"Process terminated with status: {exit_status}, stopping the command handler"
);
let marker_result =
ExecutionTimestamps::new(&bench_order_by_timestamp, &markers);
let marker_result = ExecutionTimestamps::new(
&bench_order_by_timestamp,
&markers,
&bench_pid_by_ts,
);
let fifo_data = FifoBenchmarkData {
integration,
bench_pids,
Expand Down
Loading