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 #[clap(short, long, global = true)]
23 pub bin: Option<String>,
24
25 #[clap(short, long, global = true)]
27 pub json: bool,
28
29 #[clap(last = true, global = true)]
31 pub target_args: Vec<String>,
32}
33
34#[derive(Subcommand, Debug)]
35pub enum Commands {
36 Heap(Heap),
38 Leak(Leak),
40 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 #[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}