prof/
lib.rs

1pub mod types;
2pub mod utils;
3
4use clap::{Args, Parser, Subcommand};
5use color_eyre::eyre::{bail, Context};
6use color_eyre::Result;
7use types::{CacheMiss, HeapSummary, LeakSummary};
8use utils::{check_commands, parse_output_line, parse_output_line_f64, Capture};
9
10use crate::types::{HeapSummaryHuman, LeakSummaryHuman};
11use crate::utils::human_bytes;
12
13#[derive(Parser, Debug)]
14#[clap(version)]
15#[clap(name = "prof")]
16#[clap(bin_name = "prof")]
17pub struct Prof {
18    #[clap(subcommand)]
19    pub command: Commands,
20
21    /// The binary target to profile
22    #[clap(short, long, global = true)]
23    pub bin: Option<String>,
24
25    /// JSON output with total bytes. Deafults to YAML with human readable bytes
26    #[clap(short, long, global = true)]
27    pub json: bool,
28
29    /// Pass any additional args to the target binary with --
30    #[clap(last = true, global = true)]
31    pub target_args: Vec<String>,
32}
33
34#[derive(Subcommand, Debug)]
35pub enum Commands {
36    /// Output the total bytes allocated and freed by the program
37    Heap(Heap),
38    /// Output leaked bytes from the program
39    Leak(Leak),
40    /// Check cache miss rates
41    Cache(Cache),
42}
43
44#[derive(Args, Clone, Debug)]
45pub struct Leak {}
46
47#[derive(Args, Clone, Debug)]
48pub struct Cache {}
49
50#[derive(Args, Clone, Debug)]
51pub struct Heap {
52    /// Subtract bytes from total allocated
53    #[clap(short, long, default_value_t = 0)]
54    pub subtract_bytes: i64,
55}
56
57pub fn valgrind(
58    bin: Option<String>,
59    target_args: Vec<String>,
60    valgrind_args: Vec<&str>,
61) -> Result<String> {
62    if cfg!(target_os = "windows") {
63        bail!("Valgrind is not supported on Windows");
64    }
65    check_commands(&["valgrind"])?;
66    let mut command = std::process::Command::new("valgrind");
67    command.args(valgrind_args);
68    command.args([format!("{}", bin.expect("provide a --bin <BIN>"))]);
69
70    command.args(target_args);
71
72    Ok(String::from_utf8(command.output()?.stderr)?)
73}
74
75pub fn cache(
76    args: &Prof,
77    _cache_args: &Cache,
78    cargo_fn: Option<fn(bin: &Option<String>) -> Result<Option<String>>>,
79) -> Result<()> {
80    let mut bin = args.bin.clone();
81    if let Some(cargo_fn) = cargo_fn {
82        bin = cargo_fn(&args.bin)?;
83    };
84    let output = valgrind(bin, args.target_args.clone(), vec!["--tool=cachegrind"])?;
85
86    let i1_cap =
87        Capture::new(r"I1\s*miss rate:\s*([\d|\.]*)", &output).context("Cachegrind output")?;
88    let mut l1i_miss = i1_cap.iter_next();
89
90    let l2i_cap =
91        Capture::new(r"L[L|2]i\s*miss rate:\s*([\d|\.]*)", &output).context("Cachegrind output")?;
92    let mut lli_miss = l2i_cap.iter_next();
93
94    let d1_cap =
95        Capture::new(r"D1\s*miss rate:\s*([\d|\.]*)", &output).context("Cachegrind output")?;
96    let mut l1d_miss = d1_cap.iter_next();
97
98    let l2d_cap =
99        Capture::new(r"L[L|2]d\s*miss rate:\s*([\d|\.]*)", &output).context("Cachegrind output")?;
100    let mut lld_miss = l2d_cap.iter_next();
101
102    let l2_cap =
103        Capture::new(r"L[L|2]\s*miss rate:\s*([\d|\.]*)", &output).context("Cachegrind output")?;
104    let mut ll_total_miss = l2_cap.iter_next();
105
106    let cache_miss = CacheMiss {
107        l1i: parse_output_line_f64("l1i miss rate", l1i_miss.next()),
108        l1d: parse_output_line_f64("l1d miss rate", l1d_miss.next()),
109        lli: parse_output_line_f64("lli miss rate", lli_miss.next()),
110        lld: parse_output_line_f64("lld miss rate", lld_miss.next()),
111        llt: parse_output_line_f64("ilt miss rate", ll_total_miss.next()),
112    };
113
114    if args.json {
115        println!("{}", serde_json::to_string(&cache_miss)?);
116    } else {
117        println!("{}", serde_yaml::to_string(&cache_miss)?);
118    }
119
120    Ok(())
121}
122
123pub fn heap(
124    args: &Prof,
125    heap_args: &Heap,
126    cargo_fn: Option<fn(bin: &Option<String>) -> Result<Option<String>>>,
127) -> Result<()> {
128    let mut bin = args.bin.clone();
129    if let Some(cargo_fn) = cargo_fn {
130        bin = cargo_fn(&args.bin)?;
131    };
132    let output = valgrind(bin, args.target_args.clone(), Vec::new())?;
133    let exit_cap = Capture::new(r".*in use at exit\D*([\d|,]*)\D*([\d|,]*)", &output)
134        .context("Valgrind output")?;
135    let mut exit = exit_cap.iter_next();
136
137    let total_cap = Capture::new(
138        r".*total heap usage: ([\d|,]*)\D*([\d|,]*)\D*([\d|,]*)",
139        &output,
140    )
141    .context("Valgrind output")?;
142    let mut total = total_cap.iter_next();
143
144    let heap_usage = HeapSummary {
145        allocated_at_exit: parse_output_line("in use at exit", exit.next()),
146        blocks_at_exit: parse_output_line("in use at exit blocks", exit.next()),
147        allocations: parse_output_line("heap allocated", total.next()),
148        frees: parse_output_line("heap frees", total.next()),
149        allocated_total: parse_output_line("total heap usage", total.next())
150            - heap_args.subtract_bytes,
151    };
152
153    if args.json {
154        let parsed = serde_json::to_string(&heap_usage)?;
155        println!("{parsed}");
156    } else {
157        let human_readble = HeapSummaryHuman {
158            allocated_at_exit: human_bytes(heap_usage.allocated_at_exit),
159            blocks_at_exit: heap_usage.blocks_at_exit,
160            allocations: heap_usage.allocations,
161            frees: heap_usage.frees,
162            allocated_total: human_bytes(heap_usage.allocated_total),
163        };
164        let parsed = serde_yaml::to_string(&human_readble)?;
165        println!("{parsed}");
166    }
167
168    Ok(())
169}
170
171pub fn leak(
172    args: &Prof,
173    _leak_args: &Leak,
174    cargo_fn: Option<fn(bin: &Option<String>) -> Result<Option<String>>>,
175) -> Result<()> {
176    let mut bin = args.bin.clone();
177    if let Some(cargo_fn) = cargo_fn {
178        bin = cargo_fn(&args.bin)?;
179    };
180    let res = valgrind(bin, args.target_args.clone(), Vec::new())?;
181
182    let definite_cap = Capture::new(r".*definitely lost: ([\d|,]*)\D*([\d|,]*)", &res)
183        .context("Valgrind output")?;
184    let mut definite = definite_cap.iter_next();
185
186    let indirect_cap = Capture::new(r".*indirectly lost: ([\d|,]*)\D*([\d|,]*)", &res)
187        .context("Valgrind output")?;
188    let mut indirect = indirect_cap.iter_next();
189
190    let possible_cap =
191        Capture::new(r".*possibly lost: ([\d|,]*)\D*([\d|,]*)", &res).context("Valgrind output")?;
192    let mut possible = possible_cap.iter_next();
193
194    let reachable_cap = Capture::new(r".*still reachable: ([\d|,]*)\D*([\d|,]*)", &res)
195        .context("Valgrind output")?;
196    let mut reachable = reachable_cap.iter_next();
197
198    let suppressed_cap =
199        Capture::new(r".*suppressed: ([\d|,]*)\D*([\d|,]*)", &res).context("Valgrind output")?;
200    let mut suppressed = suppressed_cap.iter_next();
201
202    let leak_summary = LeakSummary {
203        definitely_lost: parse_output_line("definitely_lost", definite.next()),
204        definitely_lost_blocks: parse_output_line("definitely_lost_blocks", definite.next()),
205
206        indirectly_lost: parse_output_line("indirectly_lost", indirect.next()),
207        indrectly_lost_blocks: parse_output_line("indirect_lost_blocks", indirect.next()),
208
209        possibly_lost: parse_output_line("possibly_lost", possible.next()),
210        possibly_lost_blocks: parse_output_line("possibly_lost_blocks", possible.next()),
211
212        still_reachable: parse_output_line("still_reachable", reachable.next()),
213        still_reachable_blocks: parse_output_line("still_reachable_blocks", reachable.next()),
214
215        supressed: parse_output_line("supressed", suppressed.next()),
216        supressed_blocks: parse_output_line("supressed_blocks", suppressed.next()),
217    };
218
219    if !args.json {
220        let human_readble = LeakSummaryHuman {
221            definitely_lost: human_bytes(leak_summary.definitely_lost),
222            definitely_lost_blocks: leak_summary.definitely_lost,
223            indirectly_lost: human_bytes(leak_summary.indirectly_lost),
224            indrectly_lost_blocks: leak_summary.indrectly_lost_blocks,
225            possibly_lost: human_bytes(leak_summary.possibly_lost),
226            possibly_lost_blocks: leak_summary.possibly_lost_blocks,
227            still_reachable: human_bytes(leak_summary.still_reachable),
228            still_reachable_blocks: leak_summary.still_reachable_blocks,
229            supressed: human_bytes(leak_summary.supressed),
230            supressed_blocks: leak_summary.supressed_blocks,
231        };
232        let parsed = serde_yaml::to_string(&human_readble)?;
233        println!("{parsed}");
234    } else {
235        let parsed = serde_json::to_string(&leak_summary)?;
236        println!("{parsed}");
237    }
238
239    Ok(())
240}