feat(memtrack): collect RSS via rss_stat and folio-rmap reconstruction - #453
feat(memtrack): collect RSS via rss_stat and folio-rmap reconstruction#453not-matthias wants to merge 16 commits into
Conversation
Merging this PR will not alter performance
|
Greptile SummaryThis PR adds RSS reporting to memtrack. The main changes are:
Confidence Score: 4/5This is close, but the startup failure path should be fixed before merging.
Files Needing Attention: crates/memtrack/src/ebpf/memtrack/tracking.rs
|
| Filename | Overview |
|---|---|
| crates/memtrack/src/ebpf/memtrack/tracking.rs | Attaches the RSS tracepoint, lifecycle tracepoints, and optional rmap fentry hooks. |
| crates/memtrack/src/ebpf/c/rss.bpf.h | Adds RSS and rmap event programs plus process lifecycle event emission. |
| crates/memtrack/src/ebpf/tracker.rs | Adds rmap gating and an RSS-only tracker construction path. |
Prompt To Fix All With AI
### Issue 1
crates/memtrack/src/ebpf/memtrack/tracking.rs:12-14
**Lifecycle attaches abort**
These lifecycle tracepoints still make tracker startup fail before allocator tracking can run. `rss_stat` is handled as optional below, but `task_newtask`, `sched_process_exec`, and `sched_process_exit` still return attach errors through `attach_tracepoints()`. On a host where allocator probes can attach but one of these RSS reconciliation tracepoints cannot, `Tracker::new()` returns an error instead of starting memory tracking with that RSS path disabled.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (28): Last reviewed commit: "ci(memtrack): run bpf tests on arm and a..." | Re-trigger Greptile
b670b0a to
2f41984
Compare
41945a5 to
64688a9
Compare
b02f3cb to
411b713
Compare
e00fb6b to
a68424a
Compare
e48a0a0 to
f305358
Compare
|
@greptileai pause reviews for now until i enable them again |
|
Understood — pausing reviews. I'll hold off until you re-enable them. Tip: You can customize Greptile's behavior for this repo with |
f4df079 to
921e469
Compare
29fa3da to
e5b96d3
Compare
Set AllowShortFunctionsOnASingleLine: None in .clang-format and apply it, reformatting the allocator uprobe macros accordingly.
8a47e82 to
205b479
Compare
A forked child's inherited RSS is invisible to rss_stat: the fork-time counter copies fire outside the child's context, and anon COW faults are counter-neutral, so a child that only touches inherited memory never reports anything on its own. A fork event carrying the parent pid lets consumers seed the child from the parent's last absolutes; exec and exit mark where the address space is replaced or torn down.
Sample the kernel's per-mm resident counter through the kmem:rss_stat tracepoint, emitting absolute byte values per mm member. Adds the EVENT_TYPE_RSS contract, MemtrackEventKind::Rss, the parser arm, and a writer bench case. An rss_stat update from reclaim or another process's madvise fires in the actor's context; track (mm_id, member) -> owning pid so those updates reach the owner. External events may only lower a counter, so stale reads and mm_id collisions cannot invent peaks.
Attach fentry hooks on the folio-rmap add/remove functions, emitting signed page-count deltas per MM_* bucket so anon, file, and shmem RSS can be reconstructed over time. Gated behind CODSPEED_MEMTRACK_TRACK_RMAP; the programs stay autoload-off by default so the skeleton loads on any kernel, with the PUD pair (only present since v6.15) gated separately from the core set so rmap still works on older kernels. Adds the EVENT_TYPE_RMAP contract, MemtrackEventKind::Rmap, parser arm, and bench case. Recover the owning pid for rmap events run by another task (reclaim, process_madvise, khugepaged, KSM) from the mm_struct pointer, and maintain the ownership maps across exec and thread-group exit. The same ownership binding also validates external (curr==0) rss_stat updates, so a stale mm can no longer attribute a counter to the wrong pid.
Add the rss_tests integration suite: per-workload RSS/rmap reconstruction snapshots against /proc ground truth, fork-seeded child RSS, exec/exit resets, foreign-actor rmap attribution (reclaim, external madvise), and mm-ownership across CLONE_VM and exec. Extend tests/shared.rs with the tracker/fixture helpers these tests need and move compile_c_source into it for reuse. The suite needs two surfaces the production paths don't: a tracker mode that skips the allocator probes and exec-mapping watcher, and readers for the mm-ownership maps.
Add an aarch64 lane to the bpf-tests matrix and run the rss integration tests alongside the existing test binaries.
205b479 to
0b9565e
Compare
|
@greptileai review again, focus on the eBPF code (mostly RMAP tracking), be exhaustive |
| self.attach_task_newtask()?; | ||
| self.attach_sched_process_exec()?; | ||
| self.attach_sched_process_exit()?; |
There was a problem hiding this comment.
These lifecycle tracepoints still make tracker startup fail before allocator tracking can run. rss_stat is handled as optional below, but task_newtask, sched_process_exec, and sched_process_exit still return attach errors through attach_tracepoints(). On a host where allocator probes can attach but one of these RSS reconciliation tracepoints cannot, Tracker::new() returns an error instead of starting memory tracking with that RSS path disabled.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/memtrack/src/ebpf/memtrack/tracking.rs
Line: 12-14
Comment:
**Lifecycle attaches abort**
These lifecycle tracepoints still make tracker startup fail before allocator tracking can run. `rss_stat` is handled as optional below, but `task_newtask`, `sched_process_exec`, and `sched_process_exit` still return attach errors through `attach_tracepoints()`. On a host where allocator probes can attach but one of these RSS reconciliation tracepoints cannot, `Tracker::new()` returns an error instead of starting memory tracking with that RSS path disabled.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
GuillaumeLagrange
left a comment
There was a problem hiding this comment.
olgtm, obviously quite a big chunk to chew, but looks mostly fine to me. Main point of concern is the maintainability of "which function can be track for which kernel version" which IMO could be improved.
| /* Kernels < 6.18 store folio->flags as a bare unsigned long instead of | ||
| * memdesc_flags_t; probe which layout the running kernel has. */ | ||
| struct folio___legacy { | ||
| unsigned long flags; | ||
| } __attribute__((preserve_access_index)); | ||
|
|
||
| static __always_inline unsigned long folio_read_flags(struct folio* folio) { | ||
| if (bpf_core_field_exists(folio->flags.f)) { | ||
| return BPF_CORE_READ(folio, flags).f; | ||
| } | ||
| return BPF_CORE_READ((struct folio___legacy*)folio, flags); | ||
| } | ||
|
|
||
| /* Kernels < 6.6 store the large-folio order in a dedicated byte instead of | ||
| * the low byte of _flags_1. */ | ||
| struct folio___order_byte { | ||
| unsigned char _folio_order; | ||
| } __attribute__((preserve_access_index)); |
There was a problem hiding this comment.
this could benefit form being in a dedicated legacy or kernel support file
| int BPF_PROG(tracepoint_task_newtask, struct task_struct* child, __u64 clone_flags) { | ||
| if (clone_flags & CLONE_THREAD) { | ||
| return 0; | ||
| } | ||
|
|
||
| __u32 parent_pid = current_tgid(); | ||
| if (!is_tracked(parent_pid)) { | ||
| return 0; | ||
| } | ||
|
|
||
| /* Register the child here rather than on sched_process_fork: that | ||
| * tracepoint fires for CLONE_THREAD too and carries only raw task pids, | ||
| * which would fill the tracking maps with thread tids that no exit path | ||
| * removes (group death deletes only the tgid). task_newtask fires before | ||
| * wake_up_new_task, so registration precedes any event from the child. | ||
| * The BTF-typed variant is used because the child's pid must be read in | ||
| * the tracker's namespace, which the tracepoint's raw pid field cannot | ||
| * give. */ | ||
| __u32 child_pid = task_ns_tgid(child); | ||
| if (!child_pid) { | ||
| return 0; | ||
| } | ||
| track_child(child_pid, parent_pid); | ||
|
|
||
| SUBMIT_EVENT_AS(child_pid, EVENT_TYPE_FORK, { e->data.fork.parent_pid = parent_pid; }); | ||
| } |
There was a problem hiding this comment.
Does not belong to the rss.bpf, it belongs to a specific fork/exec events watcher module
There was a problem hiding this comment.
Same for the sched_process_exec probe, WDYT?
| SEC("tp_btf/sched_process_fork") | ||
| int BPF_PROG(tracepoint_sched_fork, struct task_struct* parent, struct task_struct* child) { | ||
| follow_fork(task_ns_tgid(parent), task_ns_tgid(child)); | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Related to the other comments, why was this removing from the process_tracking code and moved into the rss one?
…ction
Rename the ownership maps to {object}_by_{key}.
…ooks Rewrite the rss_owner_by_counter doc comment as key/value/rule.
State that swap entries are out of scope.
…ooks Split rmap hooks and mm-ownership state out of rss.bpf.h.
…ooks Replace the two-bool RmapSupport with a three-level enum resolved from the kernel release, so the illegal core-off/pud-on state is unrepresentable and no vmlinux BTF load is needed. Every folio rmap target is an unconditional definition in mm/rmap.c (only the bodies are ifdef-ed), so the release is as precise a gate as a BTF symbol probe.
7573f98 to
b5a8e9e
Compare
Summary
Adds RSS (resident set size) collection to
memtrack, in two layers:kmem:rss_stattracepoint — absolute per-mm resident bytes (anon/file/shmem/swap), latest-wins.fentryhooks on the anon folio-rmap add/remove functions emit signed page-count deltas, so anon RSS can be rebuilt over time asΣ(add − remove) × PAGE_SIZE.The reconstruction is a total anon RSS delta-sum, not a per-vaddr resident map — the kernel remove hook (
folio_remove_rmap_ptes) carries no address, so removals can't be attributed to a vaddr (the add hooks' faulting vaddr is emitted for observability only).Commits
feat(memtrack): track RSS via kmem:rss_stat tracepoint— the baseline:EVENT_TYPE_RSScontract,MemtrackEventKind::Rss, parser arm, writer bench case, gated integration test.feat(memtrack): reconstruct anon RSS from gated folio rmap fentry hooks—EVENT_TYPE_RMAP_ANON+RmapAnonevent, fivefentryprograms (add_new / add_ptes / remove_ptes / remove_pmd / remove_pud), CO-RE folio helpers, and the load/attach gating.test(memtrack): validate anon RSS reconstruction against rss_stat— ramps anon RSS viammap/munmapand asserts the reconstructed estimate tracks therss_statMM_ANONPAGES peak within 25%.What's on by default vs gated
rss_stattracepoint: always on. EmittingRssevents is the intended new default behavior introduced by this change — the RSS tracepoint is not gated.RmapAnonfolio-rmapfentryprograms: off by default, gated behindCODSPEED_MEMTRACK_TRACK_RMAP=1. When the flag is unset they areset_autoload(false)before load and never attached, so:fentryBTF target would otherwise fail the whole load), and--mode memory) and out of the existing test suites — noRmapAnonevents are produced by default.Verification
Run in a privileged,
--pid=hostcontainer sharing the host kernel (7.0.12):real anon amplitude = 64 MiB, estimated peak = 64 MiB(ratio 1.00).rss_tests, flag unset): ✅ passes — noRmapAnonevents, folio-rmap programs stay unloaded.cargo fmt, andclippyclean.folio_*_rmap*functions verified to match theBPF_PROGarg layouts.Review notes (draft)
track_commandordering: the shared test helper spawns the child beforeenable()/track(root_pid). In practice the child'sfork→execve→ld.so→libc-initfar outlasts the two BPF-map updates, so tracking is armed before the workload allocates (both fixtures captured full event streams). Flagging in case we'd prefer a leading settle-usleepin the fixtures or an enable-before-spawn change in the helper.PAGE_SIZE: hardcoded to 4096 (correct on x86_64). On a 16K/64K-page arm64 runner the estimate would needsysconf(_SC_PAGESIZE);rss_statis already in bytes and unaffected. Happy to switch tosysconfif these tests run on arm64 CI.