Skip to main content

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}