diff --git a/crates/memtrack/src/ebpf/c/event.h b/crates/memtrack/src/ebpf/c/event.h index f914f634..33b87b6c 100644 --- a/crates/memtrack/src/ebpf/c/event.h +++ b/crates/memtrack/src/ebpf/c/event.h @@ -9,6 +9,11 @@ #define EVENT_TYPE_MMAP 6 #define EVENT_TYPE_MUNMAP 7 #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 { @@ -45,6 +50,21 @@ struct event { uint64_t addr; /* address of mapping */ uint64_t size; /* size of mapping */ } mmap; + + struct { + int32_t member; + uint64_t size; + } rss; + + struct { + 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 06601e27..6363160b 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, { @@ -389,3 +400,48 @@ 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/c/rss.bpf.h b/crates/memtrack/src/ebpf/c/rss.bpf.h new file mode 100644 index 00000000..e3971cfe --- /dev/null +++ b/crates/memtrack/src/ebpf/c/rss.bpf.h @@ -0,0 +1,126 @@ +#ifndef __RSS_BPF_H__ +#define __RSS_BPF_H__ + +#define FOLIO_MAPPING_ANON 0x1UL + +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; + }); +} + +SEC("tracepoint/kmem/rss_stat") +int tracepoint_rss_stat(struct trace_event_raw_rss_stat* ctx) { + if (!ctx->curr) { + return 0; + } + + __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) { + 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 __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; + }); +} + +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(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(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") +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(vma, -(__s64)nr_pages, folio_page_address(folio, page, vma)); +} + +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(vma, -(__s64)folio_nr_pages_est(folio), + folio_page_address(folio, page, vma)); +} + +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(vma, -(__s64)folio_nr_pages_est(folio), + folio_page_address(folio, page, vma)); +} + +#endif /* __RSS_BPF_H__ */ diff --git a/crates/memtrack/src/ebpf/events.rs b/crates/memtrack/src/ebpf/events.rs index 3ecb7a67..91157a62 100644 --- a/crates/memtrack/src/ebpf/events.rs +++ b/crates/memtrack/src/ebpf/events.rs @@ -74,6 +74,27 @@ 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, + }, + ), + EVENT_TYPE_RMAP_ANON => ( + event.data.rmap.addr, + MemtrackEventKind::RmapAnon { + 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}"); } @@ -160,4 +181,90 @@ 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"), + } + } + + #[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"), + } + } + + #[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 3a1bfa26..8828861a 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. /// @@ -172,20 +192,101 @@ 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, + btf_disabled_rmap_targets: Vec<&'static str>, } 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())); - let open_skel = builder + let mut open_skel = builder .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 + .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_add_anon_rmap_pmd + .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); + } 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( open_skel .load() @@ -195,6 +296,8 @@ impl MemtrackBpf { Ok(Self { skel, probes: Vec::new(), + track_rmap, + btf_disabled_rmap_targets, }) } @@ -447,9 +550,67 @@ impl MemtrackBpf { Ok(()) } 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 + ); + attach_fentry!( + 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 + ); + 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 + ); + + 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()?; + 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:#}"); + } + if self.track_rmap { + 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(()) } 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 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/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 new file mode 100644 index 00000000..a7ee1303 --- /dev/null +++ b/crates/memtrack/testdata/rss/rss_report.h @@ -0,0 +1,48 @@ +#ifndef RSS_REPORT_H +#define RSS_REPORT_H + +#include +#include +#include +#include + +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; + 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 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:"); + 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_tests.rs b/crates/memtrack/tests/rss_tests.rs new file mode 100644 index 00000000..c27db1a6 --- /dev/null +++ b/crates/memtrack/tests/rss_tests.rs @@ -0,0 +1,246 @@ +#[macro_use] +mod shared; + +use itertools::Itertools; +use rstest::rstest; +use runner_shared::artifacts::{MemtrackEvent, MemtrackEventKind}; +use std::collections::BTreeMap; +use std::process::Command; +use tempfile::TempDir; + +const MIB: u64 = 1024 * 1024; + +fn mib_16(bytes: u64) -> u64 { + (bytes + 8 * MIB) / (16 * MIB) * 16 +} + +#[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 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 timeline: Vec<&MemtrackEvent> = events + .iter() + .filter(|event| { + matches!( + event.kind, + MemtrackEventKind::Rss { .. } + | MemtrackEventKind::Fork { .. } + | MemtrackEventKind::Exec + | MemtrackEventKind::Exit + ) + }) + .sorted_by_key(|event| event.timestamp) + .collect(); + + let mut accum: BTreeMap = BTreeMap::new(); + 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 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 timeline: Vec<&MemtrackEvent> = events + .iter() + .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 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 + .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()?; + 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::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, + #[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 054b3b8e..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. /// @@ -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,21 +126,55 @@ 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() { + error!("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 /// 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)?; 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..0bed65f2 --- /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: { + 0x1b1a24: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b1a24: 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..05e59c88 --- /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: { + 0x1b1cb8: PidRss { + file_mib: 0x40, + anon_mib: 0x0, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b1cb8: 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..84d135fc --- /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: { + 0x1b2218: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x1b225f: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b2218: PidRss { + file_mib: 0x0, + anon_mib: 0x20, + shmem_mib: 0x0, + max_rss_mib: 0x20, + }, + 0x1b225f: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + 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 new file mode 100644 index 00000000..dfc81bcb --- /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: { + 0x1b1f92: PidRss { + file_mib: 0x0, + anon_mib: 0x0, + shmem_mib: 0x40, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b1f92: 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..a6a03e15 --- /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:": 0xa0, + "RssAnon:": 0x40, + "RssFile:": 0x0, + "RssShmem:": 0x0, + }, +} +RssStats { + per_pid: { + 0x1b275e: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} +RssRmapStats { + per_pid: { + 0x1b275e: PidRss { + file_mib: 0x0, + anon_mib: 0x40, + shmem_mib: 0x0, + max_rss_mib: 0x40, + }, + }, +} diff --git a/crates/runner-shared/benches/memtrack_writer.rs b/crates/runner-shared/benches/memtrack_writer.rs index 482b2a7b..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..8) { + let kind = match rng.gen_range(0..10) { 0 => MemtrackEventKind::Malloc { size }, 1 => MemtrackEventKind::Free, 2 => MemtrackEventKind::Realloc { @@ -25,6 +25,13 @@ 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, + }, + 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 64c90e82..9eaa1691 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -72,6 +72,18 @@ pub enum MemtrackEventKind { Brk { size: u64, }, + Rss { + member: i32, + size: u64, + }, + RmapAnon { + delta: i64, + }, + Fork { + parent_pid: pid_t, + }, + Exec, + Exit, } pub struct MemtrackEventStream { @@ -147,6 +159,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 {