|
| 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