running_process_probe/snapshot/
attribute.rs1use super::modules::LoadedModule;
23use super::Snapshot;
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct AttributedModule {
28 pub name: String,
30 pub path: Option<String>,
32 pub debug_id: Option<String>,
34 pub debug_file: Option<String>,
36 pub base: u64,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct AttributedFrame {
44 pub module_index: Option<u32>,
47 pub relative_address: u64,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct AttributedThread {
57 pub os_tid: u64,
59 pub frames: Vec<AttributedFrame>,
61}
62
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
65pub struct AttributedCapture {
66 pub modules: Vec<AttributedModule>,
71 pub threads: Vec<AttributedThread>,
73}
74
75impl AttributedCapture {
76 pub fn unattributed_frames(&self) -> usize {
82 self.threads
83 .iter()
84 .flat_map(|t| &t.frames)
85 .filter(|f| f.module_index.is_none())
86 .count()
87 }
88}
89
90pub fn attribute(snapshot: &Snapshot, modules: &[LoadedModule]) -> AttributedCapture {
95 let mut out = AttributedCapture::default();
96 let mut index_by_base: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
99
100 for sample in &snapshot.threads {
101 let mut frames = Vec::with_capacity(sample.frames.len());
102 for &address in &sample.frames {
103 match modules.iter().find(|m| m.contains(address)) {
104 Some(module) => {
105 let next = u32::try_from(out.modules.len()).unwrap_or(u32::MAX);
106 let index = *index_by_base.entry(module.base).or_insert_with(|| {
107 out.modules.push(AttributedModule {
108 name: module_name(module),
109 path: module.path.clone(),
110 debug_id: module.debug_id.clone(),
111 debug_file: module.debug_file.clone(),
112 base: module.base,
113 });
114 next
115 });
116 frames.push(AttributedFrame {
117 module_index: Some(index),
118 relative_address: address - module.base,
119 });
120 }
121 None => frames.push(AttributedFrame {
125 module_index: None,
126 relative_address: address,
127 }),
128 }
129 }
130 out.threads.push(AttributedThread {
131 os_tid: sample.os_tid,
132 frames,
133 });
134 }
135 out
136}
137
138fn module_name(module: &LoadedModule) -> String {
139 module
140 .path
141 .as_deref()
142 .and_then(|path| path.rsplit(['/', '\\']).find(|part| !part.is_empty()))
145 .map(str::to_owned)
146 .unwrap_or_else(|| format!("{:#x}", module.base))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::snapshot::modules::Section;
153 use crate::snapshot::{CaptureKind, ThreadSample};
154
155 fn module(base: u64, size: u64, path: Option<&str>) -> LoadedModule {
156 LoadedModule {
157 base,
158 size,
159 mapped_ranges: Vec::new(),
160 executable_ranges: Vec::new(),
161 path: path.map(str::to_owned),
162 debug_id: None,
163 debug_file: None,
164 sections: Vec::<Section>::new(),
165 }
166 }
167
168 fn snapshot_with(threads: Vec<(u64, Vec<u64>)>) -> Snapshot {
169 Snapshot {
170 threads: threads
171 .into_iter()
172 .map(|(os_tid, frames)| ThreadSample {
173 os_tid,
174 stack_pointer: 0,
175 instruction_pointer: 0,
176 frame_pointer: 0,
177 link_register: None,
178 stack_bytes: Vec::new(),
179 truncated: false,
180 kind: CaptureKind::RawContext,
181 frames,
182 })
183 .collect(),
184 ..Default::default()
185 }
186 }
187
188 #[test]
189 fn an_address_becomes_an_offset_from_its_module() {
190 let modules = vec![module(0x1000, 0x1000, Some(r"C:\app\a.dll"))];
191 let capture = attribute(&snapshot_with(vec![(7, vec![0x1234])]), &modules);
192
193 assert_eq!(capture.modules.len(), 1);
194 assert_eq!(capture.modules[0].name, "a.dll");
195 assert_eq!(capture.threads[0].frames[0].module_index, Some(0));
196 assert_eq!(capture.threads[0].frames[0].relative_address, 0x234);
197 }
198
199 #[test]
202 fn an_address_outside_every_module_is_left_unattributed() {
203 let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
204 let capture = attribute(&snapshot_with(vec![(7, vec![0x9999])]), &modules);
205
206 assert!(capture.modules.is_empty(), "no module was referenced");
207 assert_eq!(capture.threads[0].frames[0].module_index, None);
208 assert_eq!(
209 capture.threads[0].frames[0].relative_address, 0x9999,
210 "the raw address must survive so the frame is not lost"
211 );
212 assert_eq!(capture.unattributed_frames(), 1);
213 }
214
215 #[test]
217 fn each_address_lands_in_its_own_module() {
218 let modules = vec![
219 module(0x1000, 0x1000, Some("a.dll")),
220 module(0x8000, 0x1000, Some("b.dll")),
221 ];
222 let capture = attribute(&snapshot_with(vec![(7, vec![0x8100, 0x1100])]), &modules);
223
224 let by_name: Vec<_> = capture.threads[0]
225 .frames
226 .iter()
227 .map(|f| {
228 (
229 capture.modules[f.module_index.unwrap() as usize]
230 .name
231 .as_str(),
232 f.relative_address,
233 )
234 })
235 .collect();
236 assert_eq!(by_name, vec![("b.dll", 0x100), ("a.dll", 0x100)]);
237 }
238
239 #[test]
240 fn a_module_referenced_twice_is_listed_once() {
241 let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
242 let capture = attribute(
243 &snapshot_with(vec![(7, vec![0x1100, 0x1200, 0x1300])]),
244 &modules,
245 );
246
247 assert_eq!(capture.modules.len(), 1, "one module, three frames");
248 for frame in &capture.threads[0].frames {
249 assert_eq!(frame.module_index, Some(0));
250 }
251 }
252
253 #[test]
255 fn only_referenced_modules_are_listed() {
256 let modules = vec![
257 module(0x1000, 0x1000, Some("used.dll")),
258 module(0x8000, 0x1000, Some("unused.dll")),
259 ];
260 let capture = attribute(&snapshot_with(vec![(7, vec![0x1100])]), &modules);
261
262 assert_eq!(capture.modules.len(), 1);
263 assert_eq!(capture.modules[0].name, "used.dll");
264 }
265
266 #[test]
267 fn thread_identity_and_order_survive() {
268 let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
269 let capture = attribute(
270 &snapshot_with(vec![(100, vec![0x1100]), (200, vec![0x1200])]),
271 &modules,
272 );
273 assert_eq!(capture.threads[0].os_tid, 100);
274 assert_eq!(capture.threads[1].os_tid, 200);
275 }
276
277 #[test]
279 fn a_pathless_module_is_named_by_its_base() {
280 let modules = vec![module(0x4000, 0x1000, None)];
281 let capture = attribute(&snapshot_with(vec![(7, vec![0x4010])]), &modules);
282 assert_eq!(capture.modules[0].name, "0x4000");
283 assert_eq!(capture.modules[0].path, None);
284 }
285
286 #[cfg(windows)]
288 #[test]
289 fn a_real_capture_attributes_most_of_its_frames() {
290 use crate::snapshot::modules::enumerate_modules;
291 use crate::snapshot::{capture_and_resolve, SnapshotConfig};
292
293 let snapshot = capture_and_resolve(&SnapshotConfig::default()).expect("capture");
294 let modules = enumerate_modules().expect("modules");
295 let capture = attribute(&snapshot, &modules);
296
297 let total: usize = capture.threads.iter().map(|t| t.frames.len()).sum();
298 if total == 0 {
299 assert!(
304 std::env::var_os("GITHUB_ACTIONS").is_none(),
305 "captured no frames during a CI run; this test would assert nothing"
306 );
307 return;
308 }
309 for thread in &capture.threads {
322 for frame in &thread.frames {
323 let Some(index) = frame.module_index else {
324 continue;
325 };
326 let module = &capture.modules[index as usize];
327 let size = modules
328 .iter()
329 .find(|m| m.base == module.base)
330 .map(|m| m.size)
331 .expect("attributed module must come from the inventory");
332 assert!(
333 frame.relative_address < size,
334 "offset {:#x} exceeds {}'s size {size:#x}; the frame was attributed to the wrong module",
335 frame.relative_address,
336 module.name
337 );
338 }
339 }
340
341 let attributed = total - capture.unattributed_frames();
344 assert!(
345 attributed > 0,
346 "no frame of {total} matched any module; attribution is not working"
347 );
348 assert!(
349 !capture.modules.is_empty(),
350 "attributed frames but listed no modules"
351 );
352 }
353}