From 2f7c81bec4d447d12fb3e17fe55576a2e4757cf3 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 13 Jul 2026 14:44:44 +0200 Subject: [PATCH 01/12] feat(memtrack): track RSS via kmem:rss_stat tracepoint 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, a writer bench case, and a gated integration test. --- crates/memtrack/src/ebpf/c/event.h | 6 ++ crates/memtrack/src/ebpf/c/memtrack.bpf.c | 2 + crates/memtrack/src/ebpf/c/rss.bpf.h | 20 +++++++ crates/memtrack/src/ebpf/events.rs | 37 ++++++++++++ crates/memtrack/src/ebpf/memtrack.rs | 2 + crates/memtrack/tests/rss_tests.rs | 56 +++++++++++++++++++ crates/memtrack/tests/shared.rs | 23 ++++++++ .../runner-shared/benches/memtrack_writer.rs | 6 +- .../runner-shared/src/artifacts/memtrack.rs | 14 +++++ 9 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 crates/memtrack/src/ebpf/c/rss.bpf.h create mode 100644 crates/memtrack/tests/rss_tests.rs diff --git a/crates/memtrack/src/ebpf/c/event.h b/crates/memtrack/src/ebpf/c/event.h index f914f634..dc8e8441 100644 --- a/crates/memtrack/src/ebpf/c/event.h +++ b/crates/memtrack/src/ebpf/c/event.h @@ -9,6 +9,7 @@ #define EVENT_TYPE_MMAP 6 #define EVENT_TYPE_MUNMAP 7 #define EVENT_TYPE_BRK 8 +#define EVENT_TYPE_RSS 9 /* Common header shared by all event types */ struct event_header { @@ -45,6 +46,11 @@ struct event { uint64_t addr; /* address of mapping */ uint64_t size; /* size of mapping */ } mmap; + + struct { + int32_t member; + uint64_t size; + } rss; } data; }; diff --git a/crates/memtrack/src/ebpf/c/memtrack.bpf.c b/crates/memtrack/src/ebpf/c/memtrack.bpf.c index 06601e27..6d040dc0 100644 --- a/crates/memtrack/src/ebpf/c/memtrack.bpf.c +++ b/crates/memtrack/src/ebpf/c/memtrack.bpf.c @@ -389,3 +389,5 @@ int tracepoint_sys_exit_brk(struct trace_event_raw_sys_exit* ctx) { return submit_mmap_event(new_brk, 0, EVENT_TYPE_BRK); } + +#include "rss.bpf.h" diff --git a/crates/memtrack/src/ebpf/c/rss.bpf.h b/crates/memtrack/src/ebpf/c/rss.bpf.h new file mode 100644 index 00000000..f30c5a29 --- /dev/null +++ b/crates/memtrack/src/ebpf/c/rss.bpf.h @@ -0,0 +1,20 @@ +#ifndef __RSS_BPF_H__ +#define __RSS_BPF_H__ + +static __always_inline int submit_rss_event(__s32 member, __u64 size) { + SUBMIT_EVENT(EVENT_TYPE_RSS, { + e->data.rss.member = member; + e->data.rss.size = size; + }); +} + +SEC("tracepoint/kmem/rss_stat") +int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) { + if (!ctx->curr) { + return 0; + } + + return submit_rss_event(ctx->member, ctx->size); +} + +#endif /* __RSS_BPF_H__ */ diff --git a/crates/memtrack/src/ebpf/events.rs b/crates/memtrack/src/ebpf/events.rs index 3ecb7a67..b769cf8e 100644 --- a/crates/memtrack/src/ebpf/events.rs +++ b/crates/memtrack/src/ebpf/events.rs @@ -74,6 +74,13 @@ pub fn parse_event(data: &[u8]) -> Option { size: event.data.mmap.size, }, ), + EVENT_TYPE_RSS => ( + 0, + MemtrackEventKind::Rss { + member: event.data.rss.member, + size: event.data.rss.size, + }, + ), unknown => { panic!("Unknown event type: {unknown}"); } @@ -160,4 +167,34 @@ mod tests { _ => panic!("Expected Malloc event kind"), } } + + #[test] + fn test_parse_rss_event() { + let mut event: bindings::event = unsafe { std::mem::zeroed() }; + event.header.event_type = bindings::EVENT_TYPE_RSS as u8; + event.header.timestamp = 12345678; + event.header.pid = 1000; + event.header.tid = 2000; + event.data.rss.member = 1; + event.data.rss.size = 4096 * 10; + + 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, 1000); + assert_eq!(parsed.addr, 0); + + match parsed.kind { + MemtrackEventKind::Rss { member, size } => { + assert_eq!(member, 1); + assert_eq!(size, 4096 * 10); + } + _ => panic!("Expected Rss event kind"), + } + } } diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 3a1bfa26..6915dc3a 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -447,9 +447,11 @@ impl MemtrackBpf { Ok(()) } attach_tracepoint!(sched_fork); + attach_tracepoint!(rss_stat); pub fn attach_tracepoints(&mut self) -> Result<()> { self.attach_sched_fork()?; + self.attach_rss_stat()?; Ok(()) } diff --git a/crates/memtrack/tests/rss_tests.rs b/crates/memtrack/tests/rss_tests.rs new file mode 100644 index 00000000..9a24ed9f --- /dev/null +++ b/crates/memtrack/tests/rss_tests.rs @@ -0,0 +1,56 @@ +#[macro_use] +mod shared; + +use runner_shared::artifacts::MemtrackEventKind; +use tempfile::TempDir; + +const RSS_TOUCH_SOURCE: &str = r#" +#include +#include + +int main(void) { + const size_t len = 64 * 1024 * 1024; + volatile char* buf = malloc(len); + if (!buf) return 1; + memset((void*)buf, 0x42, len); + volatile char sink = 0; + for (size_t i = 0; i < len; i += 4096) sink ^= buf[i]; + free((void*)buf); + return sink; +} +"#; + +#[test_with::env(GITHUB_ACTIONS)] +#[test_log::test] +fn test_rss_tracking() -> Result<(), Box> { + let temp_dir = TempDir::new()?; + let binary = shared::compile_c_source(RSS_TOUCH_SOURCE, "rss_touch", temp_dir.path())?; + + let (events, thread_handle) = shared::track_binary(&binary)?; + + let rss_events: Vec<(i32, u64)> = events + .iter() + .filter_map(|e| match e.kind { + MemtrackEventKind::Rss { member, size } => Some((member, size)), + _ => None, + }) + .collect(); + + assert!( + !rss_events.is_empty(), + "expected at least one Rss event, got none" + ); + assert!( + rss_events.iter().all(|(member, _)| (0..4).contains(member)), + "rss member out of range: {rss_events:?}" + ); + + let peak = rss_events.iter().map(|(_, size)| *size).max().unwrap(); + assert!( + peak >= 32 * 1024 * 1024, + "expected peak RSS >= 32 MiB, got {peak} bytes" + ); + + thread_handle.join().unwrap(); + Ok(()) +} diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs index 054b3b8e..19156e20 100644 --- a/crates/memtrack/tests/shared.rs +++ b/crates/memtrack/tests/shared.rs @@ -29,6 +29,7 @@ macro_rules! assert_events_snapshot { // Dedup events by address and type to remove duplicates let events = $events .iter() + .filter(|e| !matches!(e.kind, MemtrackEventKind::Rss { .. })) .sorted_by_key(|e| e.timestamp) .dedup_by(|a, b| a.addr == b.addr && discriminant(&a.kind) == discriminant(&b.kind)) .collect::>(); @@ -78,6 +79,7 @@ macro_rules! assert_events_with_marker { // Remove events outside our 0xC0D59EED marker allocations let filtered_events = $events .iter() + .filter(|e| !matches!(e.kind, MemtrackEventKind::Rss { .. })) .sorted_by_key(|e| e.timestamp) .dedup_by(|a, b| a.addr == b.addr && discriminant(&a.kind) == discriminant(&b.kind)) .skip_while(|e| { @@ -124,6 +126,27 @@ pub fn compile_rust_binary( Ok(crate_dir.join(format!("target/release/{name}"))) } +pub fn compile_c_source( + source_code: &str, + name: &str, + output_dir: &Path, +) -> Result> { + let source_path = output_dir.join(format!("{name}.c")); + let binary_path = output_dir.join(name); + std::fs::write(&source_path, source_code)?; + + let output = Command::new("gcc") + .args(["-O0", "-o", binary_path.to_str().unwrap()]) + .arg(&source_path) + .output()?; + if !output.status.success() { + eprintln!("gcc stderr: {}", String::from_utf8_lossy(&output.stderr)); + return Err("Failed to compile C fixture".into()); + } + + Ok(binary_path) +} + /// Track a spawned command, collecting all memory events. /// /// When `discover_system_allocators` is true, the tracker will scan for all diff --git a/crates/runner-shared/benches/memtrack_writer.rs b/crates/runner-shared/benches/memtrack_writer.rs index 482b2a7b..156ed3a5 100644 --- a/crates/runner-shared/benches/memtrack_writer.rs +++ b/crates/runner-shared/benches/memtrack_writer.rs @@ -13,7 +13,7 @@ fn generate_events(n: usize) -> Vec { let mut events = Vec::with_capacity(n); for _ in 0..n { let size = rng.gen_range(8..8192); - let kind = match rng.gen_range(0..8) { + let kind = match rng.gen_range(0..9) { 0 => MemtrackEventKind::Malloc { size }, 1 => MemtrackEventKind::Free, 2 => MemtrackEventKind::Realloc { @@ -25,6 +25,10 @@ fn generate_events(n: usize) -> Vec { 5 => MemtrackEventKind::Mmap { size }, 6 => MemtrackEventKind::Munmap { size }, 7 => MemtrackEventKind::Brk { size }, + 8 => MemtrackEventKind::Rss { + member: rng.gen_range(0..4), + size, + }, _ => unreachable!(), }; diff --git a/crates/runner-shared/src/artifacts/memtrack.rs b/crates/runner-shared/src/artifacts/memtrack.rs index 64c90e82..9af6110a 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -72,6 +72,10 @@ pub enum MemtrackEventKind { Brk { size: u64, }, + Rss { + member: i32, + size: u64, + }, } pub struct MemtrackEventStream { @@ -147,6 +151,16 @@ mod tests { addr: 0x20, kind: MemtrackEventKind::Free, }, + MemtrackEvent { + pid: 1, + tid: 11, + timestamp: 300, + addr: 0, + kind: MemtrackEventKind::Rss { + member: 1, + size: 40960, + }, + }, ]; let artifact = MemtrackArtifact { From 53594853e504ee3aafca8c0edd074b59c2344fcc Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 13 Jul 2026 14:47:45 +0200 Subject: [PATCH 02/12] feat(memtrack): reconstruct anon RSS from gated folio rmap fentry hooks Attach fentry hooks on the anon folio-rmap add/remove functions, emitting signed page-count deltas so anon 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. Adds the EVENT_TYPE_RMAP_ANON contract, MemtrackEventKind::RmapAnon, parser arm, and bench case. --- crates/memtrack/src/ebpf/c/event.h | 6 ++ crates/memtrack/src/ebpf/c/rss.bpf.h | 62 ++++++++++++++++ crates/memtrack/src/ebpf/events.rs | 35 ++++++++++ crates/memtrack/src/ebpf/memtrack.rs | 70 ++++++++++++++++++- .../runner-shared/benches/memtrack_writer.rs | 5 +- .../runner-shared/src/artifacts/memtrack.rs | 3 + 6 files changed, 179 insertions(+), 2 deletions(-) diff --git a/crates/memtrack/src/ebpf/c/event.h b/crates/memtrack/src/ebpf/c/event.h index dc8e8441..6667fd10 100644 --- a/crates/memtrack/src/ebpf/c/event.h +++ b/crates/memtrack/src/ebpf/c/event.h @@ -10,6 +10,7 @@ #define EVENT_TYPE_MUNMAP 7 #define EVENT_TYPE_BRK 8 #define EVENT_TYPE_RSS 9 +#define EVENT_TYPE_RMAP_ANON 10 /* Common header shared by all event types */ struct event_header { @@ -51,6 +52,11 @@ struct event { int32_t member; uint64_t size; } rss; + + struct { + int64_t delta; + uint64_t addr; + } rmap; } data; }; diff --git a/crates/memtrack/src/ebpf/c/rss.bpf.h b/crates/memtrack/src/ebpf/c/rss.bpf.h index f30c5a29..3f60240a 100644 --- a/crates/memtrack/src/ebpf/c/rss.bpf.h +++ b/crates/memtrack/src/ebpf/c/rss.bpf.h @@ -1,6 +1,8 @@ #ifndef __RSS_BPF_H__ #define __RSS_BPF_H__ +#define FOLIO_MAPPING_ANON 0x1UL + static __always_inline int submit_rss_event(__s32 member, __u64 size) { SUBMIT_EVENT(EVENT_TYPE_RSS, { e->data.rss.member = member; @@ -17,4 +19,64 @@ int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) { return submit_rss_event(ctx->member, ctx->size); } +static __always_inline __u64 folio_nr_pages_est(struct folio* folio) { + unsigned long flags = BPF_CORE_READ(folio, flags).f; + if (!(flags & (1UL << PG_head))) { + return 1; + } + unsigned long order = BPF_CORE_READ(folio, _flags_1) & 0xff; + return 1UL << order; +} + +static __always_inline int folio_is_anon(struct folio* folio) { + unsigned long mapping = (unsigned long)BPF_CORE_READ(folio, mapping); + return (mapping & FOLIO_MAPPING_ANON) != 0; +} + +static __always_inline int submit_rmap_anon(__s64 delta, __u64 addr) { + SUBMIT_EVENT(EVENT_TYPE_RMAP_ANON, { + e->data.rmap.delta = delta; + e->data.rmap.addr = addr; + }); +} + +SEC("fentry/folio_add_new_anon_rmap") +int BPF_PROG(fentry_folio_add_new_anon_rmap, struct folio* folio, struct vm_area_struct* vma, + unsigned long address) { + return submit_rmap_anon((__s64)folio_nr_pages_est(folio), address); +} + +SEC("fentry/folio_add_anon_rmap_ptes") +int BPF_PROG(fentry_folio_add_anon_rmap_ptes, struct folio* folio, struct page* page, int nr_pages, + struct vm_area_struct* vma, unsigned long address) { + return submit_rmap_anon((__s64)nr_pages, address); +} + +SEC("fentry/folio_remove_rmap_ptes") +int BPF_PROG(fentry_folio_remove_rmap_ptes, struct folio* folio, struct page* page, int nr_pages, + struct vm_area_struct* vma) { + if (!folio_is_anon(folio)) { + return 0; + } + return submit_rmap_anon(-(__s64)nr_pages, 0); +} + +SEC("fentry/folio_remove_rmap_pmd") +int BPF_PROG(fentry_folio_remove_rmap_pmd, struct folio* folio, struct page* page, + struct vm_area_struct* vma) { + if (!folio_is_anon(folio)) { + return 0; + } + return submit_rmap_anon(-(__s64)folio_nr_pages_est(folio), 0); +} + +SEC("fentry/folio_remove_rmap_pud") +int BPF_PROG(fentry_folio_remove_rmap_pud, struct folio* folio, struct page* page, + struct vm_area_struct* vma) { + if (!folio_is_anon(folio)) { + return 0; + } + return submit_rmap_anon(-(__s64)folio_nr_pages_est(folio), 0); +} + #endif /* __RSS_BPF_H__ */ diff --git a/crates/memtrack/src/ebpf/events.rs b/crates/memtrack/src/ebpf/events.rs index b769cf8e..ca619246 100644 --- a/crates/memtrack/src/ebpf/events.rs +++ b/crates/memtrack/src/ebpf/events.rs @@ -81,6 +81,12 @@ pub fn parse_event(data: &[u8]) -> Option { size: event.data.rss.size, }, ), + EVENT_TYPE_RMAP_ANON => ( + event.data.rmap.addr, + MemtrackEventKind::RmapAnon { + delta: event.data.rmap.delta, + }, + ), unknown => { panic!("Unknown event type: {unknown}"); } @@ -197,4 +203,33 @@ mod tests { _ => panic!("Expected Rss event kind"), } } + + #[test] + fn test_parse_rmap_anon_event() { + let mut event: bindings::event = unsafe { std::mem::zeroed() }; + event.header.event_type = bindings::EVENT_TYPE_RMAP_ANON as u8; + event.header.timestamp = 12345678; + event.header.pid = 1000; + event.header.tid = 2000; + event.data.rmap.delta = 8; + event.data.rmap.addr = 0x7f00; + + 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, 1000); + assert_eq!(parsed.addr, 0x7f00); + + match parsed.kind { + MemtrackEventKind::RmapAnon { delta } => { + assert_eq!(delta, 8); + } + _ => panic!("Expected RmapAnon event kind"), + } + } } diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 6915dc3a..e3a34a5b 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -172,9 +172,25 @@ macro_rules! attach_tracepoint { }; } +macro_rules! attach_fentry { + ($func:ident, $prog:ident) => { + fn $func(&mut self) -> Result<()> { + let link = self + .skel + .progs + .$prog + .attach() + .context(format!("Failed to attach {} fentry", stringify!($prog)))?; + self.probes.push(link); + Ok(()) + } + }; +} + pub struct MemtrackBpf { skel: Box>, probes: Vec, + track_rmap: bool, } impl MemtrackBpf { @@ -182,10 +198,34 @@ impl MemtrackBpf { // Build and open the syscalls BPF program let builder = MemtrackSkelBuilder::default(); let open_object = Box::leak(Box::new(MaybeUninit::uninit())); - let open_skel = builder + let mut open_skel = builder .open(open_object) .context("Failed to open syscalls BPF skeleton")?; + let track_rmap = std::env::var("CODSPEED_MEMTRACK_TRACK_RMAP").is_ok_and(|v| v == "1"); + if !track_rmap { + open_skel + .progs + .fentry_folio_add_new_anon_rmap + .set_autoload(false); + open_skel + .progs + .fentry_folio_add_anon_rmap_ptes + .set_autoload(false); + open_skel + .progs + .fentry_folio_remove_rmap_ptes + .set_autoload(false); + open_skel + .progs + .fentry_folio_remove_rmap_pmd + .set_autoload(false); + open_skel + .progs + .fentry_folio_remove_rmap_pud + .set_autoload(false); + } + let skel = Box::new( open_skel .load() @@ -195,6 +235,7 @@ impl MemtrackBpf { Ok(Self { skel, probes: Vec::new(), + track_rmap, }) } @@ -448,10 +489,37 @@ impl MemtrackBpf { } attach_tracepoint!(sched_fork); attach_tracepoint!(rss_stat); + attach_fentry!( + attach_fentry_folio_add_new_anon_rmap, + fentry_folio_add_new_anon_rmap + ); + attach_fentry!( + attach_fentry_folio_add_anon_rmap_ptes, + fentry_folio_add_anon_rmap_ptes + ); + attach_fentry!( + attach_fentry_folio_remove_rmap_ptes, + fentry_folio_remove_rmap_ptes + ); + attach_fentry!( + attach_fentry_folio_remove_rmap_pmd, + fentry_folio_remove_rmap_pmd + ); + attach_fentry!( + attach_fentry_folio_remove_rmap_pud, + fentry_folio_remove_rmap_pud + ); pub fn attach_tracepoints(&mut self) -> Result<()> { self.attach_sched_fork()?; self.attach_rss_stat()?; + if self.track_rmap { + self.attach_fentry_folio_add_new_anon_rmap()?; + self.attach_fentry_folio_add_anon_rmap_ptes()?; + self.attach_fentry_folio_remove_rmap_ptes()?; + self.attach_fentry_folio_remove_rmap_pmd()?; + self.attach_fentry_folio_remove_rmap_pud()?; + } Ok(()) } diff --git a/crates/runner-shared/benches/memtrack_writer.rs b/crates/runner-shared/benches/memtrack_writer.rs index 156ed3a5..81f2e37d 100644 --- a/crates/runner-shared/benches/memtrack_writer.rs +++ b/crates/runner-shared/benches/memtrack_writer.rs @@ -13,7 +13,7 @@ fn generate_events(n: usize) -> Vec { let mut events = Vec::with_capacity(n); for _ in 0..n { let size = rng.gen_range(8..8192); - let kind = match rng.gen_range(0..9) { + let kind = match rng.gen_range(0..10) { 0 => MemtrackEventKind::Malloc { size }, 1 => MemtrackEventKind::Free, 2 => MemtrackEventKind::Realloc { @@ -29,6 +29,9 @@ fn generate_events(n: usize) -> Vec { member: rng.gen_range(0..4), size, }, + 9 => MemtrackEventKind::RmapAnon { + delta: rng.gen_range(-1024..1024), + }, _ => unreachable!(), }; diff --git a/crates/runner-shared/src/artifacts/memtrack.rs b/crates/runner-shared/src/artifacts/memtrack.rs index 9af6110a..907a70c9 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -76,6 +76,9 @@ pub enum MemtrackEventKind { member: i32, size: u64, }, + RmapAnon { + delta: i64, + }, } pub struct MemtrackEventStream { From 2f419848d0a2dcbdf8249fd773f5730c0d1a7f40 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Mon, 13 Jul 2026 14:48:19 +0200 Subject: [PATCH 03/12] test(memtrack): validate anon RSS reconstruction against rss_stat Add a gated integration test that ramps anon RSS via mmap/munmap, reconstructs it from RmapAnon page-count deltas, and asserts the estimate tracks the rss_stat MM_ANONPAGES peak within 25%. --- .../tests/rss_reconstruction_tests.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/memtrack/tests/rss_reconstruction_tests.rs diff --git a/crates/memtrack/tests/rss_reconstruction_tests.rs b/crates/memtrack/tests/rss_reconstruction_tests.rs new file mode 100644 index 00000000..e873b169 --- /dev/null +++ b/crates/memtrack/tests/rss_reconstruction_tests.rs @@ -0,0 +1,118 @@ +#[macro_use] +mod shared; + +use runner_shared::artifacts::{ArtifactExt, MemtrackArtifact, MemtrackEventKind}; +use std::fs::File; +use std::path::PathBuf; +use std::process::Command; +use tempfile::TempDir; + +const RSS_TRIANGLE_SOURCE: &str = r#" +#include +#include +#include + +int main(void) { + const size_t chunk = 16UL * 1024 * 1024; + void* bufs[4]; + for (int i = 0; i < 4; i++) { + bufs[i] = mmap(NULL, chunk, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (bufs[i] == MAP_FAILED) return 1; + memset(bufs[i], 0x42, chunk); + usleep(50 * 1000); + } + for (int i = 0; i < 4; i++) { + munmap(bufs[i], chunk); + usleep(50 * 1000); + } + return 0; +} +"#; + +#[test_with::env(GITHUB_ACTIONS)] +#[test_log::test] +fn test_rss_reconstruction() -> Result<(), Box> { + unsafe { + std::env::set_var("CODSPEED_MEMTRACK_TRACK_RMAP", "1"); + } + + let temp_dir = TempDir::new()?; + let binary = shared::compile_c_source(RSS_TRIANGLE_SOURCE, "rss_triangle", temp_dir.path())?; + + let (mut events, thread_handle) = shared::track_command(Command::new(&binary), &[], false)?; + + let artifact_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) + .join("rss_reconstruction.MemtrackArtifact.msgpack"); + let artifact = MemtrackArtifact { + events: events.clone(), + }; + artifact.encode_to_writer(File::create(&artifact_path)?)?; + eprintln!("rss artifact: {}", artifact_path.display()); + + events.sort_by_key(|e| e.timestamp); + + const PAGE_SIZE: i64 = 4096; + + let real_anon: Vec = events + .iter() + .filter_map(|e| match e.kind { + MemtrackEventKind::Rss { member: 1, size } => Some(size as i64), + _ => None, + }) + .collect(); + + let rmap_deltas: Vec = events + .iter() + .filter_map(|e| match e.kind { + MemtrackEventKind::RmapAnon { delta } => Some(delta), + _ => None, + }) + .collect(); + + assert!( + !real_anon.is_empty(), + "expected at least one MM_ANONPAGES rss_stat event; got none (rss_stat hook did not fire)" + ); + assert!( + !rmap_deltas.is_empty(), + "expected at least one RmapAnon event; got none (folio rmap fentry hooks did not attach/fire)" + ); + + let real_peak = *real_anon.iter().max().unwrap(); + let real_base = *real_anon.iter().min().unwrap(); + let real_amplitude = real_peak - real_base; + + let mut cum: i64 = 0; + let mut est_peak: i64 = 0; + for delta in &rmap_deltas { + cum += delta; + est_peak = est_peak.max(cum * PAGE_SIZE); + } + + eprintln!( + "real anon amplitude = {} MiB, estimated peak = {} MiB", + real_amplitude / (1024 * 1024), + est_peak / (1024 * 1024) + ); + + const MIN_BYTES: i64 = 48 * 1024 * 1024; + assert!( + real_amplitude >= MIN_BYTES, + "real anon RSS amplitude {real_amplitude} bytes < {MIN_BYTES}" + ); + assert!( + est_peak >= MIN_BYTES, + "estimated anon RSS peak {est_peak} bytes < {MIN_BYTES}" + ); + + let low = (real_amplitude as f64 * 0.75) as i64; + let high = (real_amplitude as f64 * 1.25) as i64; + assert!( + (low..=high).contains(&est_peak), + "estimated peak {est_peak} not within [0.75, 1.25] x real amplitude {real_amplitude} ([{low}, {high}])" + ); + + thread_handle.join().unwrap(); + Ok(()) +} From 885c3761fcd0f004ecfa00e586b2afe882a48de7 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 11:50:47 +0200 Subject: [PATCH 04/12] fixup! feat(memtrack): track RSS via kmem:rss_stat tracepoint --- crates/memtrack/tests/shared.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs index 19156e20..ff7583cc 100644 --- a/crates/memtrack/tests/shared.rs +++ b/crates/memtrack/tests/shared.rs @@ -140,7 +140,7 @@ pub fn compile_c_source( .arg(&source_path) .output()?; if !output.status.success() { - eprintln!("gcc stderr: {}", String::from_utf8_lossy(&output.stderr)); + error!("gcc stderr: {}", String::from_utf8_lossy(&output.stderr)); return Err("Failed to compile C fixture".into()); } From 3410f17859f1c1215c18cbfe2cd04a4e6a6ab094 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 12:01:43 +0200 Subject: [PATCH 05/12] fixup! feat(memtrack): track RSS via kmem:rss_stat tracepoint --- crates/memtrack/src/ebpf/memtrack.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index e3a34a5b..451305fa 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -512,7 +512,9 @@ impl MemtrackBpf { pub fn attach_tracepoints(&mut self) -> Result<()> { self.attach_sched_fork()?; - self.attach_rss_stat()?; + if let Err(e) = self.attach_rss_stat() { + warn!("Failed to attach rss_stat tracepoint, RSS collection disabled: {e:#}"); + } if self.track_rmap { self.attach_fentry_folio_add_new_anon_rmap()?; self.attach_fentry_folio_add_anon_rmap_ptes()?; From 56a77ad78d0128eecf206e4e27b40a9ca27937b8 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 12:01:53 +0200 Subject: [PATCH 06/12] fixup! feat(memtrack): reconstruct anon RSS from gated folio rmap fentry hooks --- crates/memtrack/src/ebpf/memtrack.rs | 6 +++++- crates/memtrack/src/ebpf/tracker.rs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 451305fa..2b1a2091 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -195,6 +195,11 @@ pub struct MemtrackBpf { impl MemtrackBpf { pub fn new() -> Result { + let track_rmap = std::env::var("CODSPEED_MEMTRACK_TRACK_RMAP").is_ok_and(|v| v == "1"); + Self::new_with_rmap(track_rmap) + } + + pub fn new_with_rmap(track_rmap: bool) -> Result { // Build and open the syscalls BPF program let builder = MemtrackSkelBuilder::default(); let open_object = Box::leak(Box::new(MaybeUninit::uninit())); @@ -202,7 +207,6 @@ impl MemtrackBpf { .open(open_object) .context("Failed to open syscalls BPF skeleton")?; - let track_rmap = std::env::var("CODSPEED_MEMTRACK_TRACK_RMAP").is_ok_and(|v| v == "1"); if !track_rmap { open_skel .progs diff --git a/crates/memtrack/src/ebpf/tracker.rs b/crates/memtrack/src/ebpf/tracker.rs index 9f1af587..73c03d97 100644 --- a/crates/memtrack/src/ebpf/tracker.rs +++ b/crates/memtrack/src/ebpf/tracker.rs @@ -35,6 +35,15 @@ impl Tracker { Ok(Self { bpf }) } + pub fn new_without_allocators_with_rmap(track_rmap: bool) -> Result { + Self::bump_memlock_rlimit()?; + + let mut bpf = MemtrackBpf::new_with_rmap(track_rmap)?; + bpf.attach_tracepoints()?; + + Ok(Self { bpf }) + } + pub fn attach_allocators(&mut self, libs: &[AllocatorLib]) -> Result<()> { for allocator in libs { self.bpf From d9c6abd2c78392d8f3caa9bdca06330e04f96371 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 12:01:57 +0200 Subject: [PATCH 07/12] fixup! test(memtrack): validate anon RSS reconstruction against rss_stat --- .../tests/rss_reconstruction_tests.rs | 10 +++------ crates/memtrack/tests/shared.rs | 21 +++++++++++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/memtrack/tests/rss_reconstruction_tests.rs b/crates/memtrack/tests/rss_reconstruction_tests.rs index e873b169..e57be814 100644 --- a/crates/memtrack/tests/rss_reconstruction_tests.rs +++ b/crates/memtrack/tests/rss_reconstruction_tests.rs @@ -33,14 +33,10 @@ int main(void) { #[test_with::env(GITHUB_ACTIONS)] #[test_log::test] fn test_rss_reconstruction() -> Result<(), Box> { - unsafe { - std::env::set_var("CODSPEED_MEMTRACK_TRACK_RMAP", "1"); - } - let temp_dir = TempDir::new()?; let binary = shared::compile_c_source(RSS_TRIANGLE_SOURCE, "rss_triangle", temp_dir.path())?; - let (mut events, thread_handle) = shared::track_command(Command::new(&binary), &[], false)?; + let (mut events, thread_handle) = shared::track_command_with_rmap(Command::new(&binary), &[])?; let artifact_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) .join("rss_reconstruction.MemtrackArtifact.msgpack"); @@ -48,7 +44,7 @@ fn test_rss_reconstruction() -> Result<(), Box> { events: events.clone(), }; artifact.encode_to_writer(File::create(&artifact_path)?)?; - eprintln!("rss artifact: {}", artifact_path.display()); + log::info!("rss artifact: {}", artifact_path.display()); events.sort_by_key(|e| e.timestamp); @@ -90,7 +86,7 @@ fn test_rss_reconstruction() -> Result<(), Box> { est_peak = est_peak.max(cum * PAGE_SIZE); } - eprintln!( + log::info!( "real anon amplitude = {} MiB, estimated peak = {} MiB", real_amplitude / (1024 * 1024), est_peak / (1024 * 1024) diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs index ff7583cc..5e5e7d85 100644 --- a/crates/memtrack/tests/shared.rs +++ b/crates/memtrack/tests/shared.rs @@ -152,16 +152,29 @@ pub fn compile_c_source( /// When `discover_system_allocators` is true, the tracker will scan for all /// allocators on the system (slower). When false, only `extra_allocators` are used. pub fn track_command( + command: Command, + extra_allocators: &[AllocatorLib], + discover_system_allocators: bool, +) -> TrackResult { + track_command_inner(command, extra_allocators, discover_system_allocators, false) +} + +pub fn track_command_with_rmap(command: Command, extra_allocators: &[AllocatorLib]) -> TrackResult { + track_command_inner(command, extra_allocators, false, true) +} + +fn track_command_inner( mut command: Command, extra_allocators: &[AllocatorLib], discover_system_allocators: bool, + track_rmap: bool, ) -> TrackResult { // IMPORTANT: Always initialize the tracker BEFORE spawning the binary, as it can take some time to // attach to all the allocator libraries (especially when using NixOS). - let mut tracker = if discover_system_allocators { - memtrack::Tracker::new()? - } else { - memtrack::Tracker::new_without_allocators()? + let mut tracker = match (discover_system_allocators, track_rmap) { + (true, false) => memtrack::Tracker::new()?, + (false, rmap) => memtrack::Tracker::new_without_allocators_with_rmap(rmap)?, + (true, true) => bail!("rmap tracking with allocator discovery is not supported"), }; tracker.attach_allocators(extra_allocators)?; From ea72fab0d8ba1ef2286a28468fdddeae8a367e16 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 17:32:39 +0200 Subject: [PATCH 08/12] fixup! feat(memtrack): track RSS via kmem:rss_stat tracepoint --- crates/memtrack/src/ebpf/c/memtrack.bpf.c | 19 +++++++++++++++---- crates/memtrack/src/ebpf/c/rss.bpf.h | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/crates/memtrack/src/ebpf/c/memtrack.bpf.c b/crates/memtrack/src/ebpf/c/memtrack.bpf.c index 6d040dc0..066b00be 100644 --- a/crates/memtrack/src/ebpf/c/memtrack.bpf.c +++ b/crates/memtrack/src/ebpf/c/memtrack.bpf.c @@ -131,12 +131,11 @@ static __always_inline __u64* take_param(void* map) { /* Macro to handle common event submission boilerplate * Usage: SUBMIT_EVENT(event_type, { e->data.foo = bar; }) */ -#define SUBMIT_EVENT(evt_type, fill_data) \ +#define SUBMIT_EVENT_AS(owner_pid, evt_type, fill_data) \ { \ __u64 tid = bpf_get_current_pid_tgid(); \ - __u32 pid = tid >> 32; \ \ - if (!is_tracked(pid) || !is_enabled()) { \ + if (!is_enabled()) { \ return 0; \ } \ \ @@ -151,7 +150,7 @@ static __always_inline __u64* take_param(void* map) { } \ \ e->header.timestamp = bpf_ktime_get_ns(); \ - e->header.pid = pid; \ + e->header.pid = owner_pid; \ e->header.tid = tid & 0xFFFFFFFF; \ e->header.event_type = evt_type; \ \ @@ -161,6 +160,18 @@ static __always_inline __u64* take_param(void* map) { return 0; \ } +#define SUBMIT_EVENT(evt_type, fill_data) \ + { \ + __u64 tid = bpf_get_current_pid_tgid(); \ + __u32 pid = tid >> 32; \ + \ + if (!is_tracked(pid)) { \ + return 0; \ + } \ + \ + SUBMIT_EVENT_AS(pid, evt_type, fill_data); \ + } + /* Helper to submit an allocation event (malloc, calloc) */ static __always_inline int submit_alloc_event(__u64 size, __u64 addr) { SUBMIT_EVENT(EVENT_TYPE_MALLOC, { diff --git a/crates/memtrack/src/ebpf/c/rss.bpf.h b/crates/memtrack/src/ebpf/c/rss.bpf.h index 3f60240a..231a7b7f 100644 --- a/crates/memtrack/src/ebpf/c/rss.bpf.h +++ b/crates/memtrack/src/ebpf/c/rss.bpf.h @@ -3,8 +3,10 @@ #define FOLIO_MAPPING_ANON 0x1UL -static __always_inline int submit_rss_event(__s32 member, __u64 size) { - SUBMIT_EVENT(EVENT_TYPE_RSS, { +const volatile __u32 page_shift = 12; + +static __always_inline int submit_rss_event(__u32 owner_pid, __s32 member, __u64 size) { + SUBMIT_EVENT_AS(owner_pid, EVENT_TYPE_RSS, { e->data.rss.member = member; e->data.rss.size = size; }); @@ -16,7 +18,17 @@ int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) { return 0; } - return submit_rss_event(ctx->member, ctx->size); + __u64 tid = bpf_get_current_pid_tgid(); + __u32 pid = tid >> 32; + if (!is_tracked(pid)) { + return 0; + } + + if (ctx->member == MM_SWAPENTS) { + return 0; + } + + return submit_rss_event(pid, ctx->member, ctx->size); } static __always_inline __u64 folio_nr_pages_est(struct folio* folio) { From f74be82849805a1984020b362f47a01fe2ad3481 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 17:33:16 +0200 Subject: [PATCH 09/12] fixup! feat(memtrack): reconstruct anon RSS from gated folio rmap fentry hooks --- crates/memtrack/src/ebpf/c/rss.bpf.h | 46 +++++++++++-- crates/memtrack/src/ebpf/memtrack.rs | 97 +++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/crates/memtrack/src/ebpf/c/rss.bpf.h b/crates/memtrack/src/ebpf/c/rss.bpf.h index 231a7b7f..e3971cfe 100644 --- a/crates/memtrack/src/ebpf/c/rss.bpf.h +++ b/crates/memtrack/src/ebpf/c/rss.bpf.h @@ -45,8 +45,32 @@ static __always_inline int folio_is_anon(struct folio* folio) { return (mapping & FOLIO_MAPPING_ANON) != 0; } -static __always_inline int submit_rmap_anon(__s64 delta, __u64 addr) { - SUBMIT_EVENT(EVENT_TYPE_RMAP_ANON, { +static __always_inline __u64 folio_page_address(struct folio* folio, struct page* page, + struct vm_area_struct* vma) { + __u64 page_idx = ((__u64)page - (__u64)folio) / bpf_core_type_size(struct page); + __u64 pgoff = BPF_CORE_READ(folio, index) + page_idx; + __u64 vm_pgoff = BPF_CORE_READ(vma, vm_pgoff); + __u64 vm_start = BPF_CORE_READ(vma, vm_start); + if (pgoff < vm_pgoff) { + return vm_start; + } + return vm_start + ((pgoff - vm_pgoff) << page_shift); +} + +static __always_inline int submit_rmap_anon(struct vm_area_struct* vma, __s64 delta, __u64 addr) { + __u64 mm = (__u64)BPF_CORE_READ(vma, vm_mm); + struct task_struct* task = bpf_get_current_task_btf(); + if ((__u64)BPF_CORE_READ(task, mm) != mm) { + return 0; + } + + __u64 tid = bpf_get_current_pid_tgid(); + __u32 pid = tid >> 32; + if (!is_tracked(pid)) { + return 0; + } + + SUBMIT_EVENT_AS(pid, EVENT_TYPE_RMAP_ANON, { e->data.rmap.delta = delta; e->data.rmap.addr = addr; }); @@ -55,13 +79,19 @@ static __always_inline int submit_rmap_anon(__s64 delta, __u64 addr) { SEC("fentry/folio_add_new_anon_rmap") int BPF_PROG(fentry_folio_add_new_anon_rmap, struct folio* folio, struct vm_area_struct* vma, unsigned long address) { - return submit_rmap_anon((__s64)folio_nr_pages_est(folio), address); + return submit_rmap_anon(vma, (__s64)folio_nr_pages_est(folio), address); } SEC("fentry/folio_add_anon_rmap_ptes") int BPF_PROG(fentry_folio_add_anon_rmap_ptes, struct folio* folio, struct page* page, int nr_pages, struct vm_area_struct* vma, unsigned long address) { - return submit_rmap_anon((__s64)nr_pages, address); + return submit_rmap_anon(vma, (__s64)nr_pages, address); +} + +SEC("fentry/folio_add_anon_rmap_pmd") +int BPF_PROG(fentry_folio_add_anon_rmap_pmd, struct folio* folio, struct page* page, + struct vm_area_struct* vma, unsigned long address) { + return submit_rmap_anon(vma, (__s64)folio_nr_pages_est(folio), address); } SEC("fentry/folio_remove_rmap_ptes") @@ -70,7 +100,7 @@ int BPF_PROG(fentry_folio_remove_rmap_ptes, struct folio* folio, struct page* pa if (!folio_is_anon(folio)) { return 0; } - return submit_rmap_anon(-(__s64)nr_pages, 0); + return submit_rmap_anon(vma, -(__s64)nr_pages, folio_page_address(folio, page, vma)); } SEC("fentry/folio_remove_rmap_pmd") @@ -79,7 +109,8 @@ int BPF_PROG(fentry_folio_remove_rmap_pmd, struct folio* folio, struct page* pag if (!folio_is_anon(folio)) { return 0; } - return submit_rmap_anon(-(__s64)folio_nr_pages_est(folio), 0); + return submit_rmap_anon(vma, -(__s64)folio_nr_pages_est(folio), + folio_page_address(folio, page, vma)); } SEC("fentry/folio_remove_rmap_pud") @@ -88,7 +119,8 @@ int BPF_PROG(fentry_folio_remove_rmap_pud, struct folio* folio, struct page* pag if (!folio_is_anon(folio)) { return 0; } - return submit_rmap_anon(-(__s64)folio_nr_pages_est(folio), 0); + return submit_rmap_anon(vma, -(__s64)folio_nr_pages_est(folio), + folio_page_address(folio, page, vma)); } #endif /* __RSS_BPF_H__ */ diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 2b1a2091..22663a61 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -1,9 +1,8 @@ use crate::prelude::*; -use libbpf_rs::Link; -use libbpf_rs::skel::OpenSkel; -use libbpf_rs::skel::SkelBuilder; -use libbpf_rs::{MapCore, UprobeOpts}; +use libbpf_rs::skel::{OpenSkel, SkelBuilder}; +use libbpf_rs::{AsRawLibbpf, Link, MapCore, UprobeOpts}; use paste::paste; +use std::ffi::CString; use std::mem::MaybeUninit; use std::path::Path; @@ -44,6 +43,27 @@ fn ensure_symbol_exists(lib_path: &Path, symbol_name: &str) -> Result<()> { bail!("Symbol {symbol_name} not found in {}", lib_path.display()) } +fn page_shift() -> Result { + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + ensure!(page_size > 0, "Failed to read system page size"); + Ok((page_size as u32).trailing_zeros()) +} + +fn kernel_func_exists(btf: &libbpf_rs::btf::Btf, name: &str) -> bool { + let Ok(name) = CString::new(name) else { + return false; + }; + + let id = unsafe { + libbpf_rs::libbpf_sys::btf__find_by_name_kind( + btf.as_libbpf_object().as_ptr(), + name.as_ptr(), + libbpf_rs::libbpf_sys::BTF_KIND_FUNC, + ) + }; + id >= 0 +} + /// Macro to attach a function with both entry and return probes. /// Also generates a `try_attach_*` variant that logs errors instead of returning them. /// @@ -191,6 +211,7 @@ pub struct MemtrackBpf { skel: Box>, probes: Vec, track_rmap: bool, + btf_disabled_rmap_targets: Vec<&'static str>, } impl MemtrackBpf { @@ -207,6 +228,14 @@ impl MemtrackBpf { .open(open_object) .context("Failed to open syscalls BPF skeleton")?; + open_skel + .maps + .rodata_data + .as_deref_mut() + .context("rodata map missing")? + .page_shift = page_shift()?; + let mut btf_disabled_rmap_targets = Vec::new(); + if !track_rmap { open_skel .progs @@ -216,6 +245,10 @@ impl MemtrackBpf { .progs .fentry_folio_add_anon_rmap_ptes .set_autoload(false); + open_skel + .progs + .fentry_folio_add_anon_rmap_pmd + .set_autoload(false); open_skel .progs .fentry_folio_remove_rmap_ptes @@ -228,6 +261,30 @@ impl MemtrackBpf { .progs .fentry_folio_remove_rmap_pud .set_autoload(false); + } else { + let btf = libbpf_rs::btf::Btf::from_vmlinux().context("Failed to load vmlinux BTF")?; + macro_rules! disable_missing_kernel_func { + ($prog:ident, $target:literal) => { + if !kernel_func_exists(&btf, $target) { + open_skel.progs.$prog.set_autoload(false); + btf_disabled_rmap_targets.push($target); + warn!( + "Kernel function {} not present in BTF, disabling rmap fentry", + $target + ); + } + }; + } + + disable_missing_kernel_func!(fentry_folio_add_new_anon_rmap, "folio_add_new_anon_rmap"); + disable_missing_kernel_func!( + fentry_folio_add_anon_rmap_ptes, + "folio_add_anon_rmap_ptes" + ); + disable_missing_kernel_func!(fentry_folio_add_anon_rmap_pmd, "folio_add_anon_rmap_pmd"); + disable_missing_kernel_func!(fentry_folio_remove_rmap_ptes, "folio_remove_rmap_ptes"); + disable_missing_kernel_func!(fentry_folio_remove_rmap_pmd, "folio_remove_rmap_pmd"); + disable_missing_kernel_func!(fentry_folio_remove_rmap_pud, "folio_remove_rmap_pud"); } let skel = Box::new( @@ -240,6 +297,7 @@ impl MemtrackBpf { skel, probes: Vec::new(), track_rmap, + btf_disabled_rmap_targets, }) } @@ -501,6 +559,10 @@ impl MemtrackBpf { attach_fentry_folio_add_anon_rmap_ptes, fentry_folio_add_anon_rmap_ptes ); + attach_fentry!( + attach_fentry_folio_add_anon_rmap_pmd, + fentry_folio_add_anon_rmap_pmd + ); attach_fentry!( attach_fentry_folio_remove_rmap_ptes, fentry_folio_remove_rmap_ptes @@ -514,17 +576,34 @@ impl MemtrackBpf { fentry_folio_remove_rmap_pud ); + fn rmap_target_enabled(&self, target: &str) -> bool { + !self.btf_disabled_rmap_targets.contains(&target) + } + pub fn attach_tracepoints(&mut self) -> Result<()> { self.attach_sched_fork()?; if let Err(e) = self.attach_rss_stat() { warn!("Failed to attach rss_stat tracepoint, RSS collection disabled: {e:#}"); } if self.track_rmap { - self.attach_fentry_folio_add_new_anon_rmap()?; - self.attach_fentry_folio_add_anon_rmap_ptes()?; - self.attach_fentry_folio_remove_rmap_ptes()?; - self.attach_fentry_folio_remove_rmap_pmd()?; - self.attach_fentry_folio_remove_rmap_pud()?; + if self.rmap_target_enabled("folio_add_new_anon_rmap") { + self.attach_fentry_folio_add_new_anon_rmap()?; + } + if self.rmap_target_enabled("folio_add_anon_rmap_ptes") { + self.attach_fentry_folio_add_anon_rmap_ptes()?; + } + if self.rmap_target_enabled("folio_add_anon_rmap_pmd") { + self.attach_fentry_folio_add_anon_rmap_pmd()?; + } + if self.rmap_target_enabled("folio_remove_rmap_ptes") { + self.attach_fentry_folio_remove_rmap_ptes()?; + } + if self.rmap_target_enabled("folio_remove_rmap_pmd") { + self.attach_fentry_folio_remove_rmap_pmd()?; + } + if self.rmap_target_enabled("folio_remove_rmap_pud") { + self.attach_fentry_folio_remove_rmap_pud()?; + } } Ok(()) } From bffbe8e7b78707f0cf2cb8e1ed7765d3f137f779 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 17:33:51 +0200 Subject: [PATCH 10/12] amend! test(memtrack): validate anon RSS reconstruction against rss_stat test(memtrack): snapshot per-process RSS and rmap reconstruction against /proc --- crates/memtrack/testdata/rss/anon.c | 17 ++ crates/memtrack/testdata/rss/file.c | 30 +++ crates/memtrack/testdata/rss/fork.c | 39 ++++ crates/memtrack/testdata/rss/rss_report.h | 41 ++++ crates/memtrack/testdata/rss/shmem.c | 22 ++ crates/memtrack/testdata/rss/triangle.c | 24 ++ .../tests/rss_reconstruction_tests.rs | 114 --------- crates/memtrack/tests/rss_tests.rs | 220 ++++++++++++++---- crates/memtrack/tests/shared.rs | 2 +- .../tests/snapshots/rss_tests__rss_anon.snap | 32 +++ .../tests/snapshots/rss_tests__rss_file.snap | 32 +++ .../tests/snapshots/rss_tests__rss_fork.snap | 42 ++++ .../tests/snapshots/rss_tests__rss_shmem.snap | 32 +++ .../snapshots/rss_tests__rss_triangle.snap | 32 +++ 14 files changed, 521 insertions(+), 158 deletions(-) create mode 100644 crates/memtrack/testdata/rss/anon.c create mode 100644 crates/memtrack/testdata/rss/file.c create mode 100644 crates/memtrack/testdata/rss/fork.c create mode 100644 crates/memtrack/testdata/rss/rss_report.h create mode 100644 crates/memtrack/testdata/rss/shmem.c create mode 100644 crates/memtrack/testdata/rss/triangle.c delete mode 100644 crates/memtrack/tests/rss_reconstruction_tests.rs create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_file.snap create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap diff --git a/crates/memtrack/testdata/rss/anon.c b/crates/memtrack/testdata/rss/anon.c new file mode 100644 index 00000000..8aa0974e --- /dev/null +++ b/crates/memtrack/testdata/rss/anon.c @@ -0,0 +1,17 @@ +#include +#include +#include + +#include "rss_report.h" + +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + size_t len = 64UL * 1024 * 1024; + void* mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mem == MAP_FAILED) return 1; + memset(mem, 0x42, len); + int ret = write_rss_report(argv[1]); + munmap(mem, len); + return ret; +} diff --git a/crates/memtrack/testdata/rss/file.c b/crates/memtrack/testdata/rss/file.c new file mode 100644 index 00000000..5d45c293 --- /dev/null +++ b/crates/memtrack/testdata/rss/file.c @@ -0,0 +1,30 @@ +#include +#include +#include +#include +#include + +#include "rss_report.h" + +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + size_t len = 64UL * 1024 * 1024; + char template[] = "/tmp/memtrack-rss-file-XXXXXX"; + int fd = mkstemp(template); + if (fd < 0) return 1; + unlink(template); + char chunk[65536]; + memset(chunk, 0x42, sizeof(chunk)); + for (size_t off = 0; off < len; off += sizeof(chunk)) { + if (write(fd, chunk, sizeof(chunk)) != (ssize_t)sizeof(chunk)) return 1; + } + void* mem = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0); + if (mem == MAP_FAILED) return 1; + volatile char sink = 0; + for (size_t i = 0; i < len; i += 4096) sink ^= ((volatile char*)mem)[i]; + int ret = write_rss_report(argv[1]); + munmap(mem, len); + close(fd); + return ret + (sink & 0); +} diff --git a/crates/memtrack/testdata/rss/fork.c b/crates/memtrack/testdata/rss/fork.c new file mode 100644 index 00000000..f6fcfac0 --- /dev/null +++ b/crates/memtrack/testdata/rss/fork.c @@ -0,0 +1,39 @@ +#include +#include +#include +#include +#include + +#include "rss_report.h" + +static int append_rss(const char* path, const char* label) { + long kb = rss_status_kb("RssAnon:"); + if (kb < 0) return 1; + FILE* report = fopen(path, "a"); + if (!report) return 1; + fprintf(report, "%s: %ld\n", label, kb); + fclose(report); + return 0; +} + +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + size_t len = 0x20 * 1024 * 1024; + void* parent = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (parent == MAP_FAILED) return 1; + memset(parent, 0x42, len); + pid_t pid = fork(); + if (pid < 0) return 1; + if (pid == 0) { + void* child = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (child == MAP_FAILED) _exit(1); + memset(child, 0x24, len); + _exit(append_rss(argv[1], "ChildRssAnonKb")); + } + int status; + if (waitpid(pid, &status, 0) < 0) return 1; + int ret = append_rss(argv[1], "ParentRssAnonKb"); + munmap(parent, len); + return ret || !WIFEXITED(status) || WEXITSTATUS(status) != 0; +} diff --git a/crates/memtrack/testdata/rss/rss_report.h b/crates/memtrack/testdata/rss/rss_report.h new file mode 100644 index 00000000..237062e7 --- /dev/null +++ b/crates/memtrack/testdata/rss/rss_report.h @@ -0,0 +1,41 @@ +#ifndef RSS_REPORT_H +#define RSS_REPORT_H + +#include +#include +#include + +static long rss_status_kb(const char* key) { + FILE* status = fopen("/proc/self/status", "r"); + if (!status) return -1; + char line[256]; + long kb = -1; + size_t key_len = strlen(key); + while (fgets(line, sizeof(line), status)) { + if (strncmp(line, key, key_len) == 0 && sscanf(line + key_len, " %ld", &kb) == 1) { + break; + } + } + fclose(status); + return kb; +} + +static int write_rss_report(const char* path) { + long anon = rss_status_kb("RssAnon:"); + long file = rss_status_kb("RssFile:"); + long shmem = rss_status_kb("RssShmem:"); + struct rusage usage; + if (anon < 0 || file < 0 || shmem < 0 || getrusage(RUSAGE_SELF, &usage) != 0) { + return 1; + } + FILE* report = fopen(path, "w"); + if (!report) { + return 1; + } + fprintf(report, "RssAnon: %ld\nRssFile: %ld\nRssShmem: %ld\nMaxRssKb: %ld\n", anon, file, + shmem, usage.ru_maxrss); + fclose(report); + return 0; +} + +#endif diff --git a/crates/memtrack/testdata/rss/shmem.c b/crates/memtrack/testdata/rss/shmem.c new file mode 100644 index 00000000..63b53e01 --- /dev/null +++ b/crates/memtrack/testdata/rss/shmem.c @@ -0,0 +1,22 @@ +#define _GNU_SOURCE +#include +#include +#include + +#include "rss_report.h" + +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + size_t len = 64UL * 1024 * 1024; + int fd = memfd_create("memtrack-rss-shmem", 0); + if (fd < 0) return 1; + if (ftruncate(fd, len) != 0) return 1; + void* mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) return 1; + memset(mem, 0x42, len); + int ret = write_rss_report(argv[1]); + munmap(mem, len); + close(fd); + return ret; +} diff --git a/crates/memtrack/testdata/rss/triangle.c b/crates/memtrack/testdata/rss/triangle.c new file mode 100644 index 00000000..69bf7bf6 --- /dev/null +++ b/crates/memtrack/testdata/rss/triangle.c @@ -0,0 +1,24 @@ +#include +#include +#include + +#include "rss_report.h" + +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + const size_t chunk = 16UL * 1024 * 1024; + void* bufs[4]; + for (int i = 0; i < 4; i++) { + bufs[i] = mmap(NULL, chunk, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (bufs[i] == MAP_FAILED) return 1; + memset(bufs[i], 0x42, chunk); + usleep(50 * 1000); + } + int ret = write_rss_report(argv[1]); + for (int i = 0; i < 4; i++) { + munmap(bufs[i], chunk); + usleep(50 * 1000); + } + return ret; +} diff --git a/crates/memtrack/tests/rss_reconstruction_tests.rs b/crates/memtrack/tests/rss_reconstruction_tests.rs deleted file mode 100644 index e57be814..00000000 --- a/crates/memtrack/tests/rss_reconstruction_tests.rs +++ /dev/null @@ -1,114 +0,0 @@ -#[macro_use] -mod shared; - -use runner_shared::artifacts::{ArtifactExt, MemtrackArtifact, MemtrackEventKind}; -use std::fs::File; -use std::path::PathBuf; -use std::process::Command; -use tempfile::TempDir; - -const RSS_TRIANGLE_SOURCE: &str = r#" -#include -#include -#include - -int main(void) { - const size_t chunk = 16UL * 1024 * 1024; - void* bufs[4]; - for (int i = 0; i < 4; i++) { - bufs[i] = mmap(NULL, chunk, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - if (bufs[i] == MAP_FAILED) return 1; - memset(bufs[i], 0x42, chunk); - usleep(50 * 1000); - } - for (int i = 0; i < 4; i++) { - munmap(bufs[i], chunk); - usleep(50 * 1000); - } - return 0; -} -"#; - -#[test_with::env(GITHUB_ACTIONS)] -#[test_log::test] -fn test_rss_reconstruction() -> Result<(), Box> { - let temp_dir = TempDir::new()?; - let binary = shared::compile_c_source(RSS_TRIANGLE_SOURCE, "rss_triangle", temp_dir.path())?; - - let (mut events, thread_handle) = shared::track_command_with_rmap(Command::new(&binary), &[])?; - - let artifact_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) - .join("rss_reconstruction.MemtrackArtifact.msgpack"); - let artifact = MemtrackArtifact { - events: events.clone(), - }; - artifact.encode_to_writer(File::create(&artifact_path)?)?; - log::info!("rss artifact: {}", artifact_path.display()); - - events.sort_by_key(|e| e.timestamp); - - const PAGE_SIZE: i64 = 4096; - - let real_anon: Vec = events - .iter() - .filter_map(|e| match e.kind { - MemtrackEventKind::Rss { member: 1, size } => Some(size as i64), - _ => None, - }) - .collect(); - - let rmap_deltas: Vec = events - .iter() - .filter_map(|e| match e.kind { - MemtrackEventKind::RmapAnon { delta } => Some(delta), - _ => None, - }) - .collect(); - - assert!( - !real_anon.is_empty(), - "expected at least one MM_ANONPAGES rss_stat event; got none (rss_stat hook did not fire)" - ); - assert!( - !rmap_deltas.is_empty(), - "expected at least one RmapAnon event; got none (folio rmap fentry hooks did not attach/fire)" - ); - - let real_peak = *real_anon.iter().max().unwrap(); - let real_base = *real_anon.iter().min().unwrap(); - let real_amplitude = real_peak - real_base; - - let mut cum: i64 = 0; - let mut est_peak: i64 = 0; - for delta in &rmap_deltas { - cum += delta; - est_peak = est_peak.max(cum * PAGE_SIZE); - } - - log::info!( - "real anon amplitude = {} MiB, estimated peak = {} MiB", - real_amplitude / (1024 * 1024), - est_peak / (1024 * 1024) - ); - - const MIN_BYTES: i64 = 48 * 1024 * 1024; - assert!( - real_amplitude >= MIN_BYTES, - "real anon RSS amplitude {real_amplitude} bytes < {MIN_BYTES}" - ); - assert!( - est_peak >= MIN_BYTES, - "estimated anon RSS peak {est_peak} bytes < {MIN_BYTES}" - ); - - let low = (real_amplitude as f64 * 0.75) as i64; - let high = (real_amplitude as f64 * 1.25) as i64; - assert!( - (low..=high).contains(&est_peak), - "estimated peak {est_peak} not within [0.75, 1.25] x real amplitude {real_amplitude} ([{low}, {high}])" - ); - - thread_handle.join().unwrap(); - Ok(()) -} diff --git a/crates/memtrack/tests/rss_tests.rs b/crates/memtrack/tests/rss_tests.rs index 9a24ed9f..fa3b78f0 100644 --- a/crates/memtrack/tests/rss_tests.rs +++ b/crates/memtrack/tests/rss_tests.rs @@ -1,56 +1,190 @@ #[macro_use] mod shared; -use runner_shared::artifacts::MemtrackEventKind; +use itertools::Itertools; +use rstest::rstest; +use runner_shared::artifacts::{MemtrackEvent, MemtrackEventKind}; +use std::collections::BTreeMap; +use std::process::Command; use tempfile::TempDir; -const RSS_TOUCH_SOURCE: &str = r#" -#include -#include +const MIB: u64 = 1024 * 1024; -int main(void) { - const size_t len = 64 * 1024 * 1024; - volatile char* buf = malloc(len); - if (!buf) return 1; - memset((void*)buf, 0x42, len); - volatile char sink = 0; - for (size_t i = 0; i < len; i += 4096) sink ^= buf[i]; - free((void*)buf); - return sink; +fn mib_16(bytes: u64) -> u64 { + (bytes + 8 * MIB) / (16 * MIB) * 16 } -"#; -#[test_with::env(GITHUB_ACTIONS)] -#[test_log::test] -fn test_rss_tracking() -> Result<(), Box> { +#[derive(Debug)] +struct RssReport { + proc_mib: BTreeMap, +} + +impl RssReport { + fn parse(report: &str) -> Self { + let proc_mib = report + .lines() + .filter_map(|line| { + let mut parts = line.split_whitespace(); + let key = parts.next()?.to_string(); + let kb: u64 = parts.next()?.parse().ok()?; + Some((key, mib_16(kb * 1024))) + }) + .collect(); + Self { proc_mib } + } +} + +#[derive(Debug, Default)] +struct PidRss { + file_mib: u64, + anon_mib: u64, + shmem_mib: u64, + max_rss_mib: u64, +} + +/// Per-process RSS peaks from the aggregated kmem:rss_stat tracepoint, keyed by pid +/// (redact with insta for stable snapshots). +#[derive(Debug)] +struct RssStats { + per_pid: BTreeMap, +} + +#[derive(Default)] +struct PidAccum { + peaks: [u64; 4], + latest: [u64; 4], + max_rss: u64, +} + +impl RssStats { + fn from_events(events: &[MemtrackEvent]) -> Self { + let mut rss: Vec<&MemtrackEvent> = events + .iter() + .filter(|event| matches!(event.kind, MemtrackEventKind::Rss { .. })) + .collect(); + rss.sort_by_key(|event| event.timestamp); + + let mut accum: BTreeMap = BTreeMap::new(); + for event in rss { + let MemtrackEventKind::Rss { member, size } = event.kind else { + continue; + }; + let Ok(index) = usize::try_from(member) else { + continue; + }; + if index >= 4 { + continue; + } + let acc = accum.entry(event.pid).or_default(); + acc.peaks[index] = acc.peaks[index].max(size); + acc.latest[index] = size; + acc.max_rss = acc.max_rss.max(acc.latest[0] + acc.latest[1] + acc.latest[3]); + } + + let per_pid = accum + .into_iter() + .map(|(pid, acc)| { + ( + pid, + PidRss { + file_mib: mib_16(acc.peaks[0]), + anon_mib: mib_16(acc.peaks[1]), + shmem_mib: mib_16(acc.peaks[3]), + max_rss_mib: mib_16(acc.max_rss), + }, + ) + }) + .collect(); + + Self { per_pid } + } +} + +/// Per-process anon RSS reconstructed from folio rmap deltas, keyed by pid. Same +/// layout as [`RssStats`] so the two can be compared per-pid. rmap only observes +/// anon, so `file`/`shmem` stay zero; `max_rss` is the peak of the running anon total. +#[derive(Debug)] +struct RssRmapStats { + per_pid: BTreeMap, +} + +impl RssRmapStats { + fn from_events(events: &[MemtrackEvent]) -> Self { + let rmap: Vec<&MemtrackEvent> = events + .iter() + .filter(|event| matches!(event.kind, MemtrackEventKind::RmapAnon { .. })) + .sorted_by_key(|event| event.timestamp) + .collect(); + + let mut accum: BTreeMap = BTreeMap::new(); + for event in rmap { + let MemtrackEventKind::RmapAnon { delta } = event.kind else { + continue; + }; + let (total, max_rss) = accum.entry(event.pid).or_default(); + *total += delta * 4096; + *max_rss = (*max_rss).max(*total); + } + + let per_pid = accum + .into_iter() + .map(|(pid, (_total, max_rss))| { + let anon_mib = mib_16(max_rss.max(0) as u64); + ( + pid, + PidRss { + anon_mib, + max_rss_mib: anon_mib, + ..Default::default() + }, + ) + }) + .collect(); + + Self { per_pid } + } +} + +/// Compile a fixture that writes a `/proc` RSS report to its argv[1], run it under +/// `track`, and return the parsed report alongside the collected events. +fn track_fixture( + source: &str, + name: &str, + track: impl FnOnce(Command) -> shared::TrackResult, +) -> Result<(RssReport, Vec), Box> { let temp_dir = TempDir::new()?; - let binary = shared::compile_c_source(RSS_TOUCH_SOURCE, "rss_touch", temp_dir.path())?; - - let (events, thread_handle) = shared::track_binary(&binary)?; - - let rss_events: Vec<(i32, u64)> = events - .iter() - .filter_map(|e| match e.kind { - MemtrackEventKind::Rss { member, size } => Some((member, size)), - _ => None, - }) - .collect(); - - assert!( - !rss_events.is_empty(), - "expected at least one Rss event, got none" - ); - assert!( - rss_events.iter().all(|(member, _)| (0..4).contains(member)), - "rss member out of range: {rss_events:?}" - ); - - let peak = rss_events.iter().map(|(_, size)| *size).max().unwrap(); - assert!( - peak >= 32 * 1024 * 1024, - "expected peak RSS >= 32 MiB, got {peak} bytes" - ); + std::fs::write( + temp_dir.path().join("rss_report.h"), + include_str!("../testdata/rss/rss_report.h"), + )?; + let binary = shared::compile_c_source(source, name, temp_dir.path())?; + let report_path = temp_dir.path().join(format!("{name}.report")); + let mut command = Command::new(&binary); + command.arg(&report_path); + let (events, thread_handle) = track(command)?; + let report = RssReport::parse(&std::fs::read_to_string(&report_path)?); thread_handle.join().unwrap(); + Ok((report, events)) +} + +#[test_with::env(GITHUB_ACTIONS)] +#[rstest] +#[case::anon(include_str!("../testdata/rss/anon.c"), "anon")] +#[case::file(include_str!("../testdata/rss/file.c"), "file")] +#[case::shmem(include_str!("../testdata/rss/shmem.c"), "shmem")] +#[case::fork(include_str!("../testdata/rss/fork.c"), "fork")] +#[case::triangle(include_str!("../testdata/rss/triangle.c"), "triangle")] +fn test_rss_rmap_tracking( + #[case] source: &str, + #[case] name: &str, +) -> Result<(), Box> { + let (report, events) = track_fixture(source, name, |cmd| { + shared::track_command_with_rmap(cmd, &[]) + })?; + let rss = RssStats::from_events(&events); + let rmap = RssRmapStats::from_events(&events); + let summary = format!("{report:#x?}\n{rss:#x?}\n{rmap:#x?}"); + insta::assert_snapshot!(format!("rss_{name}"), summary); Ok(()) } diff --git a/crates/memtrack/tests/shared.rs b/crates/memtrack/tests/shared.rs index 5e5e7d85..c8c93951 100644 --- a/crates/memtrack/tests/shared.rs +++ b/crates/memtrack/tests/shared.rs @@ -8,7 +8,7 @@ use std::path::Path; use std::process::Command; use std::time::Duration; -type TrackResult = anyhow::Result<(Vec, std::thread::JoinHandle<()>)>; +pub type TrackResult = anyhow::Result<(Vec, std::thread::JoinHandle<()>)>; /// Asserts memory events using snapshot testing without marker filtering. /// diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap new file mode 100644 index 00000000..340684dd --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap @@ -0,0 +1,32 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "MaxRssKb:": 0x40, + "RssAnon:": 0x40, + "RssFile:": 0x0, + "RssShmem:": 0x0, + }, +} +RssStats { + per_pid: { + 0x172f89: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x172f89: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap new file mode 100644 index 00000000..a3e7ae17 --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap @@ -0,0 +1,32 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "MaxRssKb:": 0x40, + "RssAnon:": 0x0, + "RssFile:": 0x40, + "RssShmem:": 0x0, + }, +} +RssStats { + per_pid: { + 0x172f8f: PidRss { + file_mib: 0x40, + anon_mib: 0x0, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x172f8f: PidRss { + file_mib: 0x0, + anon_mib: 0x0, + shmem_mib: 0x0, + max_rss_mib: 0x0, + }, + }, +} diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap new file mode 100644 index 00000000..8b71d32b --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap @@ -0,0 +1,42 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "ChildRssAnonKb:": 0x40, + "ParentRssAnonKb:": 0x20, + }, +} +RssStats { + per_pid: { + 0x172f8c: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x172fc5: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x172f8c: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x172fc5: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + }, +} diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap new file mode 100644 index 00000000..9e1fd672 --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap @@ -0,0 +1,32 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "MaxRssKb:": 0x40, + "RssAnon:": 0x0, + "RssFile:": 0x0, + "RssShmem:": 0x40, + }, +} +RssStats { + per_pid: { + 0x172f96: PidRss { + file_mib: 0x0, + anon_mib: 0x0, + shmem_mib: 0x40, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x172f96: PidRss { + file_mib: 0x0, + anon_mib: 0x0, + shmem_mib: 0x0, + max_rss_mib: 0x0, + }, + }, +} diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap new file mode 100644 index 00000000..9cc9275c --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap @@ -0,0 +1,32 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "MaxRssKb:": 0x90, + "RssAnon:": 0x40, + "RssFile:": 0x0, + "RssShmem:": 0x0, + }, +} +RssStats { + per_pid: { + 0x1734e4: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1734e4: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} From 6ffcb4937759242d09c9cca940238e7dab548820 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 18:24:39 +0200 Subject: [PATCH 11/12] feat(memtrack): emit fork/exec/exit lifecycle events 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. --- crates/memtrack/src/ebpf/c/event.h | 8 ++++ crates/memtrack/src/ebpf/c/memtrack.bpf.c | 43 +++++++++++++++++++ crates/memtrack/src/ebpf/events.rs | 35 +++++++++++++++ crates/memtrack/src/ebpf/memtrack.rs | 6 +++ .../runner-shared/src/artifacts/memtrack.rs | 5 +++ 5 files changed, 97 insertions(+) diff --git a/crates/memtrack/src/ebpf/c/event.h b/crates/memtrack/src/ebpf/c/event.h index 6667fd10..33b87b6c 100644 --- a/crates/memtrack/src/ebpf/c/event.h +++ b/crates/memtrack/src/ebpf/c/event.h @@ -11,6 +11,9 @@ #define EVENT_TYPE_BRK 8 #define EVENT_TYPE_RSS 9 #define EVENT_TYPE_RMAP_ANON 10 +#define EVENT_TYPE_FORK 11 +#define EVENT_TYPE_EXEC 12 +#define EVENT_TYPE_EXIT 13 /* Common header shared by all event types */ struct event_header { @@ -57,6 +60,11 @@ struct event { int64_t delta; uint64_t addr; } rmap; + + /* Process lifecycle events (fork carries the parent; exec/exit have no payload) */ + struct { + uint32_t parent_pid; + } fork; } data; }; diff --git a/crates/memtrack/src/ebpf/c/memtrack.bpf.c b/crates/memtrack/src/ebpf/c/memtrack.bpf.c index 066b00be..6363160b 100644 --- a/crates/memtrack/src/ebpf/c/memtrack.bpf.c +++ b/crates/memtrack/src/ebpf/c/memtrack.bpf.c @@ -401,4 +401,47 @@ int tracepoint_sys_exit_brk(struct trace_event_raw_sys_exit* ctx) { return submit_mmap_event(new_brk, 0, EVENT_TYPE_BRK); } +/* == Process lifecycle events == + * + * FORK lets userland seed a child's RSS from its parent at fork time: the + * kernel copies the mm counters during dup_mmap, but those updates fire + * rss_stat out of the child's context and anon COW faults are + * counter-neutral, so a child that only touches inherited memory never + * reports its RSS on its own. EXEC and EXIT mark the points where the + * address space is replaced or torn down, so userland resets to zero. + */ + +#define CLONE_THREAD 0x00010000 + +SEC("tracepoint/task/task_newtask") +int tracepoint_task_newtask(struct trace_event_raw_task_newtask* ctx) { + if (ctx->clone_flags & CLONE_THREAD) { + return 0; + } + + __u64 tid = bpf_get_current_pid_tgid(); + __u32 parent_pid = tid >> 32; + if (!is_tracked(parent_pid)) { + return 0; + } + + __u32 child_pid = ctx->pid; + SUBMIT_EVENT_AS(child_pid, EVENT_TYPE_FORK, { e->data.fork.parent_pid = parent_pid; }); +} + +SEC("tracepoint/sched/sched_process_exec") +int tracepoint_sched_process_exec(void* ctx) { + SUBMIT_EVENT(EVENT_TYPE_EXEC, {}); +} + +SEC("tracepoint/sched/sched_process_exit") +int tracepoint_sched_process_exit(void* ctx) { + __u64 tid = bpf_get_current_pid_tgid(); + if ((tid >> 32) != (tid & 0xFFFFFFFF)) { + return 0; + } + + SUBMIT_EVENT(EVENT_TYPE_EXIT, {}); +} + #include "rss.bpf.h" diff --git a/crates/memtrack/src/ebpf/events.rs b/crates/memtrack/src/ebpf/events.rs index ca619246..91157a62 100644 --- a/crates/memtrack/src/ebpf/events.rs +++ b/crates/memtrack/src/ebpf/events.rs @@ -87,6 +87,14 @@ pub fn parse_event(data: &[u8]) -> Option { delta: event.data.rmap.delta, }, ), + EVENT_TYPE_FORK => ( + 0, + MemtrackEventKind::Fork { + parent_pid: event.data.fork.parent_pid as i32, + }, + ), + EVENT_TYPE_EXEC => (0, MemtrackEventKind::Exec), + EVENT_TYPE_EXIT => (0, MemtrackEventKind::Exit), unknown => { panic!("Unknown event type: {unknown}"); } @@ -232,4 +240,31 @@ mod tests { _ => panic!("Expected RmapAnon 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 = 1001; + event.header.tid = 2000; + event.data.fork.parent_pid = 1000; + + 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, 1001); + + match parsed.kind { + MemtrackEventKind::Fork { parent_pid } => { + assert_eq!(parent_pid, 1000); + } + _ => panic!("Expected Fork event kind"), + } + } } diff --git a/crates/memtrack/src/ebpf/memtrack.rs b/crates/memtrack/src/ebpf/memtrack.rs index 22663a61..8828861a 100644 --- a/crates/memtrack/src/ebpf/memtrack.rs +++ b/crates/memtrack/src/ebpf/memtrack.rs @@ -551,6 +551,9 @@ impl MemtrackBpf { } attach_tracepoint!(sched_fork); attach_tracepoint!(rss_stat); + attach_tracepoint!(task_newtask); + attach_tracepoint!(sched_process_exec); + attach_tracepoint!(sched_process_exit); attach_fentry!( attach_fentry_folio_add_new_anon_rmap, fentry_folio_add_new_anon_rmap @@ -582,6 +585,9 @@ impl MemtrackBpf { pub fn attach_tracepoints(&mut self) -> Result<()> { self.attach_sched_fork()?; + self.attach_task_newtask()?; + self.attach_sched_process_exec()?; + self.attach_sched_process_exit()?; if let Err(e) = self.attach_rss_stat() { warn!("Failed to attach rss_stat tracepoint, RSS collection disabled: {e:#}"); } diff --git a/crates/runner-shared/src/artifacts/memtrack.rs b/crates/runner-shared/src/artifacts/memtrack.rs index 907a70c9..9eaa1691 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -79,6 +79,11 @@ pub enum MemtrackEventKind { RmapAnon { delta: i64, }, + Fork { + parent_pid: pid_t, + }, + Exec, + Exit, } pub struct MemtrackEventStream { From 41945a515a70cb9c7caa9cbe7d62014a1c99aaf3 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 14 Jul 2026 18:24:53 +0200 Subject: [PATCH 12/12] test(memtrack): validate fork-seeded child RSS reconstruction fork_idle's child only touches inherited memory (counter-neutral COW, no allocations), so its 32 MiB is observable exclusively through the fork-event seed; the parent samples /proc//status via a pipe handshake since any child-side allocation would emit a self-correcting absolute and defeat the test. --- crates/memtrack/testdata/rss/fork_idle.c | 49 ++++++++ crates/memtrack/testdata/rss/rss_report.h | 11 +- crates/memtrack/tests/rss_tests.rs | 106 +++++++++++++----- .../tests/snapshots/rss_tests__rss_anon.snap | 4 +- .../tests/snapshots/rss_tests__rss_file.snap | 4 +- .../tests/snapshots/rss_tests__rss_fork.snap | 12 +- .../snapshots/rss_tests__rss_fork_idle.snap | 45 ++++++++ .../tests/snapshots/rss_tests__rss_shmem.snap | 4 +- .../snapshots/rss_tests__rss_triangle.snap | 6 +- 9 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 crates/memtrack/testdata/rss/fork_idle.c create mode 100644 crates/memtrack/tests/snapshots/rss_tests__rss_fork_idle.snap diff --git a/crates/memtrack/testdata/rss/fork_idle.c b/crates/memtrack/testdata/rss/fork_idle.c new file mode 100644 index 00000000..0c5e5643 --- /dev/null +++ b/crates/memtrack/testdata/rss/fork_idle.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include +#include + +#include "rss_report.h" + +/* The child only touches memory inherited from the parent: COW faults of anon + * pages are counter-neutral, so the child never reports its own RSS. Its + * footprint is only observable through fork-event seeding. The child must not + * allocate (no stdio/malloc), so the parent samples /proc//status while + * the child blocks on a pipe. */ +int main(int argc, char** argv) { + if (argc != 2) return 1; + sleep(1); + size_t len = 0x20 * 1024 * 1024; + void* mem = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mem == MAP_FAILED) return 1; + memset(mem, 0x42, len); + + int ready[2]; + int release[2]; + if (pipe(ready) || pipe(release)) return 1; + + pid_t pid = fork(); + if (pid < 0) return 1; + if (pid == 0) { + memset(mem, 0x24, len); + char b = 1; + if (write(ready[1], &b, 1) != 1) _exit(1); + if (read(release[0], &b, 1) != 1) _exit(1); + _exit(0); + } + + char b; + if (read(ready[0], &b, 1) != 1) return 1; + long child_anon = rss_status_kb_pid(pid, "RssAnon:"); + int ret = write_rss_report(argv[1]); + FILE* report = fopen(argv[1], "a"); + if (!report) return 1; + fprintf(report, "ChildRssAnon: %ld\n", child_anon); + fclose(report); + if (write(release[1], &b, 1) != 1) return 1; + + int status; + if (waitpid(pid, &status, 0) < 0) return 1; + return ret || child_anon < 0 || !WIFEXITED(status) || WEXITSTATUS(status) != 0; +} diff --git a/crates/memtrack/testdata/rss/rss_report.h b/crates/memtrack/testdata/rss/rss_report.h index 237062e7..a7ee1303 100644 --- a/crates/memtrack/testdata/rss/rss_report.h +++ b/crates/memtrack/testdata/rss/rss_report.h @@ -3,10 +3,13 @@ #include #include +#include #include -static long rss_status_kb(const char* key) { - FILE* status = fopen("/proc/self/status", "r"); +static long rss_status_kb_pid(int pid, const char* key) { + char status_path[64]; + snprintf(status_path, sizeof(status_path), "/proc/%d/status", pid); + FILE* status = fopen(status_path, "r"); if (!status) return -1; char line[256]; long kb = -1; @@ -20,6 +23,10 @@ static long rss_status_kb(const char* key) { return kb; } +static long rss_status_kb(const char* key) { + return rss_status_kb_pid(getpid(), key); +} + static int write_rss_report(const char* path) { long anon = rss_status_kb("RssAnon:"); long file = rss_status_kb("RssFile:"); diff --git a/crates/memtrack/tests/rss_tests.rs b/crates/memtrack/tests/rss_tests.rs index fa3b78f0..c27db1a6 100644 --- a/crates/memtrack/tests/rss_tests.rs +++ b/crates/memtrack/tests/rss_tests.rs @@ -56,29 +56,63 @@ struct PidAccum { max_rss: u64, } +impl PidAccum { + fn update_peaks(&mut self) { + for (peak, latest) in self.peaks.iter_mut().zip(self.latest) { + *peak = (*peak).max(latest); + } + self.max_rss = self + .max_rss + .max(self.latest[0] + self.latest[1] + self.latest[3]); + } +} + impl RssStats { fn from_events(events: &[MemtrackEvent]) -> Self { - let mut rss: Vec<&MemtrackEvent> = events + let timeline: Vec<&MemtrackEvent> = events .iter() - .filter(|event| matches!(event.kind, MemtrackEventKind::Rss { .. })) + .filter(|event| { + matches!( + event.kind, + MemtrackEventKind::Rss { .. } + | MemtrackEventKind::Fork { .. } + | MemtrackEventKind::Exec + | MemtrackEventKind::Exit + ) + }) + .sorted_by_key(|event| event.timestamp) .collect(); - rss.sort_by_key(|event| event.timestamp); let mut accum: BTreeMap = BTreeMap::new(); - for event in rss { - let MemtrackEventKind::Rss { member, size } = event.kind else { - continue; - }; - let Ok(index) = usize::try_from(member) else { - continue; - }; - if index >= 4 { - continue; + for event in timeline { + match event.kind { + MemtrackEventKind::Rss { member, size } => { + let Ok(index) = usize::try_from(member) else { + continue; + }; + if index >= 4 { + continue; + } + let acc = accum.entry(event.pid).or_default(); + acc.latest[index] = size; + acc.update_peaks(); + } + MemtrackEventKind::Fork { parent_pid } => { + let seed = accum + .get(&parent_pid) + .map(|parent| parent.latest) + .unwrap_or_default(); + let acc = accum.entry(event.pid).or_default(); + acc.latest = seed; + acc.update_peaks(); + } + MemtrackEventKind::Exec | MemtrackEventKind::Exit => { + if let Some(acc) = accum.get_mut(&event.pid) { + acc.latest = [0; 4]; + } + } + _ => {} } - let acc = accum.entry(event.pid).or_default(); - acc.peaks[index] = acc.peaks[index].max(size); - acc.latest[index] = size; - acc.max_rss = acc.max_rss.max(acc.latest[0] + acc.latest[1] + acc.latest[3]); } let per_pid = accum @@ -110,20 +144,41 @@ struct RssRmapStats { impl RssRmapStats { fn from_events(events: &[MemtrackEvent]) -> Self { - let rmap: Vec<&MemtrackEvent> = events + let timeline: Vec<&MemtrackEvent> = events .iter() - .filter(|event| matches!(event.kind, MemtrackEventKind::RmapAnon { .. })) + .filter(|event| { + matches!( + event.kind, + MemtrackEventKind::RmapAnon { .. } + | MemtrackEventKind::Fork { .. } + | MemtrackEventKind::Exec + | MemtrackEventKind::Exit + ) + }) .sorted_by_key(|event| event.timestamp) .collect(); let mut accum: BTreeMap = BTreeMap::new(); - for event in rmap { - let MemtrackEventKind::RmapAnon { delta } = event.kind else { - continue; - }; - let (total, max_rss) = accum.entry(event.pid).or_default(); - *total += delta * 4096; - *max_rss = (*max_rss).max(*total); + for event in timeline { + match event.kind { + MemtrackEventKind::RmapAnon { delta } => { + let (total, max_rss) = accum.entry(event.pid).or_default(); + *total += delta * 4096; + *max_rss = (*max_rss).max(*total); + } + MemtrackEventKind::Fork { parent_pid } => { + let seed = accum.get(&parent_pid).map(|(total, _)| *total).unwrap_or(0); + let (total, max_rss) = accum.entry(event.pid).or_default(); + *total = seed; + *max_rss = (*max_rss).max(*total); + } + MemtrackEventKind::Exec | MemtrackEventKind::Exit => { + if let Some((total, _)) = accum.get_mut(&event.pid) { + *total = 0; + } + } + _ => {} + } } let per_pid = accum @@ -174,6 +229,7 @@ fn track_fixture( #[case::file(include_str!("../testdata/rss/file.c"), "file")] #[case::shmem(include_str!("../testdata/rss/shmem.c"), "shmem")] #[case::fork(include_str!("../testdata/rss/fork.c"), "fork")] +#[case::fork_idle(include_str!("../testdata/rss/fork_idle.c"), "fork_idle")] #[case::triangle(include_str!("../testdata/rss/triangle.c"), "triangle")] fn test_rss_rmap_tracking( #[case] source: &str, diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap index 340684dd..0bed65f2 100644 --- a/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_anon.snap @@ -12,7 +12,7 @@ RssReport { } RssStats { per_pid: { - 0x172f89: PidRss { + 0x1b1a24: PidRss { file_mib: 0x0, anon_mib: 0x40, shmem_mib: 0x0, @@ -22,7 +22,7 @@ RssStats { } RssRmapStats { per_pid: { - 0x172f89: PidRss { + 0x1b1a24: PidRss { file_mib: 0x0, anon_mib: 0x40, shmem_mib: 0x0, diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap index a3e7ae17..05e59c88 100644 --- a/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_file.snap @@ -12,7 +12,7 @@ RssReport { } RssStats { per_pid: { - 0x172f8f: PidRss { + 0x1b1cb8: PidRss { file_mib: 0x40, anon_mib: 0x0, shmem_mib: 0x0, @@ -22,7 +22,7 @@ RssStats { } RssRmapStats { per_pid: { - 0x172f8f: PidRss { + 0x1b1cb8: PidRss { file_mib: 0x0, anon_mib: 0x0, shmem_mib: 0x0, diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap index 8b71d32b..84d135fc 100644 --- a/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_fork.snap @@ -10,13 +10,13 @@ RssReport { } RssStats { per_pid: { - 0x172f8c: PidRss { + 0x1b2218: PidRss { file_mib: 0x0, anon_mib: 0x20, shmem_mib: 0x0, max_rss_mib: 0x20, }, - 0x172fc5: PidRss { + 0x1b225f: PidRss { file_mib: 0x0, anon_mib: 0x40, shmem_mib: 0x0, @@ -26,17 +26,17 @@ RssStats { } RssRmapStats { per_pid: { - 0x172f8c: PidRss { + 0x1b2218: PidRss { file_mib: 0x0, anon_mib: 0x20, shmem_mib: 0x0, max_rss_mib: 0x20, }, - 0x172fc5: PidRss { + 0x1b225f: PidRss { file_mib: 0x0, - anon_mib: 0x20, + anon_mib: 0x40, shmem_mib: 0x0, - max_rss_mib: 0x20, + max_rss_mib: 0x40, }, }, } diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_fork_idle.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_fork_idle.snap new file mode 100644 index 00000000..72cffa42 --- /dev/null +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_fork_idle.snap @@ -0,0 +1,45 @@ +--- +source: crates/memtrack/tests/rss_tests.rs +expression: summary +--- +RssReport { + proc_mib: { + "ChildRssAnon:": 0x20, + "MaxRssKb:": 0x80, + "RssAnon:": 0x20, + "RssFile:": 0x0, + "RssShmem:": 0x0, + }, +} +RssStats { + per_pid: { + 0x1b24da: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x1b250a: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b24da: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x1b250a: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + }, +} diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap index 9e1fd672..dfc81bcb 100644 --- a/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_shmem.snap @@ -12,7 +12,7 @@ RssReport { } RssStats { per_pid: { - 0x172f96: PidRss { + 0x1b1f92: PidRss { file_mib: 0x0, anon_mib: 0x0, shmem_mib: 0x40, @@ -22,7 +22,7 @@ RssStats { } RssRmapStats { per_pid: { - 0x172f96: PidRss { + 0x1b1f92: PidRss { file_mib: 0x0, anon_mib: 0x0, shmem_mib: 0x0, diff --git a/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap b/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap index 9cc9275c..a6a03e15 100644 --- a/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap +++ b/crates/memtrack/tests/snapshots/rss_tests__rss_triangle.snap @@ -4,7 +4,7 @@ expression: summary --- RssReport { proc_mib: { - "MaxRssKb:": 0x90, + "MaxRssKb:": 0xa0, "RssAnon:": 0x40, "RssFile:": 0x0, "RssShmem:": 0x0, @@ -12,7 +12,7 @@ RssReport { } RssStats { per_pid: { - 0x1734e4: PidRss { + 0x1b275e: PidRss { file_mib: 0x0, anon_mib: 0x40, shmem_mib: 0x0, @@ -22,7 +22,7 @@ RssStats { } RssRmapStats { per_pid: { - 0x1734e4: PidRss { + 0x1b275e: PidRss { file_mib: 0x0, anon_mib: 0x40, shmem_mib: 0x0,