• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xd009642 / llvm-profparser / #214

28 Sep 2025 05:30PM UTC coverage: 69.656% (+0.3%) from 69.394%
#214

push

web-flow
Test all supported LLVM versions at once (#58)

Previously, testing all supported configurations required installing
many different versions of rustc manually. Now, it only requires running
`cargo test --all-features`.

This introduces a dependency on `rustup`. It also introduces a
dependency on `cargo-binutils` with 166 merged. Note that PR has not yet
been merged at time of writing.

By default, this only tests LLVM 20, which is the latest version
currently available on stable rust.

45 of 49 new or added lines in 1 file covered. (91.84%)

2 existing lines in 1 file now uncovered.

1235 of 1773 relevant lines covered (69.66%)

3.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.7
/src/bin/profparser.rs
1
use anyhow::Result;
2
use llvm_profparser::instrumentation_profile::summary::*;
3
use llvm_profparser::instrumentation_profile::types::*;
4
use llvm_profparser::*;
5
use std::cmp::Ordering;
6
use std::collections::BinaryHeap;
7
use std::path::PathBuf;
8
use structopt::StructOpt;
9
use tracing_subscriber::filter::filter_fn;
10
use tracing_subscriber::{Layer, Registry};
11

12
#[derive(Clone, Debug, Eq, PartialEq, StructOpt)]
13
pub enum Command {
14
    Show {
15
        #[structopt(flatten)]
16
        show: ShowCommand,
17
    },
18
    Merge {
19
        #[structopt(flatten)]
20
        merge: MergeCommand,
21
    },
22
    Overlap {
23
        #[structopt(flatten)]
24
        overlap: OverlapCommand,
25
    },
26
}
27

28
#[derive(Clone, Debug, Eq, PartialEq, StructOpt)]
29
pub struct ShowCommand {
30
    /// Input profraw file to show some information about
31
    #[structopt(name = "<filename...>", long = "input", short = "i")]
32
    input: PathBuf,
33
    /// Show counter values for shown functions
34
    #[structopt(long = "counts")]
35
    show_counts: bool,
36
    /// Details for every function
37
    #[structopt(long = "all-functions")]
38
    all_functions: bool,
39
    /// Show instr profile data in text dump format
40
    #[structopt(long = "text")]
41
    text: bool,
42
    /// Show detailed profile summary
43
    #[structopt(long = "show_detailed_summary")]
44
    show_detailed_summary: bool,
45
    /// Cutoff percentages (times 10000) for generating detailed summary
46
    #[structopt(long = "detailed_summary_cutoffs")]
47
    detailed_summary_cutoffs: Vec<usize>,
48
    /// Show profile summary of a list of hot functions
49
    #[structopt(long = "show_hot_fn_list")]
50
    show_hot_fn_list: bool,
51
    /// Show context sensitive counts
52
    #[structopt(long = "showcs")]
53
    showcs: bool,
54
    /// Details for matching functions
55
    #[structopt(long = "function")]
56
    function: Option<String>,
57
    /// Output file
58
    #[structopt(long = "output", short = "o")]
59
    output: Option<String>,
60
    /// Show the list of functions with the largest internal counts
61
    #[structopt(long = "topn")]
62
    topn: Option<usize>,
63
    /// Set the count value cutoff. Functions with the maximum count less than
64
    /// this value will not be printed out. (Default is 0)
65
    #[structopt(long = "value_cutoff", default_value = "0")]
66
    value_cutoff: u64,
67
    /// Set the count value cutoff. Functions with the maximum count below the
68
    /// cutoff value
69
    #[structopt(long = "only_list_below")]
70
    only_list_below: bool,
71
    /// Show profile symbol list if it exists in the profile.
72
    #[structopt(long = "show_profile_sym_list")]
73
    show_profile_sym_list: bool,
74
    /// Show the information of each section in the sample profile. The flag is
75
    /// only usable when the sample profile is in extbinary format
76
    #[structopt(long = "show_section_info_only")]
77
    show_section_info_only: bool,
78
    /// Turn on debug logging
79
    #[structopt(long)]
80
    debug: bool,
81
}
82

83
#[derive(Clone, Debug, Eq, PartialEq, StructOpt)]
84
pub struct MergeCommand {
85
    /// Input files to merge
86
    #[structopt(name = "<filename...>", long = "input", short = "i")]
87
    input: Vec<PathBuf>,
88
    /// Output file
89
    #[structopt(long = "output", short = "o")]
90
    output: PathBuf,
91
    /// List of weights and filenames in `<weight>,<filename>` format
92
    #[structopt(long = "weighted-input", parse(try_from_str=try_parse_weighted))]
93
    weighted_input: Vec<(u64, String)>,
94
    /// Number of merge threads to use (will autodetect by default)
95
    #[structopt(long = "num-threads", short = "j")]
96
    jobs: Option<usize>,
97
    /// Turn on debug logging
98
    #[structopt(long)]
99
    debug: bool,
100
}
101

102
#[derive(Clone, Debug, Eq, PartialEq, StructOpt)]
103
pub struct OverlapCommand {
104
    #[structopt(name = "<base profile file>")]
105
    base_file: PathBuf,
106
    #[structopt(name = "<test profile file>")]
107
    test_file: PathBuf,
108
    #[structopt(long = "output", short = "o")]
109
    output: Option<PathBuf>,
110
    /// For context sensitive counts
111
    #[structopt(long = "cs")]
112
    context_sensitive_counts: bool,
113
    /// Function level overlap information for every function in test profile with max count value
114
    /// greater than the parameter value
115
    #[structopt(long = "value-cutoff")]
116
    value_cutoff: Option<usize>,
117
    /// Function level overlap information for matching functions
118
    #[structopt(long = "function")]
119
    function: Option<String>,
120
    /// Generate a sparse profile
121
    #[structopt(long = "sparse")]
122
    sparse: bool,
123
    /// Turn on debug logging
124
    #[structopt(long)]
125
    debug: bool,
126
}
127

128
#[derive(Clone, Debug, Eq, PartialEq, StructOpt)]
129
pub struct Opts {
130
    #[structopt(subcommand)]
131
    cmd: Command,
132
}
133

134
impl Opts {
135
    fn debug(&self) -> bool {
14✔
136
        match &self.cmd {
14✔
137
            &Command::Show { ref show } => show.debug,
14✔
138
            &Command::Merge { ref merge } => merge.debug,
×
139
            &Command::Overlap { ref overlap } => overlap.debug,
×
140
        }
141
    }
142
}
143

144
fn try_parse_weighted(input: &str) -> Result<(u64, String), String> {
1✔
145
    if !input.contains(',') {
2✔
146
        Ok((1, input.to_string()))
1✔
147
    } else {
148
        let parts = input.split(',').collect::<Vec<_>>();
1✔
149
        if parts.len() != 2 {
3✔
150
            Err("Unexpected weighting format, expected $weight,$name or just $name".to_string())
2✔
151
        } else {
152
            let weight = parts[0]
4✔
153
                .parse()
154
                .map_err(|e| format!("Invalid weight: {}", e))?;
4✔
155
            if weight < 1 {
1✔
156
                Err("Weight must be positive integer".to_string())
×
157
            } else {
158
                Ok((weight, parts[1].to_string()))
2✔
159
            }
160
        }
161
    }
162
}
163

164
fn check_function(name: Option<&String>, pattern: Option<&String>) -> bool {
×
165
    match pattern {
×
166
        Some(pat) => name.map(|x| x.contains(pat)).unwrap_or(false),
×
167
        None => false,
×
168
    }
169
}
170

171
#[derive(Clone, Debug, Eq)]
172
struct HotFn {
173
    name: String,
174
    count: u64,
175
}
176

177
impl PartialOrd for HotFn {
178
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
×
179
        Some(self.cmp(other))
×
180
    }
181
}
182

183
impl Ord for HotFn {
184
    fn cmp(&self, other: &Self) -> Ordering {
×
185
        // Do the reverse here
186
        other.count.cmp(&self.count)
×
187
    }
188
}
189

190
impl PartialEq for HotFn {
191
    fn eq(&self, other: &Self) -> bool {
×
192
        self.count == other.count
×
193
    }
194
}
195

196
impl ShowCommand {
197
    pub fn run(&self) -> Result<()> {
14✔
198
        let profile = parse(&self.input)?;
14✔
199
        let mut summary = ProfileSummary::new();
14✔
200

201
        let is_ir_instr = profile.is_ir_level_profile();
28✔
202
        let mut hotties =
14✔
203
            BinaryHeap::<HotFn>::with_capacity(self.topn.unwrap_or_default() as usize);
204
        let mut shown_funcs = 0;
14✔
205
        let mut below_cutoff_funcs = 0;
14✔
206
        let topn = self.topn.unwrap_or_default();
28✔
207
        for func in profile.records() {
14✔
208
            if func.name.is_none() || func.hash.is_none() {
28✔
209
                continue;
210
            }
211
            if is_ir_instr && func.has_cs_flag() != self.showcs {
14✔
212
                continue;
213
            }
214
            let show =
14✔
215
                self.all_functions || check_function(func.name.as_ref(), self.function.as_ref());
216

217
            if show && self.text {
28✔
218
                // TODO text format dump
219
                continue;
220
            }
221
            summary.add_record(&func.record);
14✔
222

223
            let (func_max, func_sum) = func.counts().iter().fold((0, 0u64), |acc, x| {
28✔
224
                (*x.max(&acc.0), acc.1.saturating_add(*x))
14✔
225
            });
226
            if func_max < self.value_cutoff {
14✔
227
                below_cutoff_funcs += 1;
×
228
                if self.only_list_below {
×
229
                    println!(
×
230
                        "  {}: (Max = {} Sum = {})",
231
                        func.name.as_ref().unwrap(),
×
232
                        func_max,
233
                        func_sum
234
                    );
235
                    continue;
236
                }
237
            } else if self.only_list_below {
14✔
238
                continue;
239
            }
240
            if topn > 0 {
14✔
241
                if hotties.len() == topn {
×
242
                    let top = hotties.peek().unwrap();
×
243
                    if top.count < func_max {
×
244
                        hotties.pop();
×
245
                        hotties.push(HotFn {
×
246
                            name: func.name.as_ref().unwrap().to_string(),
×
247
                            count: func_max,
248
                        });
249
                    }
250
                } else {
251
                    hotties.push(HotFn {
×
252
                        name: func.name.as_ref().unwrap().to_string(),
×
253
                        count: func_max,
254
                    });
255
                }
256
            }
257
            if show {
14✔
258
                if shown_funcs == 0 {
14✔
259
                    println!("Counters:");
28✔
260
                }
261
                shown_funcs += 1;
28✔
262
                println!("  {}:", func.name.as_ref().unwrap());
28✔
263
                println!("    Hash: {:#018x}", func.hash.unwrap());
14✔
264
                println!("    Counters: {}", func.counts().len());
14✔
265
                if !is_ir_instr {
14✔
266
                    let counts = if func.counts().is_empty() {
42✔
267
                        0
×
268
                    } else {
269
                        func.counts()[0]
28✔
270
                    };
271
                    println!("    Function count: {}", counts);
14✔
272
                }
273
                if self.show_counts {
14✔
274
                    let start = if is_ir_instr { 0 } else { 1 };
14✔
275
                    let counts = func
14✔
276
                        .counts()
277
                        .iter()
278
                        .skip(start)
14✔
279
                        .map(|x| x.to_string())
40✔
280
                        .collect::<Vec<String>>()
281
                        .join(", ");
282
                    println!("    Block counts: [{}]", counts);
14✔
283
                }
284
            }
285
        }
286
        if profile.get_level() == InstrumentationLevel::Ir {
14✔
UNCOV
287
            println!(
×
288
                "Instrumentation level: {}  entry_first = {}",
289
                profile.get_level(),
×
290
                // NOTE: in llvm 11 this is always false
UNCOV
291
                profile.is_entry_first() as usize
×
292
            );
293
        } else {
294
            println!("Instrumentation level: {}", profile.get_level());
28✔
295
        }
296
        if self.all_functions || self.function.is_some() {
14✔
297
            println!("Functions shown: {}", shown_funcs);
28✔
298
        }
299
        println!("Total functions: {}", summary.num_functions());
28✔
300
        if self.value_cutoff > 0 {
14✔
301
            println!(
×
302
                "Number of functions with maximum count (< {} ): {}",
303
                self.value_cutoff, below_cutoff_funcs
304
            );
305
            println!(
×
306
                "Number of functions with maximum count (>= {}): {}",
307
                self.value_cutoff,
308
                summary.num_functions() - below_cutoff_funcs
×
309
            );
310
        }
311
        println!("Maximum function count: {}", summary.max_function_count());
28✔
312
        println!(
14✔
313
            "Maximum internal block count: {}",
314
            summary.max_internal_block_count()
14✔
315
        );
316
        if let Some(topn) = self.topn {
14✔
317
            println!(
×
318
                "Top {} functions with the largest internal block counts: ",
319
                topn
320
            );
321
            let hotties = hotties.into_sorted_vec();
×
322
            for f in hotties.iter() {
×
323
                println!("  {}, max count = {}", f.name, f.count);
×
324
            }
325
        }
326

327
        if self.show_detailed_summary {
14✔
328
            println!("Total number of blocks: ?");
×
329
            println!("Total count: ?");
×
330
        }
331
        Ok(())
14✔
332
    }
333
}
334

335
impl MergeCommand {
336
    fn run(&self) -> Result<()> {
×
337
        assert!(
×
338
            !self.input.is_empty(),
×
339
            "No input files selected. See merge --help"
340
        );
341
        let profile = merge_profiles(&self.input)?;
×
342
        // Now to write it out?
343
        println!("{:#?}", profile);
×
344
        Ok(())
×
345
    }
346
}
347

348
fn enable_debug_logging() -> anyhow::Result<()> {
×
349
    let fmt = tracing_subscriber::fmt::Layer::default();
×
350
    let subscriber = fmt
351
        .with_filter(filter_fn(|metadata| {
×
352
            metadata.target().contains("llvm_profparser")
×
353
        }))
354
        .with_subscriber(Registry::default());
×
355
    tracing::subscriber::set_global_default(subscriber)?;
×
356
    Ok(())
×
357
}
358

359
fn main() -> Result<()> {
14✔
360
    let opts = Opts::from_args();
14✔
361
    if opts.debug() {
28✔
362
        let _ = enable_debug_logging();
×
363
    }
364
    match opts.cmd {
14✔
365
        Command::Show { show } => show.run(),
14✔
366
        Command::Merge { merge } => merge.run(),
×
367
        _ => {
368
            panic!("Unsupported command");
×
369
        }
370
    }
371
}
372

373
#[cfg(test)]
374
mod tests {
375
    use super::*;
376

377
    #[test]
378
    fn weight_arg_parsing() {
3✔
379
        // Examples taken from LLVM docs
380
        let foo_10 = "10,foo.profdata";
1✔
381
        let bar_1 = "1,bar.profdata";
1✔
382

383
        assert_eq!(
1✔
384
            Ok((10, "foo.profdata".to_string())),
1✔
385
            try_parse_weighted(foo_10)
1✔
386
        );
387
        assert_eq!(
1✔
388
            Ok((1, "bar.profdata".to_string())),
1✔
389
            try_parse_weighted(bar_1)
1✔
390
        );
391
        assert_eq!(
1✔
392
            Ok((1, "foo.profdata".to_string())),
1✔
393
            try_parse_weighted("foo.profdata")
1✔
394
        );
395
        assert!(try_parse_weighted("foo.profdata,1").is_err());
1✔
396
        assert!(try_parse_weighted("1,1,foo.profdata").is_err());
1✔
397
    }
398
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc