1use {
10 crate::{
11 debugger::source::SourceResolver,
12 flamegraph::trace::{find_unstripped_binary, REGS_ENTRY_SIZE},
13 },
14 anyhow::{anyhow, Context, Result},
15 std::{
16 collections::{BTreeMap, BTreeSet},
17 fs,
18 io::Write,
19 path::{Path, PathBuf},
20 },
21};
22
23pub fn generate_lcov(
39 trace_dir: &Path,
40 programs: &BTreeMap<String, PathBuf>,
41 manifest_dir: Option<&Path>,
42 output: &Path,
43) -> Result<()> {
44 let pc_sets = collect_pcs_from_traces(trace_dir)?;
45 if pc_sets.is_empty() {
46 return Err(anyhow!("no trace data found in {}", trace_dir.display()));
47 }
48
49 eprintln!("found {} program(s) in traces", pc_sets.len());
50
51 let mut line_hits: BTreeMap<PathBuf, BTreeMap<u32, u64>> = BTreeMap::new();
52
53 for (program_id, pcs) in &pc_sets {
54 let deployed = match programs.get(program_id) {
55 Some(p) => p,
56 None => {
57 eprintln!("warning: no .so found for program {program_id}, skipping");
58 continue;
59 }
60 };
61
62 let dwarf_path = find_unstripped_binary(deployed, manifest_dir)
67 .unwrap_or_else(|| deployed.to_path_buf());
68
69 let resolver = SourceResolver::from_elf_path(&dwarf_path);
70 if resolver.is_empty() {
71 eprintln!(
72 "warning: no DWARF in {} — rebuild with CARGO_PROFILE_RELEASE_DEBUG=2",
73 dwarf_path.display()
74 );
75 continue;
76 }
77
78 for loc in resolver.executable_lines() {
79 if let Some(path) = resolve_source_path(&loc.file, manifest_dir) {
80 line_hits
81 .entry(path)
82 .or_default()
83 .entry(loc.line)
84 .or_insert(0);
85 }
86 }
87
88 let mut resolved_count = 0u64;
96 for &pc in pcs {
97 let frames = resolver.resolve_frames(pc);
98 if !frames.is_empty() {
99 resolved_count += 1;
100 }
101 for loc in frames {
102 if let Some(path) = resolve_source_path(&loc.file, manifest_dir) {
103 *line_hits
104 .entry(path)
105 .or_default()
106 .entry(loc.line)
107 .or_insert(0) += 1;
108 }
109 }
110 }
111 eprintln!(
112 " {} — {} unique PCs, {} resolved to source",
113 dwarf_path.file_name().unwrap_or_default().to_string_lossy(),
114 pcs.len(),
115 resolved_count,
116 );
117 }
118
119 if line_hits.is_empty() {
120 return Err(anyhow!(
121 "no source lines resolved from trace data in {}",
122 trace_dir.display()
123 ));
124 }
125
126 let summary = write_lcov_records(&line_hits, output)?;
127 eprintln!(
128 " {} source files, {}/{} lines hit",
129 summary.files, summary.hit_lines, summary.total_lines
130 );
131 Ok(())
132}
133
134#[derive(Debug, PartialEq, Eq)]
135struct LcovSummary {
136 files: usize,
137 total_lines: usize,
138 hit_lines: usize,
139}
140
141fn write_lcov_records(
142 line_hits: &BTreeMap<PathBuf, BTreeMap<u32, u64>>,
143 output: &Path,
144) -> Result<LcovSummary> {
145 let mut out =
146 fs::File::create(output).with_context(|| format!("create {}", output.display()))?;
147
148 let mut summary = LcovSummary {
149 files: line_hits.len(),
150 total_lines: 0,
151 hit_lines: 0,
152 };
153
154 for (file, lines) in line_hits {
155 writeln!(out, "SF:{}", file.display())?;
156 for (&line, &hits) in lines {
157 writeln!(out, "DA:{line},{hits}")?;
158 }
159 let lf = lines.len();
160 let lh = lines.values().filter(|&&h| h > 0).count();
161 writeln!(out, "LF:{lf}")?;
162 writeln!(out, "LH:{lh}")?;
163 writeln!(out, "end_of_record")?;
164
165 summary.total_lines += lf;
166 summary.hit_lines += lh;
167 }
168
169 Ok(summary)
170}
171
172fn resolve_source_path(file: &Path, workspace_root: Option<&Path>) -> Option<PathBuf> {
183 if file.is_absolute() {
184 return file.exists().then(|| file.to_path_buf());
185 }
186 let root = workspace_root?;
187 let candidate = root.join(file);
188 candidate.exists().then_some(candidate)
189}
190
191fn collect_pcs_from_traces(trace_dir: &Path) -> Result<BTreeMap<String, BTreeSet<u64>>> {
199 let mut result: BTreeMap<String, BTreeSet<u64>> = BTreeMap::new();
200
201 if !trace_dir.exists() {
202 return Ok(result);
203 }
204
205 visit_dir(trace_dir, &mut result)?;
206 Ok(result)
207}
208
209fn visit_dir(dir: &Path, result: &mut BTreeMap<String, BTreeSet<u64>>) -> Result<()> {
210 for entry in fs::read_dir(dir)? {
211 let entry = entry?;
212 let path = entry.path();
213
214 if path.is_dir() {
215 visit_dir(&path, result)?;
216 continue;
217 }
218
219 if path.extension().and_then(|e| e.to_str()) != Some("regs") {
220 continue;
221 }
222
223 let pid_path = path.with_extension("program_id");
224 let program_id = match fs::read_to_string(&pid_path) {
225 Ok(s) => s.trim().to_string(),
226 Err(_) => continue,
227 };
228
229 let data = fs::read(&path)?;
230 if data.len() % REGS_ENTRY_SIZE != 0 {
231 eprintln!(
232 "warning: {} has unexpected size (not multiple of {})",
233 path.display(),
234 REGS_ENTRY_SIZE
235 );
236 continue;
237 }
238
239 let pcs = result.entry(program_id).or_default();
240 let num_steps = data.len() / REGS_ENTRY_SIZE;
241 for i in 0..num_steps {
242 let offset = i * REGS_ENTRY_SIZE + 11 * 8;
243 let pc = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
244 pcs.insert(pc);
245 }
246 }
247 Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252 use {super::*, tempfile::tempdir};
253
254 #[test]
255 fn generate_lcov_errors_when_no_trace_data_found() {
256 let dir = tempdir().unwrap();
257 let output = dir.path().join("lcov.info");
258 let err =
259 generate_lcov(dir.path(), &BTreeMap::new(), None, &output).expect_err("expected error");
260
261 assert!(err.to_string().contains("no trace data found"));
262 assert!(!output.exists());
263 }
264
265 #[test]
266 fn lcov_writer_preserves_zero_hit_lines() {
267 let dir = tempdir().unwrap();
268 let source = dir.path().join("program.rs");
269 let output = dir.path().join("lcov.info");
270 let line_hits =
271 BTreeMap::from([(source.clone(), BTreeMap::from([(10, 3), (11, 0), (12, 1)]))]);
272
273 let summary = write_lcov_records(&line_hits, &output).unwrap();
274 let lcov = fs::read_to_string(output).unwrap();
275
276 assert_eq!(
277 summary,
278 LcovSummary {
279 files: 1,
280 total_lines: 3,
281 hit_lines: 2,
282 }
283 );
284 assert!(lcov.contains(&format!("SF:{}", source.display())));
285 assert!(lcov.contains("DA:10,3\n"));
286 assert!(lcov.contains("DA:11,0\n"));
287 assert!(lcov.contains("DA:12,1\n"));
288 assert!(lcov.contains("LF:3\n"));
289 assert!(lcov.contains("LH:2\n"));
290 }
291}