1use addr2line::Loader;
2use anyhow::{Result, anyhow};
3use byteorder::{LittleEndian, ReadBytesExt};
4use cargo_metadata::MetadataCommand;
5use std::{
6 collections::BTreeMap,
7 env::var_os,
8 fs::{File, OpenOptions, metadata},
9 io::Write,
10 path::{Path, PathBuf},
11};
12
13mod insn;
14use insn::Insn;
15
16mod start_address;
17use start_address::start_address;
18
19mod util;
20use util::{StripCurrentDir, files_with_extension};
21
22mod vaddr;
23use vaddr::Vaddr;
24
25#[cfg(test)]
26mod tests;
27
28#[derive(Clone, Debug, Default, Eq, PartialEq)]
29struct Entry<'a> {
30 file: &'a str,
31 line: u32,
32}
33
34struct Dwarf {
35 path: PathBuf,
36 start_address: u64,
37 #[allow(dead_code, reason = "`vaddr` points into `loader`")]
38 loader: &'static Loader,
39 vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
40}
41
42enum Outcome {
43 Lcov(PathBuf),
44 ClosestMatch(PathBuf),
45}
46
47type Vaddrs = Vec<u64>;
48
49type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
50
51#[allow(dead_code)]
52#[derive(Debug)]
53struct ClosestMatch<'a, 'b> {
54 pcs_path: &'a Path,
55 debug_path: &'b Path,
56 mismatch: Mismatch,
57}
58
59#[allow(dead_code)]
60#[derive(Clone, Copy, Debug, Default)]
61struct Mismatch {
62 index: usize,
63 vaddr: Vaddr,
64 expected: Insn,
65 actual: Insn,
66}
67
68type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
69
70pub fn run(sbf_trace_dir: impl AsRef<Path>, debug: bool) -> Result<()> {
71 let mut lcov_paths = Vec::new();
72 let mut closest_match_paths = Vec::new();
73
74 let debug_paths = debug_paths()?;
75
76 let dwarfs = debug_paths
77 .into_iter()
78 .map(|path| build_dwarf(&path))
79 .collect::<Result<Vec<_>>>()?;
80
81 if dwarfs.is_empty() {
82 eprintln!("Found no debug files");
83 return Ok(());
84 }
85
86 if debug {
87 for dwarf in dwarfs {
88 dump_vaddr_entry_map(dwarf.vaddr_entry_map);
89 }
90 return Ok(());
91 }
92
93 let pcs_paths = files_with_extension(&sbf_trace_dir, "pcs")?;
94
95 for pcs_path in &pcs_paths {
96 match process_pcs_path(&dwarfs, pcs_path)? {
97 Outcome::Lcov(lcov_path) => {
98 lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
99 }
100 Outcome::ClosestMatch(closest_match_path) => {
101 closest_match_paths.push(closest_match_path.strip_current_dir().to_path_buf());
102 }
103 }
104 }
105
106 eprintln!(
107 "
108Processed {} of {} program counter files
109
110Lcov files written: {lcov_paths:#?}
111
112Closest match files written: {closest_match_paths:#?}
113
114If you are done generating lcov files, try running:
115
116 genhtml --output-directory coverage {}/*.lcov && open coverage/index.html
117",
118 lcov_paths.len(),
119 pcs_paths.len(),
120 sbf_trace_dir.as_ref().strip_current_dir().display()
121 );
122
123 Ok(())
124}
125
126fn debug_paths() -> Result<Vec<PathBuf>> {
127 let metadata = MetadataCommand::new().no_deps().exec()?;
128 let target_directory = metadata.target_directory;
129 files_with_extension(target_directory.join("deploy"), "debug")
130}
131
132fn build_dwarf(debug_path: &Path) -> Result<Dwarf> {
133 let start_address = start_address(debug_path)?;
134
135 let loader = Loader::new(debug_path).map_err(|error| {
136 anyhow!(
137 "failed to build loader for {}: {}",
138 debug_path.display(),
139 error.to_string()
140 )
141 })?;
142
143 let loader = Box::leak(Box::new(loader));
144
145 let vaddr_entry_map = build_vaddr_entry_map(loader, debug_path)?;
146
147 Ok(Dwarf {
148 path: debug_path.to_path_buf(),
149 start_address,
150 loader,
151 vaddr_entry_map,
152 })
153}
154
155fn process_pcs_path(dwarfs: &[Dwarf], pcs_path: &Path) -> Result<Outcome> {
156 eprintln!();
157 eprintln!(
158 "Program counters file: {}",
159 pcs_path.strip_current_dir().display()
160 );
161
162 let mut vaddrs = read_vaddrs(pcs_path)?;
163
164 eprintln!("Program counters read: {}", vaddrs.len());
165
166 let (dwarf, mismatch) = find_applicable_dwarf(dwarfs, pcs_path, &mut vaddrs)?;
167
168 if let Some(mismatch) = mismatch {
169 return write_closest_match(pcs_path, dwarf, mismatch).map(Outcome::ClosestMatch);
170 }
171
172 eprintln!(
173 "Applicable dwarf: {}",
174 dwarf.path.strip_current_dir().display()
175 );
176
177 assert!(
178 vaddrs
179 .first()
180 .is_some_and(|&vaddr| vaddr == dwarf.start_address)
181 );
182
183 vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
186
187 let vaddrs = vaddrs
190 .into_iter()
191 .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
192 .collect::<Vec<_>>();
193
194 eprintln!("Line hits: {}", vaddrs.len());
195
196 let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
197
198 write_lcov_file(pcs_path, file_line_count_map).map(Outcome::Lcov)
199}
200
201static CARGO_HOME: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| {
202 if let Some(cargo_home) = var_os("CARGO_HOME") {
203 PathBuf::from(cargo_home)
204 } else {
205 #[allow(deprecated)]
206 #[cfg_attr(
207 dylint_lib = "inconsistent_qualification",
208 allow(inconsistent_qualification)
209 )]
210 std::env::home_dir().unwrap().join(".cargo")
211 }
212});
213
214fn build_vaddr_entry_map<'a>(loader: &'a Loader, debug_path: &Path) -> Result<VaddrEntryMap<'a>> {
215 let mut vaddr_entry_map = VaddrEntryMap::new();
216 let metadata = metadata(debug_path)?;
217 for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
218 let location = loader.find_location(vaddr).map_err(|error| {
219 anyhow!(
220 "failed to find location for address 0x{vaddr:x}: {}",
221 error.to_string()
222 )
223 })?;
224 let Some(location) = location else {
225 continue;
226 };
227 let Some(file) = location.file else {
228 continue;
229 };
230 if !Path::new(file).try_exists()? {
232 continue;
233 }
234 if !include_cargo() && file.starts_with(CARGO_HOME.to_string_lossy().as_ref()) {
235 continue;
236 }
237 let Some(line) = location.line else {
238 continue;
239 };
240 let Some(_column) = location.column else {
242 continue;
243 };
244 let entry = vaddr_entry_map.entry(vaddr).or_default();
245 entry.file = file;
246 entry.line = line;
247 }
248 Ok(vaddr_entry_map)
249}
250
251fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
252 let mut prev = String::new();
253 for (vaddr, Entry { file, line }) in vaddr_entry_map {
254 let curr = format!("{file}:{line}");
255 if prev != curr {
256 eprintln!("0x{vaddr:x}: {curr}");
257 prev = curr;
258 }
259 }
260}
261
262fn read_vaddrs(pcs_path: &Path) -> Result<Vaddrs> {
263 let mut vaddrs = Vaddrs::new();
264 let mut pcs_file = File::open(pcs_path)?;
265 while let Ok(pc) = pcs_file.read_u64::<LittleEndian>() {
266 let vaddr = pc << 3;
267 vaddrs.push(vaddr);
268 }
269 Ok(vaddrs)
270}
271
272fn find_applicable_dwarf<'a>(
273 dwarfs: &'a [Dwarf],
274 pcs_path: &Path,
275 vaddrs: &mut [u64],
276) -> Result<(&'a Dwarf, Option<Mismatch>)> {
277 let dwarf_mismatches = collect_dwarf_mismatches(dwarfs, pcs_path, vaddrs)?;
278
279 if let Some((dwarf, _)) = dwarf_mismatches
280 .iter()
281 .find(|(_, mismatch)| mismatch.is_none())
282 {
283 let vaddr_first = *vaddrs.first().unwrap();
284
285 assert!(dwarf.start_address >= vaddr_first);
286
287 let shift = dwarf.start_address - vaddr_first;
288
289 vaddrs.iter_mut().for_each(|vaddr| *vaddr += shift);
291
292 return Ok((dwarf, None));
293 }
294
295 Ok(dwarf_mismatches
296 .into_iter()
297 .max_by_key(|(_, mismatch)| mismatch.as_ref().unwrap().index)
298 .unwrap())
299}
300
301fn collect_dwarf_mismatches<'a>(
302 dwarfs: &'a [Dwarf],
303 pcs_path: &Path,
304 vaddrs: &[u64],
305) -> Result<Vec<(&'a Dwarf, Option<Mismatch>)>> {
306 dwarfs
307 .iter()
308 .map(|dwarf| {
309 let mismatch = dwarf_mismatch(vaddrs, dwarf, pcs_path)?;
310 Ok((dwarf, mismatch))
311 })
312 .collect()
313}
314
315fn dwarf_mismatch(vaddrs: &[u64], dwarf: &Dwarf, pcs_path: &Path) -> Result<Option<Mismatch>> {
316 use std::io::{Seek, SeekFrom};
317
318 let Some(&vaddr_first) = vaddrs.first() else {
319 return Ok(Some(Mismatch::default()));
320 };
321
322 if dwarf.start_address < vaddr_first {
323 return Ok(Some(Mismatch::default()));
324 }
325
326 let shift = dwarf.start_address - vaddr_first;
330
331 let mut so_file = File::open(dwarf.path.with_extension("so"))?;
332 let mut insns_file = File::open(pcs_path.with_extension("insns"))?;
333
334 for (index, &vaddr) in vaddrs.iter().enumerate() {
335 let vaddr = vaddr + shift;
336
337 so_file.seek(SeekFrom::Start(vaddr))?;
338 let expected = so_file.read_u64::<LittleEndian>()?;
339
340 let actual = insns_file.read_u64::<LittleEndian>()?;
341
342 if expected & 0xff == 0x85 {
345 continue;
346 }
347
348 if expected != actual {
349 return Ok(Some(Mismatch {
350 index,
351 vaddr: Vaddr::from(vaddr),
352 expected: Insn::from(expected),
353 actual: Insn::from(actual),
354 }));
355 }
356 }
357
358 Ok(None)
359}
360
361fn write_closest_match(pcs_path: &Path, dwarf: &Dwarf, mismatch: Mismatch) -> Result<PathBuf> {
362 let closest_match_path = pcs_path.with_extension("closest_match");
363 let mut file = OpenOptions::new()
364 .create(true)
365 .truncate(true)
366 .write(true)
367 .open(&closest_match_path)?;
368 writeln!(
369 file,
370 "{:#?}",
371 ClosestMatch {
372 pcs_path,
373 debug_path: &dwarf.path,
374 mismatch
375 }
376 )?;
377 Ok(closest_match_path)
378}
379
380fn build_file_line_count_map<'a>(
381 vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
382 vaddrs: Vaddrs,
383) -> FileLineCountMap<'a> {
384 let mut file_line_count_map = FileLineCountMap::new();
385 for Entry { file, line } in vaddr_entry_map.values() {
386 let line_count_map = file_line_count_map.entry(file).or_default();
387 line_count_map.insert(*line, 0);
388 }
389
390 for vaddr in vaddrs {
391 let Some(entry) = vaddr_entry_map.get(&vaddr) else {
393 continue;
394 };
395 let line_count_map = file_line_count_map.get_mut(entry.file).unwrap();
396 let count = line_count_map.get_mut(&entry.line).unwrap();
397 *count += 1;
398 }
399
400 file_line_count_map
401}
402
403fn write_lcov_file(pcs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
404 let lcov_path = Path::new(pcs_path).with_extension("lcov");
405
406 let mut file = OpenOptions::new()
407 .create(true)
408 .truncate(true)
409 .write(true)
410 .open(&lcov_path)?;
411
412 for (source_file, line_count_map) in file_line_count_map {
413 writeln!(file, "SF:{source_file}")?;
415 for (line, count) in line_count_map {
416 writeln!(file, "DA:{line},{count}")?;
417 }
418 writeln!(file, "end_of_record")?;
419 }
420
421 Ok(lcov_path)
422}
423
424fn include_cargo() -> bool {
425 var_os("INCLUDE_CARGO").is_some()
426}