sbpf_coverage/
lib.rs

1use addr2line::Loader;
2use anyhow::{Result, anyhow, bail};
3use byteorder::{LittleEndian, ReadBytesExt};
4use std::{
5    collections::{BTreeMap, HashSet},
6    fs::{File, OpenOptions, metadata},
7    io::Write,
8    path::{Path, PathBuf},
9};
10
11mod branch;
12
13mod start_address;
14use start_address::start_address;
15
16pub mod util;
17use util::StripCurrentDir;
18
19use crate::util::{compute_hash, find_files_with_extension};
20
21mod vaddr;
22
23#[derive(Clone, Debug, Default, Eq, PartialEq)]
24struct Entry<'a> {
25    file: &'a str,
26    line: u32,
27}
28
29struct Dwarf {
30    path: PathBuf,
31    #[allow(dead_code)]
32    so_path: PathBuf,
33    so_hash: String,
34    start_address: u64,
35    #[allow(dead_code, reason = "`vaddr` points into `loader`")]
36    loader: &'static Loader,
37    vaddr_entry_map: BTreeMap<u64, Entry<'static>>,
38}
39
40enum Outcome {
41    Lcov(PathBuf),
42}
43
44type Vaddrs = Vec<u64>;
45type Insns = Vec<u64>;
46type Regs = Vec<[u64; 12]>;
47
48type VaddrEntryMap<'a> = BTreeMap<u64, Entry<'a>>;
49
50type FileLineCountMap<'a> = BTreeMap<&'a str, BTreeMap<u32, usize>>;
51
52pub fn run(
53    sbf_trace_dir: PathBuf,
54    src_paths: HashSet<PathBuf>,
55    sbf_paths: Vec<PathBuf>,
56    debug: bool,
57) -> Result<()> {
58    let mut lcov_paths = Vec::new();
59
60    let debug_paths = debug_paths(sbf_paths)?;
61
62    let dwarfs = debug_paths
63        .into_iter()
64        .map(|path| build_dwarf(&path, &src_paths))
65        .collect::<Result<Vec<_>>>()
66        .expect("Can't build dwarf");
67
68    if dwarfs.is_empty() {
69        bail!("Found no debug files");
70    }
71
72    if debug {
73        for dwarf in dwarfs {
74            dump_vaddr_entry_map(dwarf.vaddr_entry_map);
75        }
76        eprintln!("Exiting debug mode.");
77        return Ok(());
78    }
79
80    let regs_paths = find_files_with_extension(std::slice::from_ref(&sbf_trace_dir), "regs");
81    if regs_paths.is_empty() {
82        bail!(
83            "Found no regs files in: {}
84Are you sure you run your tests with register tracing enabled",
85            sbf_trace_dir.strip_current_dir().display(),
86        );
87    }
88
89    for regs_path in &regs_paths {
90        match process_regs_path(&dwarfs, regs_path, &src_paths) {
91            Ok(Outcome::Lcov(lcov_path)) => {
92                lcov_paths.push(lcov_path.strip_current_dir().to_path_buf());
93            }
94            _ => {
95                eprintln!("Skipping Regs file: {}", regs_path.to_string_lossy());
96            }
97        }
98    }
99
100    eprintln!(
101        "
102Processed {} of {} regs files
103
104Lcov files written: {lcov_paths:#?}
105
106If you are done generating lcov files, try running:
107
108    genhtml --output-directory coverage {}/*.lcov --rc branch_coverage=1 && open coverage/index.html
109",
110        lcov_paths.len(),
111        regs_paths.len(),
112        sbf_trace_dir.as_path().strip_current_dir().display()
113    );
114
115    Ok(())
116}
117
118fn debug_paths(sbf_paths: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
119    let debug_files = find_files_with_extension(&sbf_paths, "debug");
120    Ok(debug_files)
121}
122
123fn build_dwarf(debug_path: &Path, src_paths: &HashSet<PathBuf>) -> Result<Dwarf> {
124    let start_address = start_address(debug_path)?;
125
126    let loader = Loader::new(debug_path).map_err(|error| {
127        anyhow!(
128            "failed to build loader for {}: {}",
129            debug_path.display(),
130            error.to_string()
131        )
132    })?;
133
134    let loader = Box::leak(Box::new(loader));
135
136    let vaddr_entry_map = build_vaddr_entry_map(loader, debug_path, src_paths)?;
137
138    // Suppose debug_path is program.debug, swap with .so and try
139    let mut so_path = debug_path.with_extension("so");
140    let so_content = match std::fs::read(&so_path) {
141        Err(e) => {
142            if e.kind() == std::io::ErrorKind::NotFound {
143                // We might have program.so.debug - simply cut debug and try
144                so_path = debug_path.with_extension("");
145                std::fs::read(&so_path)?
146            } else {
147                return Err(e.into());
148            }
149        }
150        Ok(c) => c,
151    };
152    let so_hash = compute_hash(&so_content);
153
154    Ok(Dwarf {
155        path: debug_path.to_path_buf(),
156        so_path,
157        so_hash,
158        start_address,
159        loader,
160        vaddr_entry_map,
161    })
162}
163
164fn process_regs_path(
165    dwarfs: &[Dwarf],
166    regs_path: &Path,
167    src_paths: &HashSet<PathBuf>,
168) -> Result<Outcome> {
169    eprintln!();
170    eprintln!("Regs file: {}", regs_path.strip_current_dir().display());
171
172    let (mut vaddrs, regs) = read_vaddrs(regs_path)?;
173    eprintln!("Regs read: {}", vaddrs.len());
174    let insns = read_insns(&regs_path.with_extension("insns"))?;
175
176    let dwarf = find_applicable_dwarf(dwarfs, regs_path, &mut vaddrs)?;
177
178    eprintln!(
179        "Applicable dwarf: {}",
180        dwarf.path.strip_current_dir().display()
181    );
182
183    assert!(
184        vaddrs
185            .first()
186            .is_some_and(|&vaddr| vaddr == dwarf.start_address)
187    );
188
189    // smoelius: If a sequence of Regs refer to the same file and line, treat them as
190    // one hit to that file and line.
191    // vaddrs.dedup_by_key::<_, Option<&Entry>>(|vaddr| dwarf.vaddr_entry_map.get(vaddr));
192
193    if let Ok(branches) = branch::get_branches(&vaddrs, &insns, &regs, dwarf) {
194        let _ = branch::write_branch_coverage(&branches, regs_path, src_paths);
195    }
196
197    // smoelius: A `vaddr` could not have an entry because its file does not exist. Keep only those
198    // `vaddr`s that have entries.
199    let vaddrs = vaddrs
200        .into_iter()
201        .filter(|vaddr| dwarf.vaddr_entry_map.contains_key(vaddr))
202        .collect::<Vec<_>>();
203
204    eprintln!("Line hits: {}", vaddrs.len());
205
206    let file_line_count_map = build_file_line_count_map(&dwarf.vaddr_entry_map, vaddrs);
207
208    write_lcov_file(regs_path, file_line_count_map).map(Outcome::Lcov)
209}
210
211fn build_vaddr_entry_map<'a>(
212    loader: &'a Loader,
213    debug_path: &Path,
214    src_paths: &HashSet<PathBuf>,
215) -> Result<VaddrEntryMap<'a>> {
216    let mut vaddr_entry_map = VaddrEntryMap::new();
217    let metadata = metadata(debug_path)?;
218    for vaddr in (0..metadata.len()).step_by(size_of::<u64>()) {
219        let location = loader.find_location(vaddr).map_err(|error| {
220            anyhow!(
221                "failed to find location for address 0x{vaddr:x}: {}",
222                error.to_string()
223            )
224        })?;
225        let Some(location) = location else {
226            continue;
227        };
228        let Some(file) = location.file else {
229            continue;
230        };
231        // smoelius: Ignore files that do not exist.
232        if !Path::new(file).try_exists()? {
233            continue;
234        }
235        // procdump: ignore files other than what user has provided.
236        if !src_paths
237            .iter()
238            .any(|src_path| file.starts_with(&src_path.to_string_lossy().to_string()))
239        {
240            continue;
241        }
242        let Some(line) = location.line else {
243            continue;
244        };
245        // smoelius: Even though we ignore columns, fetch them should we ever want to act on them.
246        let Some(_column) = location.column else {
247            continue;
248        };
249        let entry = vaddr_entry_map.entry(vaddr).or_default();
250        entry.file = file;
251        entry.line = line;
252    }
253    Ok(vaddr_entry_map)
254}
255
256fn dump_vaddr_entry_map(vaddr_entry_map: BTreeMap<u64, Entry<'_>>) {
257    let mut prev = String::new();
258    for (vaddr, Entry { file, line }) in vaddr_entry_map {
259        let curr = format!("{file}:{line}");
260        if prev != curr {
261            eprintln!("0x{vaddr:x}: {curr}");
262            prev = curr;
263        }
264    }
265}
266
267fn read_insns(insns_path: &Path) -> Result<Insns> {
268    let mut insns = Vec::new();
269    let mut insns_file = File::open(insns_path)?;
270    while let Ok(insn) = insns_file.read_u64::<LittleEndian>() {
271        insns.push(insn);
272    }
273    Ok(insns)
274}
275
276fn read_vaddrs(regs_path: &Path) -> Result<(Vaddrs, Regs)> {
277    let mut regs = Regs::new();
278    let mut vaddrs = Vaddrs::new();
279    let mut regs_file = File::open(regs_path)?;
280
281    let mut data_trace = [0u64; 12];
282    'outer: loop {
283        for item in &mut data_trace {
284            match regs_file.read_u64::<LittleEndian>() {
285                Err(_) => break 'outer,
286                Ok(reg) => *item = reg,
287            }
288        }
289
290        // NB: the pc is instruction indexed, not byte indexed, keeps it aligned to 8 bytes - hence << 3 -> *8
291        let vaddr = data_trace[11] << 3;
292
293        vaddrs.push(vaddr);
294        let regs_values: [u64; 12] = data_trace[0..12].try_into().unwrap();
295        regs.push(regs_values);
296    }
297
298    Ok((vaddrs, regs))
299}
300
301fn find_applicable_dwarf<'a>(
302    dwarfs: &'a [Dwarf],
303    regs_path: &Path,
304    vaddrs: &mut [u64],
305) -> Result<&'a Dwarf> {
306    // Get the SHA-256 identifier for the Executable that has generated this tracing data.
307    let exec_sha256 = std::fs::read_to_string(regs_path.with_extension("exec.sha256"))?;
308    let dwarf = dwarfs
309        .iter()
310        .find(|dwarf| dwarf.so_hash == exec_sha256)
311        .ok_or(anyhow!(
312            "Cannot find the shared object that corresponds to: {}",
313            exec_sha256
314        ))?;
315
316    let vaddr_first = *vaddrs.first().unwrap();
317    assert!(dwarf.start_address >= vaddr_first);
318    let shift = dwarf.start_address - vaddr_first;
319
320    // smoelius: Make the shift "permanent".
321    for vaddr in vaddrs.iter_mut() {
322        *vaddr += shift;
323    }
324
325    Ok(dwarf)
326}
327
328fn build_file_line_count_map<'a>(
329    vaddr_entry_map: &BTreeMap<u64, Entry<'a>>,
330    vaddrs: Vaddrs,
331) -> FileLineCountMap<'a> {
332    let mut file_line_count_map = FileLineCountMap::new();
333    for Entry { file, line } in vaddr_entry_map.values() {
334        let line_count_map = file_line_count_map.entry(file).or_default();
335        line_count_map.insert(*line, 0);
336    }
337
338    for vaddr in vaddrs {
339        // smoelius: A `vaddr` could not have an entry because its file does not exist.
340        let Some(entry) = vaddr_entry_map.get(&vaddr) else {
341            continue;
342        };
343        let line_count_map = file_line_count_map.get_mut(entry.file).unwrap();
344        let count = line_count_map.get_mut(&entry.line).unwrap();
345        *count += 1;
346    }
347
348    file_line_count_map
349}
350
351fn write_lcov_file(regs_path: &Path, file_line_count_map: FileLineCountMap<'_>) -> Result<PathBuf> {
352    let lcov_path = Path::new(regs_path).with_extension("lcov");
353
354    let mut file = OpenOptions::new()
355        .create(true)
356        .truncate(true)
357        .write(true)
358        .open(&lcov_path)?;
359
360    for (source_file, line_count_map) in file_line_count_map {
361        // smoelius: Stripping `current_dir` from `source_file` has not effect on what's displayed.
362        writeln!(file, "SF:{source_file}")?;
363        for (line, count) in line_count_map {
364            writeln!(file, "DA:{line},{count}")?;
365        }
366        writeln!(file, "end_of_record")?;
367    }
368
369    Ok(lcov_path)
370}