Skip to content

Commit b670b0a

Browse files
committed
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%.
1 parent 9c4be14 commit b670b0a

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#[macro_use]
2+
mod shared;
3+
4+
use runner_shared::artifacts::{ArtifactExt, MemtrackArtifact, MemtrackEventKind};
5+
use std::fs::File;
6+
use std::path::PathBuf;
7+
use std::process::Command;
8+
use tempfile::TempDir;
9+
10+
const RSS_TRIANGLE_SOURCE: &str = r#"
11+
#include <string.h>
12+
#include <unistd.h>
13+
#include <sys/mman.h>
14+
15+
int main(void) {
16+
const size_t chunk = 16UL * 1024 * 1024;
17+
void* bufs[4];
18+
for (int i = 0; i < 4; i++) {
19+
bufs[i] = mmap(NULL, chunk, PROT_READ | PROT_WRITE,
20+
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
21+
if (bufs[i] == MAP_FAILED) return 1;
22+
memset(bufs[i], 0x42, chunk);
23+
usleep(50 * 1000);
24+
}
25+
for (int i = 0; i < 4; i++) {
26+
munmap(bufs[i], chunk);
27+
usleep(50 * 1000);
28+
}
29+
return 0;
30+
}
31+
"#;
32+
33+
#[test_with::env(GITHUB_ACTIONS)]
34+
#[test_log::test]
35+
fn test_rss_reconstruction() -> Result<(), Box<dyn std::error::Error>> {
36+
unsafe {
37+
std::env::set_var("CODSPEED_MEMTRACK_TRACK_RMAP", "1");
38+
}
39+
40+
let temp_dir = TempDir::new()?;
41+
let binary = shared::compile_c_source(RSS_TRIANGLE_SOURCE, "rss_triangle", temp_dir.path())?;
42+
43+
let (mut events, thread_handle) = shared::track_command(Command::new(&binary), &[], false)?;
44+
45+
let artifact_path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
46+
.join("rss_reconstruction.MemtrackArtifact.msgpack");
47+
let artifact = MemtrackArtifact {
48+
events: events.clone(),
49+
};
50+
artifact.encode_to_writer(File::create(&artifact_path)?)?;
51+
eprintln!("rss artifact: {}", artifact_path.display());
52+
53+
events.sort_by_key(|e| e.timestamp);
54+
55+
const PAGE_SIZE: i64 = 4096;
56+
57+
let real_anon: Vec<i64> = events
58+
.iter()
59+
.filter_map(|e| match e.kind {
60+
MemtrackEventKind::Rss { member: 1, size } => Some(size as i64),
61+
_ => None,
62+
})
63+
.collect();
64+
65+
let rmap_deltas: Vec<i64> = events
66+
.iter()
67+
.filter_map(|e| match e.kind {
68+
MemtrackEventKind::RmapAnon { delta } => Some(delta),
69+
_ => None,
70+
})
71+
.collect();
72+
73+
assert!(
74+
!real_anon.is_empty(),
75+
"expected at least one MM_ANONPAGES rss_stat event; got none (rss_stat hook did not fire)"
76+
);
77+
assert!(
78+
!rmap_deltas.is_empty(),
79+
"expected at least one RmapAnon event; got none (folio rmap fentry hooks did not attach/fire)"
80+
);
81+
82+
let real_peak = *real_anon.iter().max().unwrap();
83+
let real_base = *real_anon.iter().min().unwrap();
84+
let real_amplitude = real_peak - real_base;
85+
86+
let mut cum: i64 = 0;
87+
let mut est_peak: i64 = 0;
88+
for delta in &rmap_deltas {
89+
cum += delta;
90+
est_peak = est_peak.max(cum * PAGE_SIZE);
91+
}
92+
93+
eprintln!(
94+
"real anon amplitude = {} MiB, estimated peak = {} MiB",
95+
real_amplitude / (1024 * 1024),
96+
est_peak / (1024 * 1024)
97+
);
98+
99+
const MIN_BYTES: i64 = 48 * 1024 * 1024;
100+
assert!(
101+
real_amplitude >= MIN_BYTES,
102+
"real anon RSS amplitude {real_amplitude} bytes < {MIN_BYTES}"
103+
);
104+
assert!(
105+
est_peak >= MIN_BYTES,
106+
"estimated anon RSS peak {est_peak} bytes < {MIN_BYTES}"
107+
);
108+
109+
let low = (real_amplitude as f64 * 0.75) as i64;
110+
let high = (real_amplitude as f64 * 1.25) as i64;
111+
assert!(
112+
(low..=high).contains(&est_peak),
113+
"estimated peak {est_peak} not within [0.75, 1.25] x real amplitude {real_amplitude} ([{low}, {high}])"
114+
);
115+
116+
thread_handle.join().unwrap();
117+
Ok(())
118+
}

0 commit comments

Comments
 (0)