evobench_tools/evaluator/
all_fields_table.rs

1use std::{
2    borrow::Cow,
3    fmt::{Debug, Display},
4    num::NonZeroU32,
5    path::PathBuf,
6};
7
8use anyhow::Result;
9use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
10
11use crate::{
12    evaluator::{
13        data::{
14            log_data_tree::{LogDataTree, PathStringOptions, SpanId},
15            log_message::Timing,
16        },
17        index_by_call_path::IndexByCallPath,
18        options::TILE_COUNT,
19    },
20    join::{self, KeyVal, keyval_inner_join},
21    stats_tables::{
22        dynamic_typing::{StatsOrCount, StatsOrCountOrSubStats},
23        stats::{
24            Stats, StatsError, StatsField, ToStatsString,
25            weighted::{WEIGHT_ONE, WeightedValue},
26        },
27        tables::{
28            table::{Table, TableKind},
29            table_field_view::TableFieldView,
30        },
31    },
32    times::{MicroTime, NanoTime},
33    utillib::{rayon_util::par_run::ParRun, tuple_transpose::TupleTranspose},
34};
35
36fn scopestats<'t, K: KeyDetails>(
37    log_data_tree: &LogDataTree<'t>,
38    spans: &[SpanId<'t>],
39) -> Result<Stats<K::ViewType, TILE_COUNT>, StatsError> {
40    let vals: Vec<WeightedValue> = spans
41        .into_iter()
42        .filter_map(|span_id| -> Option<_> {
43            let span = span_id.get_from_db(log_data_tree);
44            let (start, end) = span.start_and_end()?;
45            let value: u64 = K::timing_extract(end)?.into() - K::timing_extract(start)?.into();
46            // Handle `EVOBENCH_SCOPE_EVERY` with `every_n > 1`
47            let weight = NonZeroU32::try_from(start.n())
48                .expect("num_runs is always at least 1 in the start Timing");
49            Some(WeightedValue { value, weight })
50        })
51        .collect();
52    Stats::from_values(vals)
53}
54
55fn pn_stats<'t, K: KeyDetails>(
56    log_data_tree: &LogDataTree<'t>,
57    spans: &[SpanId<'t>],
58    pn: &str,
59) -> Result<KeyVal<Cow<'static, str>, StatsOrCountOrSubStats<K::ViewType, TILE_COUNT>>, StatsError>
60{
61    let r: Result<Stats<K::ViewType, TILE_COUNT>, StatsError> =
62        scopestats::<K>(log_data_tree, spans);
63    match r {
64        Ok(s) => Ok(KeyVal {
65            key: pn.to_string().into(),
66            val: StatsOrCount::Stats(s).into(),
67        }),
68        Err(StatsError::NoInputs) => {
69            let count = spans.len();
70            Ok(KeyVal {
71                // Copy the keys to get a result with 'static lifetime
72                key: pn.to_string().into(),
73                val: StatsOrCount::Count(count).into(),
74            })
75        }
76        Err(e) => Err(e),
77    }
78}
79
80/// A table holding one field for all probes. We copy the keys (probe
81/// names) to get a resulting Table with 'static lifetime.
82fn table_for_field<'key, K: KeyDetails>(
83    kind: K,
84    log_data_tree: &LogDataTree<'key>,
85    index_by_call_path: &'key IndexByCallPath<'key>,
86) -> Result<Table<'static, K, StatsOrCountOrSubStats<K::ViewType, TILE_COUNT>>> {
87    let mut rows = Vec::new();
88
89    // Add the bare probe names, not paths, to the table if desired
90    if kind.show_probe_names() {
91        rows = log_data_tree
92            .probe_names()
93            .into_par_iter()
94            .map(|pn| -> Result<join::KeyVal<_, _>, StatsError> {
95                pn_stats::<K>(log_data_tree, log_data_tree.spans_by_pn(&pn).unwrap(), pn)
96            })
97            .collect::<Result<Vec<_>, StatsError>>()?;
98    }
99
100    let mut rows2 = index_by_call_path
101        .call_paths()
102        .into_par_iter()
103        .map(|call_path| {
104            pn_stats::<K>(
105                log_data_tree,
106                index_by_call_path.spans_by_call_path(call_path).unwrap(),
107                call_path,
108            )
109        })
110        .collect::<Result<Vec<_>, StatsError>>()?;
111
112    rows.append(&mut rows2);
113
114    Ok(Table { kind, rows })
115}
116
117/// How keys (in AllFieldsTable) are presented, and, unlike what the
118/// name suggests, also what rows are generated, since the grouping of
119/// the measurements depends on the set of generated key
120/// strings. (This only contains the runtime data, but unlike what the
121/// name suggests, actually there is no static data for the key column
122/// in the output? (PS. But there is the definition of the trait
123/// `KeyDetails` below, can't conflict with that.))
124#[derive(Clone, PartialEq, Debug)]
125pub struct KeyRuntimeDetails {
126    /// The separators to use
127    pub normal_separator: &'static str,
128    pub reverse_separator: &'static str,
129    /// Whether to use the probe names as keys (versus paths)
130    pub show_probe_names: bool,
131    pub show_paths_without_thread_number: bool,
132    pub show_paths_with_thread_number: bool,
133    pub show_paths_reversed_too: bool,
134    pub key_column_width: Option<f64>,
135    /// Override the standard prefixes--the same is used for all modes
136    /// above!
137    pub prefix: Option<&'static str>,
138    /// Do not show process measurement (used for flamegraph)
139    pub skip_process: bool,
140}
141
142impl KeyRuntimeDetails {
143    fn key_label(&self) -> String {
144        let mut cases = Vec::new();
145        cases.push("A: across all threads");
146        if self.show_paths_with_thread_number {
147            cases.push("N: by thread number");
148        }
149        if self.show_paths_reversed_too {
150            cases.push("..R: reversed");
151        }
152        format!("Probe name or path\n({})", cases.join(", ")).into()
153    }
154}
155
156trait KeyDetails: TableKind + Debug {
157    type ViewType: Into<u64> + From<u64> + ToStatsString + Debug + Display;
158    fn new(det: KeyRuntimeDetails) -> Self;
159    /// Extract a single value out of a `Timing`.
160    fn timing_extract(timing: &Timing) -> Option<Self::ViewType>;
161    /// Extract the statistics on these values out of an `AllFieldsTable`.
162    fn all_fields_table_extract<'f>(
163        aft: &'f AllFieldsTable<SingleRunStats>,
164    ) -> &'f Table<'static, Self, StatsOrCountOrSubStats<Self::ViewType, TILE_COUNT>>;
165    /// Whether probe *names* (not paths) are part of the table
166    fn show_probe_names(&self) -> bool;
167}
168
169macro_rules! def_key_details {
170    { $T:tt: $ViewType:tt, $table_name:tt, $timing_extract:expr, $aft_extract:expr, } => {
171        #[derive(Clone, Debug)]
172        pub struct $T(KeyRuntimeDetails);
173        impl TableKind for $T {
174            fn table_name(&self) -> Cow<'_, str> {
175                $table_name.into()
176            }
177            fn table_key_label(&self) -> Cow<'_, str> {
178                self.0.key_label().into()
179            }
180            fn table_key_column_width(&self) -> Option<f64> {
181                self.0.key_column_width
182            }
183        }
184        impl KeyDetails for $T {
185            type ViewType = $ViewType;
186            fn new(det: KeyRuntimeDetails) -> Self { Self(det) }
187            fn timing_extract(timing: &Timing) -> Option<Self::ViewType> {
188                ($timing_extract)(timing)
189            }
190            fn all_fields_table_extract<'f>(
191                aft: &'f AllFieldsTable<SingleRunStats>,
192            ) -> &'f Table<'static, Self, StatsOrCountOrSubStats<Self::ViewType, TILE_COUNT>>{
193                ($aft_extract)(aft)
194            }
195            fn show_probe_names(&self) -> bool {
196                self.0.show_probe_names
197            }
198        }
199    }
200}
201
202def_key_details! {
203    RealTime:
204    NanoTime, "real time",
205    |timing: &Timing| Some(timing.r),
206    |aft: &'f AllFieldsTable<SingleRunStats>| &aft.real_time,
207}
208def_key_details! {
209    CpuTime:
210    MicroTime, "cpu time",
211    |timing: &Timing| Some(timing.u),
212    |aft: &'f AllFieldsTable<SingleRunStats>| &aft.cpu_time,
213}
214def_key_details! {
215    SysTime:
216    MicroTime, "sys time",
217    |timing: &Timing| Some(timing.s),
218    |aft: &'f AllFieldsTable<SingleRunStats>| &aft.sys_time,
219}
220def_key_details! {
221    CtxSwitches:
222    u64, "ctx switches",
223    |timing: &Timing| Some(timing.nvcsw()? + timing.nivcsw()?),
224    |aft: &'f AllFieldsTable<SingleRunStats>| &aft.ctx_switches,
225}
226
227#[derive(Clone, Debug)]
228pub struct AllFieldsTableKindParams {
229    pub source_path: PathBuf,
230    pub key_details: KeyRuntimeDetails,
231}
232
233/// Markers to designate what a `Stats` value represents.
234pub trait AllFieldsTableKind {}
235
236/// Marks a `Stats` representing a single benchmarking run.
237pub struct SingleRunStats;
238impl AllFieldsTableKind for SingleRunStats {}
239
240/// Marks a `Stats` over multiple (or at least 1, anyway) identical
241/// benchmarking runs, to gain statistical insights. `Stats.n`
242/// represents the number of runs for these, not the number of calls.
243pub struct SummaryStats;
244impl AllFieldsTableKind for SummaryStats {}
245
246/// Marks a `Stats` containing Change records, i.e. trend values /
247/// lines, across SummaryStats.
248pub struct TrendStats;
249impl AllFieldsTableKind for TrendStats {}
250
251/// A group of 4 tables, one per real/cpu/sys time and ctx switches,
252/// rows representing probe points, although the exact rows depend on
253/// `params.key_details`
254pub struct AllFieldsTable<Kind: AllFieldsTableKind> {
255    pub kind: Kind,
256    /// The parameters this table set was created from/with, for cache
257    /// keying purposes.
258    pub params: AllFieldsTableKindParams,
259    pub real_time: Table<'static, RealTime, StatsOrCountOrSubStats<NanoTime, TILE_COUNT>>,
260    pub cpu_time: Table<'static, CpuTime, StatsOrCountOrSubStats<MicroTime, TILE_COUNT>>,
261    pub sys_time: Table<'static, SysTime, StatsOrCountOrSubStats<MicroTime, TILE_COUNT>>,
262    pub ctx_switches: Table<'static, CtxSwitches, StatsOrCountOrSubStats<u64, TILE_COUNT>>,
263}
264
265impl<Kind: AllFieldsTableKind> AsRef<AllFieldsTable<Kind>> for AllFieldsTable<Kind> {
266    fn as_ref(&self) -> &AllFieldsTable<Kind> {
267        self
268    }
269}
270
271impl<Kind: AllFieldsTableKind> AllFieldsTable<Kind> {
272    /// Return a list of tables, one for each field (real, cpu, sys
273    /// times and ctx switches), to e.g. be output to excel.
274    pub fn tables(&self) -> Vec<&dyn TableFieldView<TILE_COUNT>> {
275        let mut tables: Vec<&dyn TableFieldView<TILE_COUNT>> = vec![];
276        let Self {
277            kind: _,
278            params: _,
279            real_time,
280            cpu_time,
281            sys_time,
282            ctx_switches,
283        } = self;
284        tables.push(real_time);
285        tables.push(cpu_time);
286        tables.push(sys_time);
287        tables.push(ctx_switches);
288        tables
289    }
290}
291
292impl AllFieldsTable<SingleRunStats> {
293    pub fn from_log_data_tree(
294        log_data_tree: &LogDataTree,
295        params: AllFieldsTableKindParams,
296    ) -> Result<Self> {
297        let AllFieldsTableKindParams {
298            key_details,
299            // the whole `params` will be used below
300            source_path: _,
301        } = &params;
302
303        let KeyRuntimeDetails {
304            normal_separator,
305            reverse_separator,
306            show_paths_without_thread_number,
307            show_paths_with_thread_number,
308            show_paths_reversed_too,
309            skip_process,
310            prefix,
311            // show_probe_names and key_column_width are passed to
312            // `table_for_field` inside its `kind` argument
313            show_probe_names: _,
314            key_column_width: _,
315        } = key_details;
316        let skip_process = *skip_process;
317
318        let index_by_call_path = {
319            // Note: it's important to give prefixes here, to
320            // avoid getting rows that have the scopes counted
321            // *twice* (currently just "main thread"). (Could
322            // handle that in `IndexByCallPath::from_logdataindex`
323            // (by using a set instead of Vec), but having 1 entry
324            // that only counts thing once, but is valid for both
325            // kinds of groups, would surely still be confusing.)
326            let mut opts = vec![];
327            if *show_paths_without_thread_number {
328                opts.push(PathStringOptions {
329                    normal_separator,
330                    reverse_separator,
331                    ignore_process: true,
332                    skip_process,
333                    ignore_thread: true,
334                    include_thread_number_in_path: false,
335                    reversed: false,
336                    // "across threads / added up"
337                    prefix: prefix.unwrap_or("A:"),
338                });
339            }
340            // XX should this be nested in the above, like for
341            // show_paths_with_thread_number, or rather really not?
342            // Really should make separate options for ALL of
343            // those. Currently IIRC the logic is that the user's
344            // option is passed down only once, in
345            // show_paths_reversed_too, and we deal with it in this
346            // contorted way for that reason.
347            if *show_paths_reversed_too {
348                opts.push(PathStringOptions {
349                    normal_separator,
350                    reverse_separator,
351                    ignore_process: true,
352                    skip_process,
353                    ignore_thread: true,
354                    include_thread_number_in_path: false,
355                    reversed: true,
356                    prefix: prefix.unwrap_or("AR:"),
357                });
358            }
359            if *show_paths_with_thread_number {
360                opts.push(PathStringOptions {
361                    normal_separator,
362                    reverse_separator,
363                    ignore_process: true,
364                    skip_process,
365                    ignore_thread: true,
366                    include_thread_number_in_path: true,
367                    reversed: false,
368                    // "numbered threads"
369                    prefix: prefix.unwrap_or("N:"),
370                });
371                if *show_paths_reversed_too {
372                    opts.push(PathStringOptions {
373                        normal_separator,
374                        reverse_separator,
375                        ignore_process: true,
376                        skip_process,
377                        ignore_thread: true,
378                        include_thread_number_in_path: true,
379                        reversed: true,
380                        prefix: prefix.unwrap_or("NR:"),
381                    });
382                }
383            }
384            IndexByCallPath::from_logdataindex(&log_data_tree, &opts)
385        };
386
387        let (real_time, cpu_time, sys_time, ctx_switches) = (
388            || {
389                table_for_field(
390                    RealTime(key_details.clone()),
391                    &log_data_tree,
392                    &index_by_call_path,
393                )
394            },
395            || {
396                table_for_field(
397                    CpuTime(key_details.clone()),
398                    &log_data_tree,
399                    &index_by_call_path,
400                )
401            },
402            || {
403                table_for_field(
404                    SysTime(key_details.clone()),
405                    &log_data_tree,
406                    &index_by_call_path,
407                )
408            },
409            || {
410                table_for_field(
411                    CtxSwitches(key_details.clone()),
412                    &log_data_tree,
413                    &index_by_call_path,
414                )
415            },
416        )
417            .par_run()
418            .transpose()?;
419
420        Ok(AllFieldsTable {
421            kind: SingleRunStats,
422            params,
423            real_time,
424            cpu_time,
425            sys_time,
426            ctx_switches,
427        })
428    }
429}
430
431/// `K::all_fields_table_extract` extracts the field out of
432/// `AllFieldsTable` (e.g. cpu time, or ctx switches),
433/// `extract_stats_field` the kind of statistical value (e.g. median,
434/// average, counts, etc.)
435fn summary_stats_for_field<'t, K: KeyDetails + 'static>(
436    key_details: &KeyRuntimeDetails,
437    afts: &[impl AsRef<AllFieldsTable<SingleRunStats>> + Sync],
438    extract_stats_field: StatsField<TILE_COUNT>, // XX add to cache key somehow !
439) -> Table<'static, K, StatsOrCountOrSubStats<K::ViewType, TILE_COUNT>>
440where
441    K::ViewType: 'static,
442{
443    let mut rowss: Vec<_> = afts
444        .par_iter()
445        .map(|aft| {
446            Some(K::all_fields_table_extract(aft.as_ref()).rows.iter().map(
447                |KeyVal { key, val }| -> KeyVal<Cow<'static, str>, _> {
448                    KeyVal {
449                        key: key.clone(),
450                        val,
451                    }
452                },
453            ))
454        })
455        .collect();
456    let rows_merged: Vec<_> = keyval_inner_join(&mut rowss)
457        .expect("at least 1 table")
458        .collect();
459    let rows: Vec<_> = rows_merged
460        .into_par_iter()
461        .filter_map(|KeyVal { key, val }| {
462            // XX using WeightedValue here just because Stats requires
463            // it now! Kinda ugly? Make separate Stats methods?
464            let vals: Vec<WeightedValue> = val
465                .iter()
466                .filter_map(|s| match s {
467                    StatsOrCountOrSubStats::StatsOrCount(stats_or_count) => match stats_or_count {
468                        StatsOrCount::Stats(stats) => Some(stats.get(extract_stats_field)),
469                        StatsOrCount::Count(c) => {
470                            if extract_stats_field == StatsField::N {
471                                Some(u64::try_from(*c).expect("hopefully in range, here, too"))
472                            } else {
473                                None
474                            }
475                        }
476                    },
477                    StatsOrCountOrSubStats::SubStats(_sub_stats) => {
478                        unreachable!("SingleRunStats cannot contain SubStats")
479                    }
480                })
481                .map(|value| WeightedValue {
482                    value,
483                    weight: WEIGHT_ONE,
484                })
485                .collect();
486            let maybe_val = match Stats::<K::ViewType, TILE_COUNT>::from_values_from_field(
487                extract_stats_field,
488                vals,
489            ) {
490                Ok(val) => Some(val.into()),
491                Err(e) => match e {
492                    StatsError::NoInputs => {
493                        // This does happen, even after 'at least 1 table':
494                        // sure, if only a Count happened I guess?  So,
495                        // eliminate the row completely?
496                        None
497                    }
498                    StatsError::SaturatedU128 => {
499                        unreachable!("expecting to never see values > u64")
500                    }
501                    StatsError::VirtualCountDoesNotFitUSize => unreachable!("on 64bit archs"),
502                    StatsError::VirtualSumDoesNotFitU96 => panic!("stats error: {e:#}"),
503                },
504            };
505            let val = maybe_val?;
506            Some(KeyVal { key, val })
507        })
508        .collect();
509
510    Table {
511        kind: K::new(key_details.clone()), // XX ?*
512        rows,
513    }
514}
515
516impl AllFieldsTable<SummaryStats> {
517    pub fn summary_stats(
518        afts: &[impl AsRef<AllFieldsTable<SingleRunStats>> + Sync],
519        field_selector: StatsField<TILE_COUNT>,
520        key_details: &KeyRuntimeDetails,
521    ) -> AllFieldsTable<SummaryStats> {
522        // XX panic happy everywhere...
523        let params = afts[0].as_ref().params.clone();
524        for aft in afts {
525            if params.key_details != aft.as_ref().params.key_details {
526                panic!(
527                    "unequal key_details in params: {:?} vs. {:?}",
528                    params,
529                    aft.as_ref().params
530                );
531            }
532        }
533
534        let (real_time, cpu_time, sys_time, ctx_switches) = (
535            || summary_stats_for_field::<RealTime>(key_details, afts, field_selector),
536            || summary_stats_for_field::<CpuTime>(key_details, afts, field_selector),
537            || summary_stats_for_field::<SysTime>(key_details, afts, field_selector),
538            || summary_stats_for_field::<CtxSwitches>(key_details, afts, field_selector),
539        )
540            .par_run();
541
542        AllFieldsTable {
543            kind: SummaryStats,
544            params,
545            real_time,
546            cpu_time,
547            sys_time,
548            ctx_switches,
549        }
550    }
551}