evobench_tools/evaluator/
all_outputs_all_fields_table.rs

1use std::{
2    fs::File,
3    io::{BufWriter, Write},
4    ops::Deref,
5    path::PathBuf,
6};
7
8use anyhow::{Result, anyhow, bail};
9use cj_path_util::path_util::AppendToPath;
10use run_git::path_util::add_extension;
11
12use crate::{
13    config_file::ron_to_file_pretty,
14    evaluator::data::log_data_tree::LogDataTree,
15    evaluator::options::TILE_COUNT,
16    info,
17    io_utils::tempfile_utils::TempfileOptions,
18    join::KeyVal,
19    stats_tables::{
20        stats::StatsField,
21        tables::{excel_table_view::excel_file_write, table_view::TableView},
22    },
23    util::tree::Tree,
24    warn,
25};
26
27use super::{
28    all_fields_table::{
29        AllFieldsTable, AllFieldsTableKind, AllFieldsTableKindParams, KeyRuntimeDetails,
30        SingleRunStats, SummaryStats,
31    },
32    options::{CheckedOutputOptionsMapCase, EvaluationOpts, OutputVariants},
33};
34
35pub struct AllFieldsTableWithOutputPathOrBase<Kind: AllFieldsTableKind> {
36    aft: AllFieldsTable<Kind>,
37    /// The path or base for where this file or set of files is to end up in
38    output_path_or_base: PathBuf,
39    /// Whether *this* aft is actually to be stored at the above path;
40    /// false means, it's not processed to the final stage yet.
41    is_final_file: bool,
42}
43
44/// A wrapper holding the table sets for all requested
45/// outputs. (Wrapping since we want to have the same fields and
46/// mapping methods. A type alias would currently lose the trait
47/// restriction checks in Rust's type system.)
48pub struct AllOutputsAllFieldsTable<Kind: AllFieldsTableKind>(
49    OutputVariants<AllFieldsTableWithOutputPathOrBase<Kind>>,
50);
51
52impl<Kind: AllFieldsTableKind> Deref for AllOutputsAllFieldsTable<Kind> {
53    type Target = OutputVariants<AllFieldsTableWithOutputPathOrBase<Kind>>;
54
55    fn deref(&self) -> &Self::Target {
56        &self.0
57    }
58}
59
60fn key_details_for(
61    case: CheckedOutputOptionsMapCase,
62    evaluation_opts: &EvaluationOpts,
63) -> KeyRuntimeDetails {
64    let EvaluationOpts {
65        key_width,
66        show_thread_number,
67        show_reversed,
68    } = evaluation_opts;
69
70    let (
71        normal_separator,
72        reverse_separator,
73        show_probe_names,
74        show_paths_without_thread_number,
75        show_paths_reversed_too,
76        key_column_width,
77        skip_process,
78        prefix,
79    );
80    match case {
81        CheckedOutputOptionsMapCase::Excel => {
82            normal_separator = " > ";
83            reverse_separator = " < ";
84            show_probe_names = true;
85            show_paths_without_thread_number = true;
86            show_paths_reversed_too = *show_reversed;
87            key_column_width = Some(*key_width);
88            skip_process = false;
89            prefix = None;
90        }
91        CheckedOutputOptionsMapCase::Flame => {
92            normal_separator = ";";
93            reverse_separator = ";";
94            show_probe_names = false;
95            show_paths_without_thread_number = !*show_thread_number;
96            show_paths_reversed_too = false;
97            key_column_width = None;
98            skip_process = true;
99            prefix = Some("");
100        }
101    }
102
103    KeyRuntimeDetails {
104        normal_separator,
105        reverse_separator,
106        show_probe_names,
107        show_paths_without_thread_number,
108        show_paths_with_thread_number: *show_thread_number,
109        show_paths_reversed_too,
110        key_column_width,
111        skip_process,
112        prefix,
113    }
114}
115
116impl AllOutputsAllFieldsTable<SingleRunStats> {
117    pub fn from_log_data_tree(
118        log_data_tree: &LogDataTree,
119        evaluation_opts: &EvaluationOpts,
120        output_opts: OutputVariants<PathBuf>,
121        is_final_file: bool,
122    ) -> Result<Self> {
123        let output_variants = output_opts.try_map(|case, path| -> Result<_> {
124            Ok(AllFieldsTableWithOutputPathOrBase {
125                aft: AllFieldsTable::from_log_data_tree(
126                    log_data_tree,
127                    AllFieldsTableKindParams {
128                        source_path: log_data_tree.log_data().path.as_ref().into(),
129                        key_details: key_details_for(case, evaluation_opts),
130                    },
131                )?,
132                output_path_or_base: path,
133                is_final_file,
134            })
135        })?;
136        Ok(Self(output_variants))
137    }
138}
139
140impl AllOutputsAllFieldsTable<SummaryStats> {
141    pub fn summary_stats(
142        aoafts: &[AllOutputsAllFieldsTable<SingleRunStats>],
143        field_selector: StatsField<TILE_COUNT>,
144        evaluation_opts: &EvaluationOpts,
145        output_opts: OutputVariants<PathBuf>,
146        is_final_file: bool,
147    ) -> AllOutputsAllFieldsTable<SummaryStats> {
148        // Split up the `aoafts` by field
149        let lists_by_field = output_opts.clone().map(|case, _path| {
150            aoafts
151                .into_iter()
152                .map(|aoaft| {
153                    &aoaft
154                        .get(case)
155                        .as_ref()
156                        .expect(
157                            "same output_opts given in previous layer \
158                             leading to same set of options",
159                        )
160                        .aft
161                })
162                .collect::<Vec<_>>()
163        });
164        let x = lists_by_field.map(|case, afts| AllFieldsTableWithOutputPathOrBase {
165            aft: AllFieldsTable::summary_stats(
166                afts.as_slice(),
167                match case {
168                    CheckedOutputOptionsMapCase::Excel => field_selector,
169                    // Flame graphs always need the sums, thus ignore
170                    // the user option for those
171                    CheckedOutputOptionsMapCase::Flame => StatsField::Sum,
172                },
173                &key_details_for(case, evaluation_opts),
174            ),
175            output_path_or_base: output_opts.get(case).as_ref().expect("ditto").clone(),
176            is_final_file,
177        });
178        Self(x)
179    }
180}
181
182/// Get the sum of the children's values, and if those don't have a
183/// value, their children's values recursively. XX Could be a bit
184/// costly if there are many gaps!
185fn node_children_sum<'key>(tree: &Tree<'key, u64>) -> u64 {
186    tree.children
187        .iter()
188        .map(|(_, child)| child.value.unwrap_or_else(|| node_children_sum(child)))
189        .sum()
190}
191
192/// Convert a tree where the value of a parent include the values of
193/// the children (timings!) into one where the parent has only the
194/// remainder after subtracting the original values of the
195/// children.
196fn fix_tree<'key>(tree: Tree<'key, u64>) -> Tree<'key, u64> {
197    let value = tree.value.map(|orig_value| {
198        let orig_children_total: u64 = node_children_sum(&tree);
199        // orig_value - orig_children_total
200        orig_value
201            .checked_sub(orig_children_total)
202            .unwrap_or_else(|| {
203                eprintln!(
204                    "somehow parent has lower value, {orig_value}, \
205                     than sum of children, {orig_children_total}"
206                );
207                0
208            })
209    });
210    Tree {
211        value,
212        children: tree
213            .children
214            .into_iter()
215            .map(|(key, child)| (key, fix_tree(child)))
216            .collect(),
217    }
218}
219
220#[test]
221fn t_fix_tree() {
222    let vals = &[
223        ("a", 2),
224        ("a:b", 1),
225        ("a:b:c", 1),
226        ("c:d", 3),
227        ("d:e:f", 4),
228        ("d", 5),
229    ];
230    let tree = Tree::from_key_val(vals.into_iter().map(|(k, v)| (k.split(':'), *v)));
231    dbg!(&tree);
232    assert_eq!(tree.get("a".split(':')), Some(&2));
233    assert_eq!(tree.get("a:b".split(':')), Some(&1));
234    assert_eq!(tree.get("a:b:c".split(':')), Some(&1));
235    let tree = fix_tree(tree);
236    dbg!(&tree);
237    assert_eq!(tree.get("a".split(':')), Some(&1));
238    assert_eq!(tree.get("a:b".split(':')), Some(&0));
239    assert_eq!(tree.get("a:b:c".split(':')), Some(&1));
240    // panic!()
241}
242
243impl<Kind: AllFieldsTableKind> AllOutputsAllFieldsTable<Kind> {
244    /// Write to all output files originally specified; gives an error
245    /// unless the `is_final_file` for this instance was true. (Taking
246    /// ownership only because `try_map` currently requires so.)
247    pub fn write_to_files(self, flame_field: StatsField<TILE_COUNT>) -> Result<()> {
248        self.0.try_map(|case, aft| -> Result<()> {
249            let AllFieldsTableWithOutputPathOrBase {
250                aft,
251                output_path_or_base,
252                is_final_file,
253            } = aft;
254            if !is_final_file {
255                bail!(
256                    "trying to save a table that wasn't marked as \
257                     the last stage in a processing chain"
258                )
259            }
260            let tables = aft.tables();
261            match case {
262                CheckedOutputOptionsMapCase::Excel => {
263                    excel_file_write(
264                        tables.iter().map(|v| {
265                            let v: &dyn TableView = *v;
266                            v
267                        }),
268                        &output_path_or_base,
269                    )?;
270                }
271                CheckedOutputOptionsMapCase::Flame => {
272                    let curdir = PathBuf::from(".");
273                    let flame_base_dir = output_path_or_base.parent().unwrap_or(&*curdir);
274                    let flame_base_name = output_path_or_base
275                        .file_name()
276                        .ok_or_else(|| anyhow!("--flame option argument is missing a file name"))?
277                        .to_string_lossy();
278
279                    for table in tables {
280                        if table.table_key_vals(flame_field).next().is_none() {
281                            // The table has no rows. `inferno` is
282                            // giving errors when attempting to
283                            // generate flame graphs without data,
284                            // thus skip this table
285                            continue;
286                        }
287
288                        let lines: Vec<String> = {
289                            let tree = Tree::from_key_val(
290                                table
291                                    .table_key_vals(flame_field)
292                                    .map(|KeyVal { key, val }| (key.split(';'), val)),
293                            );
294
295                            let fixed_tree = fix_tree(tree);
296
297                            fixed_tree
298                                .into_joined_key_val(";")
299                                .into_iter()
300                                .map(|(path, val)| format!("{path} {val}"))
301                                .collect()
302                        };
303
304                        // `inferno` is really fussy, apparently it
305                        // gives a "No stack counts found" error
306                        // whenever it's missing any line with a ";"
307                        // in it, thus check:
308                        if !lines.iter().any(|s| s.contains(';')) {
309                            eprintln!(
310                                "note: there are no lines with ';' to be fed to inferno, \
311                                 thus do not attempt to generate flame graph"
312                            );
313                        } else {
314                            let target_path = flame_base_dir
315                                .append(format!("{flame_base_name}-{}.svg", table.table_name()));
316                            if let Err(e) = (|| -> Result<()> {
317                                let tempfile = TempfileOptions {
318                                    target_path: target_path.clone(),
319                                    retain_tempfile: true,
320                                    migrate_access: false,
321                                }
322                                .tempfile()?;
323
324                                let mut options = inferno::flamegraph::Options::default();
325                                options.count_name = table.resolution_unit();
326                                options.title = table.table_name().into();
327                                // options.subtitle = Some("foo".into()); XX show inputs key
328
329                                let mut out = BufWriter::new(File::create(&tempfile.temp_path)?);
330                                inferno::flamegraph::from_lines(
331                                    // why mut ??
332                                    &mut options,
333                                    lines.iter().map(|s| -> &str { s }),
334                                    &mut out,
335                                )?;
336                                out.flush()?;
337                                tempfile.finish()?;
338                                Ok(())
339                            })() {
340                                warn!(
341                                    "ignoring error creating flamegraph file \
342                                     {target_path:?}: {e:#}"
343                                );
344                                let dump_path = add_extension(&target_path, "data")
345                                    .expect("guaranteed to have file name");
346                                ron_to_file_pretty(&lines, &dump_path, false, None)?;
347                                info!(
348                                    "wrote data to be used in {target_path:?} here: {dump_path:?}"
349                                );
350                            }
351                        }
352                    }
353                }
354            }
355            Ok(())
356        })?;
357        Ok(())
358    }
359}