Skip to content
Draft
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
85 changes: 85 additions & 0 deletions crates/memtrack/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Repository Guidelines

`memtrack` is the eBPF-based memory-allocation tracker of the CodSpeed runner (workspace member, Linux-only). This guide covers the crate at `crates/memtrack/`; the workspace root has its own `AGENTS.md`.

## Project Overview

Attaches uprobes/uretprobes to allocator functions (`malloc`/`free`/`calloc`/`realloc`/`aligned_alloc`/`memalign`) and tracepoints to `mmap`/`munmap`/`brk` + `sched_process_fork` in a target process tree, streams allocation events through a BPF ring buffer to userspace, and writes them to a `MemtrackArtifact` file. Ships a CLI binary `codspeed-memtrack track`.

## Architecture & Data Flow

Allocation → disk pipeline:

1. Allocator entry uprobe stores args in a per-tid BPF hash map (`<name>_arg` / `<name>_args`).
2. Uretprobe reads the stored arg + `PT_REGS_RC`. The `SUBMIT_EVENT` macro gates on `is_tracked(pid)` (`tracked_pids` map, ancestor walk ≤5 levels via `pids_ppid`, plus `sched_fork` auto-tracking of children) **and** `is_enabled()` (`tracking_enabled` map), then `bpf_ringbuf_reserve`/`submit` into the 16 MiB `events` ring buffer. Reserve failure atomically bumps `dropped_events`.
3. `RingBufferPoller` (`src/ebpf/poller.rs`) polls every 10 ms → `parse_event` (`src/ebpf/events.rs`) casts raw bytes to the bindgen `event` struct and maps `EVENT_TYPE_*` → `runner_shared::MemtrackEventKind` → mpsc channel.
4. `Tracker::track` (`src/ebpf/tracker.rs`) forwards poller events over a keep-alive thread.
5. `main.rs` runs a 2-stage pipeline: Stage A drain thread empties the ring buffer fast → Stage B writer thread batches via `MemtrackWriter`. After the child exits, it checks `dropped_events_count()` and **bails if non-zero** (incomplete trace).

Control plane: `src/ipc.rs` exposes an out-of-band `ipc-channel` protocol (`Enable`/`Disable`/`Ping`) so the runner toggles the `tracking_enabled` map at runtime. Without `--ipc-server`, tracking is enabled up front.

Allocator discovery (`src/allocators/`): `AllocatorLib::find_all()` = dynamic (glob shared libs incl. `/nix/store/*` hints) + static-linked (scan build-dir ELF symbols) + env (`CODSPEED_MEMTRACK_BINARIES`). Each `AllocatorKind` (`Libc`/`LibCpp`/`Jemalloc`/`Mimalloc`/`Tcmalloc`) maps to best-effort attach helpers; only libc must succeed.

> Note: the "on-demand attach" design in `.agents/docs/` (AttachWorker, `CODSPEED_MEMTRACK_ONDEMAND`, SIGSTOP/SIGCONT) is a **plan, not yet in source**. Current behavior is upfront attach + `sched_fork` auto-tracking.

## Key Directories

- `src/ebpf/` — BPF stack (feature-gated `ebpf`): `tracker.rs` (facade), `memtrack/` (libbpf-rs wrapper + generated skeleton, split into `mod.rs`/`macros.rs`/`maps.rs`/`allocator.rs`/`tracking.rs`), `poller.rs`, `events.rs`, `c/main.bpf.c` + `c/event.h` + `c/utils/*.h` + `c/allocator.h`.
- `src/allocators/` — allocator classification: `mod.rs`, `dynamic.rs`, `static_linked.rs`.
- `tests/` — integration tests + `snapshots/` (insta).
- `testdata/` — allocation fixtures: `*.c` (gcc), `alloc_cpp/` (cmkr/CMake), `alloc_rust/` + `spawn_wrapper/` (standalone Cargo workspaces).
- `.agents/docs/`, `.claude/` — design notes (some current, some historical/stale).

## Development Commands

```bash
cargo build # default features include `ebpf`
cargo check
cargo fmt
cargo clippy
cargo test --lib # unit tests (no root)

# Run the tracker (needs root); tracks a shell command's whole process tree:
sudo -E cargo run --bin codspeed-memtrack -- track "<command>" --output <dir>
# e.g. sudo -E cargo run --bin codspeed-memtrack -- track "ls / >/dev/null" --output .

# Integration tests need BPF privilege + GITHUB_ACTIONS gate + single-threaded:
export GITHUB_ACTIONS=1
sudo -E cargo test --test c_tests -- --test-threads 1
```

`--test-threads 1` is **mandatory** — eBPF probes cannot self-overlap. CI runs `sudo -E cargo test --lib --test <name> -- --test-threads 1`; the main workspace `tests` job excludes memtrack (`--exclude memtrack`).

## Code Conventions & Common Patterns

- **Errors:** `anyhow` only (`Result`/`Context`/`bail`/`ensure` via `src/prelude.rs`); no `thiserror`.
- **Concurrency:** no async runtime — `std::thread` + `std::sync::mpsc` + `Arc<Mutex<_>>`.
- **Naming:** snake_case modules, PascalCase types. Macro-generated `try_<name>` (fallible) vs `<name>_if_found` (best-effort, trace-logs errors) attach helpers via `paste!`.
- **BPF C:** macro-heavy (`UPROBE_ARG_RET`/`UPROBE_RET`/`UPROBE_ARGS_RET`/`SUBMIT_EVENT`); `.clang-format` is Google-based, 4-space indent, 100-col, left pointers; vmlinux.h include wrapped in `// clang-format off/on`.
- **Module layout:** `src/lib.rs` re-exports and `#[cfg(feature = "ebpf")]` gates the whole BPF stack + binary; `prelude.rs` centralizes error/log imports.
- **Cross-crate types:** `MemtrackEvent` / `MemtrackEventKind` live in `runner-shared/src/artifacts/memtrack.rs` (a breaking change there ripples here).

## Important Files

- `src/main.rs` — CLI entry (`codspeed-memtrack track`, requires `ebpf`); drops sudo privileges to `SUDO_UID`/`SUDO_GID` so the tracked child runs unprivileged.
- `src/lib.rs` — crate root / re-exports.
- `src/ebpf/c/main.bpf.c` + `c/event.h` + `c/utils/*.h` + `c/allocator.h` — BPF program (includes only) and event struct definitions; maps/programs live in headers.
- `build.rs` — feature-gated: `libbpf_cargo::SkeletonBuilder` compiles `main.bpf.c` → `OUT_DIR/memtrack.skel.rs`; `bindgen` on `wrapper.h` → `OUT_DIR/event.rs`. Reruns on `src/ebpf/c` changes and on `GITHUB_ACTIONS` env change.
- `wrapper.h` — bindgen entry (`stdint.h` + `src/ebpf/c/event.h`).
- `Cargo.toml` — lib `memtrack` + bin `codspeed-memtrack`; feature `ebpf` (default) pulls `libbpf-rs`/`libbpf-cargo`/`vmlinux`.

## Runtime / Tooling Prerequisites

- **Linux only** — consumed from workspace root under `cfg(target_os = "linux")`, targets `x86_64`/`aarch64-unknown-linux-gnu`.
- **Root / BPF privilege** required at runtime (bumps `RLIMIT_MEMLOCK`, loads BPF).
- **Build toolchain:** `clang` + BTF/vmlinux headers, `libbpf-dev`, `zlib1g-dev`, `pkgconf`, `build-essential`; vendored libbpf also needs `autopoint`/`bison`/`flex`.
- `vmlinux.h` is pinned to a specific git rev; `libbpf-rs` uses the `vendored` feature (dist links `libbpf-rs/static`).

Env vars actually wired: `CODSPEED_MEMTRACK_BINARIES` (extra static-allocator binaries), `CODSPEED_LOG` (log filter, default `info`), `SUDO_UID`/`SUDO_GID` (privilege drop), `GITHUB_ACTIONS` (build rebuild trigger + test gate).

## Testing & QA

- **Frameworks:** `insta` (snapshots), `rstest` (parametrized `#[case]`), `test-log`, `test-with` (`#[test_with::env(GITHUB_ACTIONS)]`), `tempfile`.
- **Harness:** `tests/shared.rs` — `assert_events_snapshot!` / `assert_events_with_marker!` (dedup by addr+discriminant, `Realloc` strips `old_addr`, `0xC0D59EED` marker windowing), `compile_rust_binary`, `track_command`/`track_binary[_with_opts]`.
- **Suites:** `c_tests` (8 gcc-compiled C fixtures, no marker), `cpp_tests` (7 cmkr targets: system + jemalloc/mimalloc/tcmalloc × static/dynamic), `rust_tests` (system/jemalloc/mimalloc via features), `spawn_tests` (static-allocator discovery across `exec`).
- **Snapshots:** `tests/snapshots/<binary>__<case>.snap`, address-stripped and deduped. Every integration test is `GITHUB_ACTIONS`-gated and needs BPF privilege; unit tests (`src/ebpf/events.rs`) run without root.
2 changes: 1 addition & 1 deletion crates/memtrack/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ fn build_ebpf() {
.expect("CARGO_CFG_TARGET_ARCH must be set in build script");
let memtrack_out = PathBuf::from(env::var("OUT_DIR").unwrap()).join("memtrack.skel.rs");
SkeletonBuilder::new()
.source("src/ebpf/c/memtrack.bpf.c")
.source("src/ebpf/c/main.bpf.c")
.clang_args([
"-I",
&vmlinux::include_path_root().join(arch).to_string_lossy(),
Expand Down
167 changes: 167 additions & 0 deletions crates/memtrack/src/ebpf/c/allocator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#ifndef __ALLOCATOR_H__
#define __ALLOCATOR_H__

#include "utils/event_helpers.h"
#include "utils/map_helpers.h"
#include "utils/tracking.h"

#define UPROBE_ARG_RET(name, arg_expr, submit_block) \
BPF_HASH_MAP(name##_arg, __u64, __u64, 10000); \
SEC("uprobe") \
int uprobe_##name(struct pt_regs* ctx) { return store_param(&name##_arg, arg_expr); } \
SEC("uretprobe") \
int uretprobe_##name(struct pt_regs* ctx) { \
__u64* arg_ptr = take_param(&name##_arg); \
if (!arg_ptr) { \
return 0; \
} \
__u64 ret_val = PT_REGS_RC(ctx); \
if (ret_val == 0) { \
return 0; \
} \
__u64 arg0 = *arg_ptr; \
submit_block; \
}

#define UPROBE_RET(name, arg_expr, submit_block) \
SEC("uprobe") \
int uprobe_##name(struct pt_regs* ctx) { \
__u64 arg0 = arg_expr; \
if (arg0 == 0) { \
return 0; \
} \
submit_block; \
}

#define UPROBE_ARGS_RET(name, arg0_expr, arg1_expr, submit_block) \
struct name##_args_t { \
__u64 arg0; \
__u64 arg1; \
}; \
BPF_HASH_MAP(name##_args, __u64, struct name##_args_t, 10000); \
SEC("uprobe") \
int uprobe_##name(struct pt_regs* ctx) { \
__u64 tid = bpf_get_current_pid_tgid(); \
__u32 pid = tid >> 32; \
\
if (!is_tracked(pid)) { \
return 0; \
} \
\
struct name##_args_t args = {.arg0 = arg0_expr, .arg1 = arg1_expr}; \
\
bpf_map_update_elem(&name##_args, &tid, &args, BPF_ANY); \
return 0; \
} \
SEC("uretprobe") \
int uretprobe_##name(struct pt_regs* ctx) { \
__u64 tid = bpf_get_current_pid_tgid(); \
struct name##_args_t* args = bpf_map_lookup_elem(&name##_args, &tid); \
\
if (!args) { \
return 0; \
} \
\
struct name##_args_t a = *args; \
bpf_map_delete_elem(&name##_args, &tid); \
\
__u64 ret_val = PT_REGS_RC(ctx); \
if (ret_val == 0) { \
return 0; \
} \
\
__u64 arg0 = a.arg0; \
__u64 arg1 = a.arg1; \
submit_block; \
}

UPROBE_ARG_RET(malloc, PT_REGS_PARM1(ctx), { return submit_alloc_event(arg0, ret_val); })

UPROBE_RET(free, PT_REGS_PARM1(ctx), { return submit_free_event(arg0); })

UPROBE_ARG_RET(calloc, PT_REGS_PARM1(ctx) * PT_REGS_PARM2(ctx),
{ return submit_calloc_event(arg0, ret_val); })

UPROBE_ARGS_RET(realloc, PT_REGS_PARM2(ctx), PT_REGS_PARM1(ctx),
{ return submit_realloc_event(arg1, ret_val, arg0); })

UPROBE_ARG_RET(aligned_alloc, PT_REGS_PARM2(ctx),
{ return submit_aligned_alloc_event(arg0, ret_val); })

UPROBE_ARG_RET(memalign, PT_REGS_PARM2(ctx), { return submit_aligned_alloc_event(arg0, ret_val); })

struct mmap_args {
__u64 addr;
__u64 len;
};

BPF_HASH_MAP(mmap_temp, __u64, struct mmap_args, 10000);

static __always_inline void store_mmap_args(__u64 addr, __u64 len) {
__u64 tid = bpf_get_current_pid_tgid();
__u32 pid = tid >> 32;
if (is_tracked(pid)) {
struct mmap_args args = {.addr = addr, .len = len};
bpf_map_update_elem(&mmap_temp, &tid, &args, BPF_ANY);
}
}

SEC("tracepoint/syscalls/sys_enter_mmap")
int tracepoint_sys_enter_mmap(struct trace_event_raw_sys_enter* ctx) {
store_mmap_args(ctx->args[0], ctx->args[1]);
return 0;
}

SEC("tracepoint/syscalls/sys_exit_mmap")
int tracepoint_sys_exit_mmap(struct trace_event_raw_sys_exit* ctx) {
struct mmap_args* args = (struct mmap_args*)take_param(&mmap_temp);
if (!args) {
return 0;
}

__s64 ret = ctx->ret;
if (ret <= 0) {
return 0;
}

return submit_mmap_event((__u64)ret, args->len, EVENT_TYPE_MMAP);
}

SEC("tracepoint/syscalls/sys_enter_munmap")
int tracepoint_sys_enter_munmap(struct trace_event_raw_sys_enter* ctx) {
__u64 addr = ctx->args[0];
__u64 len = ctx->args[1];

if (addr == 0 || len == 0) {
return 0;
}

return submit_mmap_event(addr, len, EVENT_TYPE_MUNMAP);
}

BPF_HASH_MAP(brk_temp, __u64, __u64, 10000);

SEC("tracepoint/syscalls/sys_enter_brk")
int tracepoint_sys_enter_brk(struct trace_event_raw_sys_enter* ctx) {
store_param(&brk_temp, ctx->args[0]);
return 0;
}

SEC("tracepoint/syscalls/sys_exit_brk")
int tracepoint_sys_exit_brk(struct trace_event_raw_sys_exit* ctx) {
__u64* requested_brk = take_param(&brk_temp);
if (!requested_brk) {
return 0;
}

__u64 new_brk = ctx->ret;
__u64 req_brk = *requested_brk;

if (req_brk == 0 || new_brk <= 0) {
return 0;
}

return submit_mmap_event(new_brk, 0, EVENT_TYPE_BRK);
}

#endif /* __ALLOCATOR_H__ */
14 changes: 14 additions & 0 deletions crates/memtrack/src/ebpf/c/main.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// clang-format off
#include "vmlinux.h"
// clang-format on
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

#include "allocator.h"
#include "event.h"
#include "utils/event_helpers.h"
#include "utils/map_helpers.h"
#include "utils/tracking.h"

char LICENSE[] SEC("license") = "GPL";
Loading
Loading