Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5ccc29b
feat(memtrack): capture allocation stacks in eBPF
not-matthias Aug 28, 2026
47725a0
feat(memtrack): add userspace stack-capture module
not-matthias Aug 28, 2026
d77dab1
feat(memtrack): enable stack capture through the tracker
not-matthias Sep 1, 2026
424787f
test(memtrack): cover allocation stack capture
not-matthias Aug 28, 2026
2827dda
refactor(runner): move ELF artifact pipeline to executor/shared
not-matthias Aug 28, 2026
82c6ccb
feat(runner): add MemtrackMetadata sharing ModuleArtifacts with walltime
not-matthias Aug 28, 2026
d13e810
feat(memtrack): record mapped modules for offline stack attribution
not-matthias Aug 28, 2026
f528c2a
feat(runner): write memtrack module artifacts and metadata
not-matthias Aug 28, 2026
32a1fec
docs(memtrack): describe the mapping recorder
not-matthias Aug 28, 2026
fea0e11
test(memtrack): add nested allocation fixtures
not-matthias Sep 1, 2026
6f1934b
test(memtrack): assert nested stack identities
not-matthias Sep 1, 2026
11c5143
refactor(memtrack): capture module mappings with perf
not-matthias Sep 1, 2026
af2f5c5
perf(runner-shared): pre-size the frame output buffer
not-matthias Sep 2, 2026
291edae
ci: benchmark runner-shared in memory mode
not-matthias Sep 2, 2026
610d74b
refactor(runner-shared): inline pid-key deserializer into metadata
not-matthias Sep 3, 2026
70cb646
fix(memtrack): skip allocator symbols aliased at an attached offset
not-matthias Sep 3, 2026
5cab5ab
perf(memtrack): switch to mimalloc to reduce memory usage and fragmen…
not-matthias Sep 4, 2026
fb49fc3
perf(memtrack): resolve stack fp chains off the ring poll thread
not-matthias Sep 4, 2026
ab6eb04
fix(memtrack): read tracking_enabled from a global instead of an arra…
not-matthias Sep 7, 2026
e5e5113
fix(memtrack): detach BPF links through forked fd holders
not-matthias Sep 7, 2026
2152fe2
feat(runner): add experimental flag to capture allocation stacks
not-matthias Sep 7, 2026
c6f76e4
perf(memtrack): poll ring buffers every 1ms
not-matthias Sep 23, 2026
c0b2ba2
fix(memtrack): count stack-ring overflow as dropped events
not-matthias Sep 23, 2026
c0c4c19
fix(memtrack): keep stack hashing scratch inside the ring record
not-matthias Sep 23, 2026
a87e1f1
feat(memtrack): pause producers under ring pressure
not-matthias Sep 24, 2026
ebfbdef
test(memtrack): measure ring-pressure pause under an allocation storm
not-matthias Sep 24, 2026
38ea912
test(memtrack): ignore duplicate frees in cross-variant comparison
not-matthias Sep 23, 2026
c023567
fix(memtrack): never resume a reused pid
not-matthias Sep 24, 2026
e594058
fixup! feat(memtrack): pause producers under ring pressure
not-matthias Sep 24, 2026
db9cfa5
feat(memtrack): log process stops and count them per reason
not-matthias Sep 24, 2026
4953b6c
fixup! feat(memtrack): pause producers under ring pressure
not-matthias Sep 24, 2026
6637072
feat(memtrack): log ring buffer throughput behind CODSPEED_MEMTRACK_R…
not-matthias Sep 23, 2026
2fb13f7
feat(memtrack): store the stop time in pressure_stopped
not-matthias Sep 24, 2026
a3e281d
feat(memtrack): write ring and pressure stats as JSONL
not-matthias Sep 24, 2026
57367ba
feat(runner-shared): report per-window encoder stats
not-matthias Sep 24, 2026
e49142e
feat(memtrack): record backlog, resolver and encoder stats
not-matthias Sep 24, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
# Each memtrack integration test binary runs its cases serially
# (eBPF tracker can't overlap with itself in one process), so we
# shard at the test-binary level to parallelize across jobs.
test: [c_tests, cpp_tests, rust_tests, spawn_tests, dlopen_tests, rss_tests]
test: [c_tests, cpp_tests, rust_tests, spawn_tests, dlopen_tests, rss_tests, stack_tests]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down Expand Up @@ -133,7 +133,7 @@ jobs:
strategy:
fail-fast: false
matrix:
mode: [simulation, walltime]
mode: [simulation, walltime, memory]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
74 changes: 74 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ ipc-channel = "0.20"
itertools = "0.14.0"
rayon = "1.12"
linux-perf-event-reader = "0.10.2" # matches the version linux-perf-data resolves to
perf-event-open-sys = "6.0"
env_logger = "0.11.10"
tempfile = "3.27.0"
object = { version = "0.39", default-features = false, features = ["read_core", "elf"] }
Expand Down
38 changes: 36 additions & 2 deletions crates/memtrack/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,20 @@ Control plane: `src/ipc.rs` exposes an out-of-band `ipc-channel` protocol (`Enab

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.

Mapping collection (`src/perf_mappings.rs`) uses Linux's native per-CPU perf event stream, not an LSM/BPF availability gate. `PerfMappingPoller` opens a `PERF_TYPE_SOFTWARE` dummy event with `PERF_ATTR_INHERIT` and `PERF_ATTR_MMAP2` on every online CPU for the tracked process, mmaps a perf ring per CPU, and drains those rings on a poll thread. It keeps executable mappings with absolute paths from `PERF_RECORD_MMAP2` and emits the single artifact representation, `MemtrackEventKind::Mapping` inside `MemtrackArtifact.events`, carrying the mapping's pid/tid/timestamp/address/path/device/inode/file offset/length. Opening or enabling any perf event requires the host's perf permissions (for example an allowed `perf_event_paranoid` policy or `CAP_PERFMON`); a permission error is returned from `Tracker::spawn` rather than silently disabling mapping collection. Kernel `PERF_RECORD_LOST` records, ring overruns, and malformed records increment the shared mapping-loss counter. `Tracker::dropped_events_count()` includes that counter with BPF ring-buffer drops, and `codspeed-memtrack track` aborts when the total is non-zero because the artifact is incomplete.`

### Event stream compatibility

Session relies on Rust's declaration-order field drop: _poller, _stack_poller, then _perf_mapping_poller. The BPF event and stack pollers therefore disconnect, fully drain, and join before the perf poller is dropped. PerfMappingPoller buffers mapping records and emits them during shutdown, after ordinary allocation/RSS/stack events have reached encode_events; encode_events preserves input order, so Mapping records are a terminal suffix in the one artifact stream.

This ordering is compatibility-critical. Mapping is a newer event variant; older stream consumers may treat the first unknown Mapping as EOF. Keeping it as the suffix lets those consumers process the complete memory timeline before stopping at that first unknown record. Do not reorder the poller fields or emit mapping records before shutdown.

> 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/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`), `stacks/`, `poller.rs`, `events.rs`, `c/main.bpf.c` + `c/event.h` + `c/stack_capture.bpf.h` + `c/utils/*.h` + `c/allocator.h`.
- `src/perf_mappings.rs` — native per-CPU `PERF_RECORD_MMAP2` collector.
- `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).
Expand Down Expand Up @@ -75,7 +84,32 @@ sudo -E cargo test --test c_tests -- --test-threads 1
- **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).
Env vars actually wired: `CODSPEED_MEMTRACK_BINARIES` (extra static-allocator binaries), `CODSPEED_MEMTRACK_TRACK_ALLOCATORS` (0/false disables), `CODSPEED_MEMTRACK_TRACK_PHYSICAL` (1 enables), `CODSPEED_MEMTRACK_CAPTURE_STACKS` (1 enables), `CODSPEED_MEMTRACK_STACK_BUDGET` (stack copy size in bytes, default 8192), `CODSPEED_MEMTRACK_STATS` (absolute path; writes per-tick ring positions, pressure episodes, pipeline backlog + RSS, stack-resolver batches and encoder windows as JSONL, plotted by `scripts/plot_stats.py`), `CODSPEED_LOG` (log filter, default `info`), `SUDO_UID`/`SUDO_GID` (privilege drop), `GITHUB_ACTIONS` (build rebuild trigger + test gate).

### Minimum kernel version

The BPF object needs **Linux >= 5.11** with `CONFIG_DEBUG_INFO_BTF` (vmlinux BTF). Nothing below is gated or `set_autoload(false)`-able, so the verifier rejects the whole skeleton on older kernels. Per feature:

| Feature | Used in | Kernel |
|---|---|---|
| `BPF_MAP_TYPE_STACK_TRACE`, `bpf_get_stackid` | `c/stack_capture.bpf.h` | 4.6 |
| `BPF_MAP_TYPE_LRU_HASH` | `c/utils/mm_ownership.h`, `c/rss.bpf.h` | 4.10 |
| `PERF_RECORD_MMAP2` with `clockid` | `src/perf_mappings.rs` | 4.1 |
| vmlinux BTF + CO-RE relocations | everything | 5.2 |
| `bpf_send_signal` | `c/attach.h` | 5.3 |
| `fentry/*`, `tp_btf/*` programs, `bpf_probe_read_user` | `c/attach.h`, `c/process_tracking.bpf.h`, `c/allocator.h` | 5.5 |
| `tracepoint/kmem/rss_stat` with `mm_id`/`curr` fields | `c/rss.bpf.h` | 5.5 |
| `bpf_get_ns_current_pid_tgid` | `c/utils/variant.h` | 5.7 |
| `BPF_MAP_TYPE_RINGBUF`, `bpf_ringbuf_*` | all event paths | 5.8 |
| `bpf_get_current_task_btf` (sets the floor) | `c/process_tracking.bpf.h`, `c/rss.bpf.h`, `c/rmap.bpf.h` | 5.11 |

Gated on top of the floor:

- rmap physical tracking (`src/ebpf/memtrack/rmap.rs`, `RmapSupport::for_version`): `Unsupported` < 6.8, PTE/PMD hooks 6.8–6.14, PUD hooks added at 6.15.
- `Token` variant (`uprobe_multi` links, `c/memtrack_token.bpf.c`): >= 6.6; BPF token delegation itself needs 6.9. The `Legacy` variant (`perf_event_open` uprobes) is the fallback.
- `folio` layout differences (`flags.f` >= 6.18, `_folio_order` < 6.6) are probed with `bpf_core_field_exists` in `c/utils/folio.h`.

Consequences: cgroup-based BPF memory accounting (5.11) means `bump_memlock_rlimit` failing is always harmless, and userspace syscalls newer than the floor (e.g. `close_range`, 5.9) need no `ENOSYS` fallback.

## Testing & QA

Expand Down
6 changes: 5 additions & 1 deletion crates/memtrack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ required-features = ["ebpf"]

[features]
default = ["ebpf"]
ebpf = ["dep:libbpf-rs", "dep:libbpf-cargo", "dep:vmlinux"]
ebpf = ["dep:libbpf-rs", "dep:libbpf-cargo", "dep:vmlinux", "dep:mimalloc"]

[dependencies]
anyhow = { workspace = true }
Expand All @@ -34,9 +34,13 @@ itertools = { workspace = true }
paste = "1.0.15"
libbpf-rs = { version = "0.26", features = ["vendored"], optional = true }
object = { workspace = true }
perf-event2 = "0.7.4"
linux-perf-event-reader = { workspace = true }
byteorder = "1.5"
rayon = "1.12"
parking_lot = "0.12"
typed-builder = "0.23.2"
mimalloc = { version = "0.1", optional = true }

[build-dependencies]
libbpf-cargo = { version = "0.26", optional = true }
Expand Down
Loading
Loading