use super::modules::LoadedModule;
use super::Snapshot;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttributedModule {
pub name: String,
pub path: Option<String>,
pub debug_id: Option<String>,
pub debug_file: Option<String>,
pub base: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AttributedFrame {
pub module_index: Option<u32>,
pub relative_address: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AttributedThread {
pub os_tid: u64,
pub frames: Vec<AttributedFrame>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AttributedCapture {
pub modules: Vec<AttributedModule>,
pub threads: Vec<AttributedThread>,
}
impl AttributedCapture {
pub fn unattributed_frames(&self) -> usize {
self.threads
.iter()
.flat_map(|t| &t.frames)
.filter(|f| f.module_index.is_none())
.count()
}
}
pub fn attribute(snapshot: &Snapshot, modules: &[LoadedModule]) -> AttributedCapture {
let mut out = AttributedCapture::default();
let mut index_by_base: std::collections::HashMap<u64, u32> = std::collections::HashMap::new();
for sample in &snapshot.threads {
let mut frames = Vec::with_capacity(sample.frames.len());
for &address in &sample.frames {
match modules.iter().find(|m| m.contains(address)) {
Some(module) => {
let next = u32::try_from(out.modules.len()).unwrap_or(u32::MAX);
let index = *index_by_base.entry(module.base).or_insert_with(|| {
out.modules.push(AttributedModule {
name: module_name(module),
path: module.path.clone(),
debug_id: module.debug_id.clone(),
debug_file: module.debug_file.clone(),
base: module.base,
});
next
});
frames.push(AttributedFrame {
module_index: Some(index),
relative_address: address - module.base,
});
}
None => frames.push(AttributedFrame {
module_index: None,
relative_address: address,
}),
}
}
out.threads.push(AttributedThread {
os_tid: sample.os_tid,
frames,
});
}
out
}
fn module_name(module: &LoadedModule) -> String {
module
.path
.as_deref()
.and_then(|path| path.rsplit(['/', '\\']).find(|part| !part.is_empty()))
.map(str::to_owned)
.unwrap_or_else(|| format!("{:#x}", module.base))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::modules::Section;
use crate::snapshot::{CaptureKind, ThreadSample};
fn module(base: u64, size: u64, path: Option<&str>) -> LoadedModule {
LoadedModule {
base,
size,
mapped_ranges: Vec::new(),
executable_ranges: Vec::new(),
path: path.map(str::to_owned),
debug_id: None,
debug_file: None,
sections: Vec::<Section>::new(),
}
}
fn snapshot_with(threads: Vec<(u64, Vec<u64>)>) -> Snapshot {
Snapshot {
threads: threads
.into_iter()
.map(|(os_tid, frames)| ThreadSample {
os_tid,
stack_pointer: 0,
instruction_pointer: 0,
frame_pointer: 0,
link_register: None,
stack_bytes: Vec::new(),
truncated: false,
kind: CaptureKind::RawContext,
frames,
})
.collect(),
..Default::default()
}
}
#[test]
fn an_address_becomes_an_offset_from_its_module() {
let modules = vec![module(0x1000, 0x1000, Some(r"C:\app\a.dll"))];
let capture = attribute(&snapshot_with(vec![(7, vec![0x1234])]), &modules);
assert_eq!(capture.modules.len(), 1);
assert_eq!(capture.modules[0].name, "a.dll");
assert_eq!(capture.threads[0].frames[0].module_index, Some(0));
assert_eq!(capture.threads[0].frames[0].relative_address, 0x234);
}
#[test]
fn an_address_outside_every_module_is_left_unattributed() {
let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
let capture = attribute(&snapshot_with(vec![(7, vec![0x9999])]), &modules);
assert!(capture.modules.is_empty(), "no module was referenced");
assert_eq!(capture.threads[0].frames[0].module_index, None);
assert_eq!(
capture.threads[0].frames[0].relative_address, 0x9999,
"the raw address must survive so the frame is not lost"
);
assert_eq!(capture.unattributed_frames(), 1);
}
#[test]
fn each_address_lands_in_its_own_module() {
let modules = vec![
module(0x1000, 0x1000, Some("a.dll")),
module(0x8000, 0x1000, Some("b.dll")),
];
let capture = attribute(&snapshot_with(vec![(7, vec![0x8100, 0x1100])]), &modules);
let by_name: Vec<_> = capture.threads[0]
.frames
.iter()
.map(|f| {
(
capture.modules[f.module_index.unwrap() as usize]
.name
.as_str(),
f.relative_address,
)
})
.collect();
assert_eq!(by_name, vec![("b.dll", 0x100), ("a.dll", 0x100)]);
}
#[test]
fn a_module_referenced_twice_is_listed_once() {
let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
let capture = attribute(
&snapshot_with(vec![(7, vec![0x1100, 0x1200, 0x1300])]),
&modules,
);
assert_eq!(capture.modules.len(), 1, "one module, three frames");
for frame in &capture.threads[0].frames {
assert_eq!(frame.module_index, Some(0));
}
}
#[test]
fn only_referenced_modules_are_listed() {
let modules = vec![
module(0x1000, 0x1000, Some("used.dll")),
module(0x8000, 0x1000, Some("unused.dll")),
];
let capture = attribute(&snapshot_with(vec![(7, vec![0x1100])]), &modules);
assert_eq!(capture.modules.len(), 1);
assert_eq!(capture.modules[0].name, "used.dll");
}
#[test]
fn thread_identity_and_order_survive() {
let modules = vec![module(0x1000, 0x1000, Some("a.dll"))];
let capture = attribute(
&snapshot_with(vec![(100, vec![0x1100]), (200, vec![0x1200])]),
&modules,
);
assert_eq!(capture.threads[0].os_tid, 100);
assert_eq!(capture.threads[1].os_tid, 200);
}
#[test]
fn a_pathless_module_is_named_by_its_base() {
let modules = vec![module(0x4000, 0x1000, None)];
let capture = attribute(&snapshot_with(vec![(7, vec![0x4010])]), &modules);
assert_eq!(capture.modules[0].name, "0x4000");
assert_eq!(capture.modules[0].path, None);
}
#[cfg(windows)]
#[test]
fn a_real_capture_attributes_most_of_its_frames() {
use crate::snapshot::modules::enumerate_modules;
use crate::snapshot::{capture_and_resolve, SnapshotConfig};
let snapshot = capture_and_resolve(&SnapshotConfig::default()).expect("capture");
let modules = enumerate_modules().expect("modules");
let capture = attribute(&snapshot, &modules);
let total: usize = capture.threads.iter().map(|t| t.frames.len()).sum();
if total == 0 {
assert!(
std::env::var_os("GITHUB_ACTIONS").is_none(),
"captured no frames during a CI run; this test would assert nothing"
);
return;
}
for thread in &capture.threads {
for frame in &thread.frames {
let Some(index) = frame.module_index else {
continue;
};
let module = &capture.modules[index as usize];
let size = modules
.iter()
.find(|m| m.base == module.base)
.map(|m| m.size)
.expect("attributed module must come from the inventory");
assert!(
frame.relative_address < size,
"offset {:#x} exceeds {}'s size {size:#x}; the frame was attributed to the wrong module",
frame.relative_address,
module.name
);
}
}
let attributed = total - capture.unattributed_frames();
assert!(
attributed > 0,
"no frame of {total} matched any module; attribution is not working"
);
assert!(
!capture.modules.is_empty(),
"attributed frames but listed no modules"
);
}
}