Skip to content

Commit 2bbb062

Browse files
committed
test(runner-shared): benchmark finding module events in a memtrack artifact
Since 5.4.0 the memory executor decodes every event of the memtrack artifact after the run, only to keep the few Mapping, Fork and Exec events it needs for module artifacts. On large artifacts this takes minutes between memtrack's last log line and the upload. The benchmark builds an artifact laid out like memtrack writes it: allocation events with forks and execs spread through the stream and the mapping suffix at the end. It covers artifacts without stacks and with one 8 KiB stack record every 50 events, and checks that the expected number of events is found.
1 parent bfe5aee commit 2bbb062

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

‎crates/runner-shared/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ rand = "0.8"
3232
[[bench]]
3333
name = "memtrack_writer"
3434
harness = false
35+
36+
[[bench]]
37+
name = "memtrack_reader"
38+
harness = false
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//! Finding the module events (mappings, forks, execs) the runner needs for
2+
//! offline stack attribution inside a full memtrack artifact. These are a
3+
//! handful of records among millions of allocation events, so the cost is
4+
//! dominated by everything the search has to read past.
5+
6+
use divan::Bencher;
7+
use divan::counter::{BytesCount, ItemsCount};
8+
use rand::rngs::StdRng;
9+
use rand::{Rng, SeedableRng};
10+
use runner_shared::artifacts::{
11+
MemtrackArtifact, MemtrackEvent, MemtrackEventKind, StackRecord, encode_events,
12+
};
13+
14+
fn main() {
15+
divan::main();
16+
}
17+
18+
/// Four encoder frames: enough to cross frame boundaries, small enough to stay
19+
/// fast under the simulation instrument.
20+
const ARTIFACT_EVENTS: usize = 4 * 64 * 1024;
21+
/// Executable mappings memtrack appends after the event rings drain.
22+
const MAPPING_EVENTS: usize = 64;
23+
/// One fork or exec every this many events, spread through the stream like
24+
/// the BPF lifecycle tracepoints emit them.
25+
const LIFECYCLE_INTERVAL: usize = 16 * 1024;
26+
/// One captured stack every this many events.
27+
const STACK_INTERVAL: usize = 50;
28+
/// Raw stack bytes per record: memtrack's default stack copy budget.
29+
const STACK_BYTES: usize = 8192;
30+
/// Registers per record on x86_64.
31+
const STACK_REGS: usize = 33;
32+
const STACK_FP_FRAMES: usize = 16;
33+
const ENCODE_WORKERS: usize = 4;
34+
const ROOT_PID: i32 = 4242;
35+
36+
#[derive(Clone, Copy)]
37+
enum Payload {
38+
Allocations,
39+
AllocationsAndStacks,
40+
}
41+
42+
/// Search an artifact holding only allocation events, like a run without
43+
/// stack capture.
44+
#[divan::bench(max_time = 10.0)]
45+
fn find_module_events(bencher: Bencher) {
46+
bench_find_module_events(bencher, Payload::Allocations);
47+
}
48+
49+
/// Search an artifact that also carries captured stacks, which make up most of
50+
/// its bytes.
51+
#[divan::bench(max_time = 10.0)]
52+
fn find_module_events_with_stacks(bencher: Bencher) {
53+
bench_find_module_events(bencher, Payload::AllocationsAndStacks);
54+
}
55+
56+
fn bench_find_module_events(bencher: Bencher, payload: Payload) {
57+
let artifact = generate_artifact(payload);
58+
let lifecycle_events = (ARTIFACT_EVENTS - MAPPING_EVENTS).div_ceil(LIFECYCLE_INTERVAL);
59+
let expected = MAPPING_EVENTS + lifecycle_events;
60+
assert_eq!(count_module_events(&artifact), expected);
61+
62+
bencher
63+
.counter(ItemsCount::new(ARTIFACT_EVENTS))
64+
.counter(BytesCount::new(artifact.len()))
65+
.bench_local(|| count_module_events(&artifact));
66+
}
67+
68+
fn count_module_events(artifact: &[u8]) -> usize {
69+
MemtrackArtifact::decode_module_events(artifact)
70+
.unwrap()
71+
.count()
72+
}
73+
74+
/// An encoded artifact laid out like memtrack writes it: allocation events
75+
/// with forks and execs interleaved, followed by the mapping suffix.
76+
fn generate_artifact(payload: Payload) -> Vec<u8> {
77+
let mut rng = StdRng::seed_from_u64(42);
78+
let mut timestamp: u64 = 1_700_000_000_000_000_000;
79+
let mut events = Vec::with_capacity(ARTIFACT_EVENTS);
80+
81+
for i in 0..ARTIFACT_EVENTS - MAPPING_EVENTS {
82+
timestamp += rng.gen_range(50..2_000);
83+
let child_pid = ROOT_PID + 1 + (i / (2 * LIFECYCLE_INTERVAL)) as i32;
84+
let (pid, kind) = if i % (2 * LIFECYCLE_INTERVAL) == 0 {
85+
(
86+
child_pid,
87+
MemtrackEventKind::Fork {
88+
parent_pid: ROOT_PID,
89+
},
90+
)
91+
} else if i % LIFECYCLE_INTERVAL == 0 {
92+
(child_pid, MemtrackEventKind::Exec)
93+
} else if matches!(payload, Payload::AllocationsAndStacks) && i % STACK_INTERVAL == 0 {
94+
(ROOT_PID, stack_event(&mut rng))
95+
} else if rng.gen_bool(0.5) {
96+
(
97+
ROOT_PID,
98+
MemtrackEventKind::Malloc {
99+
size: rng.gen_range(8..8192),
100+
stack_hash: rng.r#gen(),
101+
},
102+
)
103+
} else {
104+
(ROOT_PID, MemtrackEventKind::Free)
105+
};
106+
107+
events.push(MemtrackEvent {
108+
pid,
109+
tid: pid,
110+
timestamp,
111+
addr: rng.r#gen(),
112+
kind,
113+
});
114+
}
115+
116+
for i in 0..MAPPING_EVENTS {
117+
events.push(MemtrackEvent {
118+
pid: ROOT_PID,
119+
tid: ROOT_PID,
120+
timestamp: timestamp + i as u64,
121+
addr: 0x7f00_0000_0000 + (i as u64) * 0x10_0000,
122+
kind: MemtrackEventKind::Mapping {
123+
path: format!("/usr/lib/x86_64-linux-gnu/libmodule{i}.so"),
124+
dev: 0x0800_0001,
125+
ino: i as u64,
126+
file_offset: 0x1000,
127+
len: 0x8_0000,
128+
},
129+
});
130+
}
131+
132+
let mut artifact = Vec::new();
133+
encode_events(events, &mut artifact, ENCODE_WORKERS).unwrap();
134+
artifact
135+
}
136+
137+
fn stack_event(rng: &mut StdRng) -> MemtrackEventKind {
138+
let mut bytes = vec![0u8; STACK_BYTES];
139+
rng.fill(&mut bytes[..]);
140+
MemtrackEventKind::Stack {
141+
record: Box::new(StackRecord {
142+
hash: rng.r#gen(),
143+
sp: 0x7fff_0000_0000 | (rng.gen_range(0..1u64 << 20) << 4),
144+
regs: (0..STACK_REGS).map(|_| rng.r#gen()).collect(),
145+
bytes,
146+
fp_chain: (0..STACK_FP_FRAMES).map(|_| rng.r#gen()).collect(),
147+
truncated: true,
148+
}),
149+
}
150+
}

0 commit comments

Comments
 (0)