inferno/collapse/ghcprof.rs
1use std::io::{self, BufRead};
2
3use log::warn;
4
5use crate::collapse::common::Occurrences;
6use crate::collapse::Collapse;
7
8// These are the identifying words of the callgraph table, note that ticks and bytes columns are optional so not present
9static START_LINE: &[&str] = &[
10 "COST", "CENTRE", "MODULE", "SRC", "no.", "entries", "%time", "%alloc", "%time", "%alloc",
11];
12
13/// `ghcprof` folder configuration options.
14#[derive(Clone, Debug, Default)]
15#[non_exhaustive]
16pub struct Options {
17 /// Column to source associated value from, default is `Source::PercentTime`.
18 pub source: Source,
19}
20
21/// Which prof column to use as the cost centre of the output stacks
22#[derive(Clone, Debug, Default)]
23#[non_exhaustive]
24pub enum Source {
25 #[default]
26 /// The indivial %time column representing individual time as a percent of the total
27 PercentTime,
28 /// The ticks column representing individual runtime ticks
29 Ticks,
30 /// The bytes column representing individual bytes allocated
31 Bytes,
32}
33
34/// A stack collapser for the output of `ghc`'s prof files.
35///
36/// To construct one, either use `ghcprof::Folder::default()` or create an [`Options`] and use
37/// `ghcprof::Folder::from(options)`.
38#[derive(Clone, Default)]
39pub struct Folder {
40 /// Cost for the current stack frame.
41 current_cost: u64,
42
43 /// Function on the stack in this entry thus far.
44 stack: Vec<String>,
45
46 opt: Options,
47}
48
49// The starting character offset of important columns
50#[derive(Debug)]
51struct Cols {
52 cost_centre: usize,
53 module: usize,
54 source: usize,
55}
56
57impl Collapse for Folder {
58 fn collapse<R, W>(&mut self, mut reader: R, writer: W) -> io::Result<()>
59 where
60 R: io::BufRead,
61 W: io::Write,
62 {
63 // Consume the header...
64 let mut line = Vec::new();
65 let cols = loop {
66 line.clear();
67 if reader.read_until(b'\n', &mut line)? == 0 {
68 warn!("File ended before start of call graph");
69 return Ok(());
70 };
71 let l = String::from_utf8_lossy(&line);
72
73 if l.split_whitespace()
74 .take(START_LINE.len())
75 .eq(START_LINE.iter().cloned())
76 {
77 let cost_centre = 0;
78 let module = l.find("MODULE").unwrap_or(0);
79 // Pick out these fixed columns, first two are individual only
80 // "%time %alloc %time %alloc"
81 // `ticks` and `bytes` columns are optional and might appear on the end
82 // ticks header is right aligned
83 // bytes header is right aligned
84 // - BUT it has a max width of 9 whilst its values can exceed (but are always space separted)
85 // "%time %alloc %time %alloc ticks bytes"
86 let source = match self.opt.source {
87 Source::PercentTime => l
88 .find("%time")
89 .expect("%time is present from matching START_LINE"),
90 // See note above about ticks and bytes columns
91 Source::Ticks => one_off_end_of_col_before(l.as_ref(), "ticks")?,
92 Source::Bytes => one_off_end_of_col_before(l.as_ref(), "bytes")?,
93 };
94 break Cols {
95 cost_centre,
96 module,
97 source,
98 };
99 }
100 };
101 // Skip one line
102 reader.read_until(b'\n', &mut line)?;
103
104 // Process the data...
105 let mut occurrences = Occurrences::new(1);
106 loop {
107 line.clear();
108 if reader.read_until(b'\n', &mut line)? == 0 {
109 // The format is not expected to contain any blank lines within the callgraph
110 break;
111 }
112 let l = String::from_utf8_lossy(&line);
113 let line = l.trim_end();
114 if line.is_empty() {
115 break;
116 } else {
117 self.on_line(line, &mut occurrences, &cols)?;
118 }
119 }
120
121 // Write the results...
122 occurrences.write_and_clear(writer)?;
123
124 // Reset the state...
125 self.current_cost = 0;
126 self.stack.clear();
127 Ok(())
128 }
129
130 /// Check for start line of a call graph.
131 fn is_applicable(&mut self, input: &str) -> Option<bool> {
132 let mut input = input.as_bytes();
133 let mut line = String::new();
134 loop {
135 line.clear();
136 if let Ok(n) = input.read_line(&mut line) {
137 if n == 0 {
138 break;
139 }
140 } else {
141 return Some(false);
142 }
143
144 if line
145 .split_whitespace()
146 .take(START_LINE.len())
147 .eq(START_LINE.iter().cloned())
148 {
149 return Some(true);
150 }
151 }
152 None
153 }
154}
155
156fn one_off_end_of_col_before(line: &str, col: &str) -> io::Result<usize> {
157 let col_start = match line.find(col) {
158 Some(col_start) => col_start,
159 _ => return invalid_data_error!("Expected '{col}' column but it was not present"),
160 };
161 let col_end = match line[..col_start].rfind(|c: char| !c.is_whitespace()) {
162 Some(col_end) => col_end,
163 _ => return invalid_data_error!("Expected a column before '{col}' but there was none"),
164 };
165 Ok(col_end + 1)
166}
167
168impl From<Options> for Folder {
169 fn from(opt: Options) -> Self {
170 Folder {
171 opt,
172 ..Default::default()
173 }
174 }
175}
176
177impl Folder {
178 // Handle call graph lines of the form:
179 //
180 // MAIN MAIN ...
181 // CAF Options.Applicative.Builder ...
182 // defaultPrefs Options.Applicative.Builder ...
183 // idm Options.Applicative.Builder ...
184 // prefs Options.Applicative.Builder ...
185 // fullDesc Options.Applicative.Builder ...
186 // hidden Options.Applicative.Builder ...
187 // option Options.Applicative.Builder ...
188 // metavar Options.Applicative.Builder ...
189 // CAF Options.Applicative.Builder.Internal ...
190 // internal Options.Applicative.Builder.Internal ...
191 // noGlobal Options.Applicative.Builder.Internal ...
192 // optionMod Options.Applicative.Builder.Internal ...
193
194 fn on_line(
195 &mut self,
196 line: &str,
197 occurrences: &mut Occurrences,
198 cols: &Cols,
199 ) -> io::Result<()> {
200 if let Some(indent_chars) = line.find(|c| c != ' ') {
201 let prev_len = self.stack.len();
202 let depth = indent_chars;
203
204 if depth < prev_len {
205 // If the line is not a child, pop stack to the stack before the new depth
206 self.stack.truncate(depth);
207 } else if depth != prev_len {
208 return invalid_data_error!("Skipped indentation level at line:\n{}", line);
209 }
210 // There can be non-ascii names so take care to char offset not byte offset
211 let string_range = |col_start: usize| {
212 line.chars()
213 .skip(col_start)
214 .skip_while(|c| c.is_whitespace())
215 // it is expected that the values to extract do not contain whitespace
216 // since this is used for functions/modules/costs where it is not allowed
217 .take_while(|c| !c.is_whitespace())
218 .collect::<String>()
219 };
220 let cost = string_range(cols.source);
221 if let Ok(cost) = cost.trim().parse::<f64>() {
222 let func = string_range(cols.cost_centre);
223 let module = string_range(cols.module);
224 // The columns we extract costs from all exclude the cost of their children
225 self.current_cost = match self.opt.source {
226 // We must `insert_or_add` a `u64` so convert to per-mille to not lose the 1dp
227 Source::PercentTime => cost * 10.0,
228 Source::Ticks => cost,
229 Source::Bytes => cost,
230 } as u64;
231 self.stack
232 .push(format!("{}.{}", module.trim(), func.trim()));
233 // identical stacks from other threads can appear so need to insert or add
234 occurrences.insert_or_add(self.stack.join(";"), self.current_cost);
235 } else {
236 return invalid_data_error!("Invalid cost field: \"{}\"", cost);
237 }
238 }
239
240 Ok(())
241 }
242}