Skip to main content

egglog_reports/
lib.rs

1use clap::clap_derive::ValueEnum;
2use rustc_hash::FxHasher;
3use serde::Serialize;
4use std::{
5    fmt::{Display, Formatter},
6    hash::BuildHasherDefault,
7    sync::Arc,
8};
9use web_time::Duration;
10
11pub(crate) type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<FxHasher>>;
12pub(crate) type IndexSet<T> = indexmap::IndexSet<T, BuildHasherDefault<FxHasher>>;
13
14#[derive(ValueEnum, Default, Serialize, Debug, Clone, Copy)]
15pub enum ReportLevel {
16    /// Report only the time taken in each search/apply, merge, rebuild phase.
17    #[default]
18    TimeOnly,
19    /// Report [`ReportLevel::TimeOnly`] and query plan for each rule
20    WithPlan,
21    /// Report [`ReportLevel::WithPlan`] and the detailed statistics at each stage of the query plan.
22    StageInfo,
23}
24
25#[derive(Serialize, Clone, Debug)]
26pub struct SingleScan(pub String, pub (String, i64));
27#[derive(Serialize, Clone, Debug)]
28pub struct Scan(pub String, pub Vec<(String, i64)>);
29
30#[derive(Serialize, Clone, Debug)]
31pub enum Stage {
32    Intersect {
33        scans: Vec<SingleScan>,
34    },
35    FusedIntersect {
36        cover: Scan,             // build side
37        to_intersect: Vec<Scan>, // probe sides
38    },
39}
40
41#[derive(Serialize, Clone, Debug)]
42pub struct StageStats {
43    pub num_candidates: usize,
44    pub num_succeeded: usize,
45}
46
47#[derive(Serialize, Clone, Debug, Default)]
48pub struct Plan {
49    pub stages: Vec<(
50        Stage,
51        Option<StageStats>,
52        // indices of next stages
53        Vec<usize>,
54    )>,
55}
56
57#[derive(Debug, Serialize, Clone, Default)]
58pub struct RuleReport {
59    pub plan: Option<Plan>,
60    pub search_and_apply_time: Duration,
61    // TODO: succeeding matches
62    pub num_matches: usize,
63}
64
65#[derive(Debug, Serialize, Clone, Default)]
66pub struct RuleSetReport {
67    pub changed: bool,
68    pub rule_reports: HashMap<Arc<str>, Vec<RuleReport>>,
69    pub search_and_apply_time: Duration,
70    pub merge_time: Duration,
71}
72
73impl RuleSetReport {
74    pub fn num_matches(&self, rule: &str) -> usize {
75        self.rule_reports
76            .get(rule)
77            .map(|r| r.iter().map(|r| r.num_matches).sum())
78            .unwrap_or(0)
79    }
80
81    pub fn rule_search_and_apply_time(&self, rule: &str) -> Duration {
82        self.rule_reports
83            .get(rule)
84            .map(|r| r.iter().map(|r| r.search_and_apply_time).sum())
85            .unwrap_or(Duration::ZERO)
86    }
87}
88
89#[derive(Debug, Serialize, Clone, Default)]
90pub struct IterationReport {
91    pub rule_set_report: RuleSetReport,
92    pub rebuild_time: Duration,
93}
94
95impl IterationReport {
96    pub fn changed(&self) -> bool {
97        self.rule_set_report.changed
98    }
99
100    pub fn search_and_apply_time(&self) -> Duration {
101        self.rule_set_report.search_and_apply_time
102    }
103
104    pub fn rule_reports(&self) -> &HashMap<Arc<str>, Vec<RuleReport>> {
105        &self.rule_set_report.rule_reports
106    }
107
108    pub fn rules(&self) -> impl Iterator<Item = &Arc<str>> {
109        self.rule_set_report.rule_reports.keys()
110    }
111}
112
113/// Running a schedule produces a report of the results.
114/// This includes rough timing information and whether
115/// the database was updated.
116/// Calling `union` on two run reports adds the timing
117/// information together.
118#[derive(Debug, Serialize, Clone, Default)]
119pub struct RunReport {
120    // Since `IterationReport`s are immutable, we can reference count them to avoid
121    // expensive cloning when e-graphs are cloned.
122    pub iterations: Vec<Arc<IterationReport>>,
123    /// If any changes were made to the database.
124    pub updated: bool,
125    pub search_and_apply_time_per_rule: HashMap<Arc<str>, Duration>,
126    pub num_matches_per_rule: HashMap<Arc<str>, usize>,
127    pub search_and_apply_time_per_ruleset: HashMap<Arc<str>, Duration>,
128    pub merge_time_per_ruleset: HashMap<Arc<str>, Duration>,
129    pub rebuild_time_per_ruleset: HashMap<Arc<str>, Duration>,
130}
131
132impl Display for RunReport {
133    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134        let mut rule_times_vec: Vec<_> = self.search_and_apply_time_per_rule.iter().collect();
135        rule_times_vec.sort_by_key(|(_, time)| **time);
136
137        for (rule, time) in rule_times_vec {
138            let name = Self::truncate_rule_name(rule.to_string());
139            let time = time.as_secs_f64();
140            let num_matches = self.num_matches_per_rule.get(rule).copied().unwrap_or(0);
141            writeln!(
142                f,
143                "Rule {name}: search and apply {time:.3}s, num matches {num_matches}",
144            )?;
145        }
146
147        let rulesets = self
148            .search_and_apply_time_per_ruleset
149            .keys()
150            .chain(self.merge_time_per_ruleset.keys())
151            .chain(self.rebuild_time_per_ruleset.keys())
152            .collect::<IndexSet<_>>();
153
154        for ruleset in rulesets {
155            // print out the search and apply time for rule
156            let search_and_apply_time = self
157                .search_and_apply_time_per_ruleset
158                .get(ruleset)
159                .cloned()
160                .unwrap_or(Duration::ZERO)
161                .as_secs_f64();
162            let merge_time = self
163                .merge_time_per_ruleset
164                .get(ruleset)
165                .cloned()
166                .unwrap_or(Duration::ZERO)
167                .as_secs_f64();
168            let rebuild_time = self
169                .rebuild_time_per_ruleset
170                .get(ruleset)
171                .cloned()
172                .unwrap_or(Duration::ZERO)
173                .as_secs_f64();
174            writeln!(
175                f,
176                "Ruleset {ruleset}: search {search_and_apply_time:.3}s, merge {merge_time:.3}s, rebuild {rebuild_time:.3}s",
177            )?;
178        }
179
180        Ok(())
181    }
182}
183
184impl RunReport {
185    /// add a ... and a maximum size to the name
186    /// for printing, since they may be the rule itself
187    fn truncate_rule_name(mut s: String) -> String {
188        // replace newlines in s with a space
189        s = s.replace('\n', " ");
190        if s.len() > 80 {
191            s.truncate(80);
192            s.push_str("...");
193        }
194        s
195    }
196
197    fn union_times(
198        times: &mut HashMap<Arc<str>, Duration>,
199        other_times: HashMap<Arc<str>, Duration>,
200    ) {
201        for (k, v) in other_times {
202            *times.entry(k).or_default() += v;
203        }
204    }
205
206    fn union_counts(counts: &mut HashMap<Arc<str>, usize>, other_counts: HashMap<Arc<str>, usize>) {
207        for (k, v) in other_counts {
208            *counts.entry(k).or_default() += v;
209        }
210    }
211
212    pub fn singleton(ruleset: &str, iteration: IterationReport) -> Self {
213        let mut report = RunReport::default();
214
215        for rule in iteration.rules() {
216            *report
217                .search_and_apply_time_per_rule
218                .entry(rule.clone())
219                .or_default() += iteration.rule_set_report.rule_search_and_apply_time(rule);
220            *report.num_matches_per_rule.entry(rule.clone()).or_default() +=
221                iteration.rule_set_report.num_matches(rule);
222        }
223
224        let ruleset: Arc<str> = ruleset.into();
225        let per_ruleset = |x| [(ruleset.clone(), x)].into_iter().collect();
226
227        report.search_and_apply_time_per_ruleset = per_ruleset(iteration.search_and_apply_time());
228        report.merge_time_per_ruleset = per_ruleset(iteration.rule_set_report.merge_time);
229        report.rebuild_time_per_ruleset = per_ruleset(iteration.rebuild_time);
230        report.updated = iteration.changed();
231        report.iterations.push(Arc::new(iteration));
232
233        report
234    }
235
236    pub fn add_iteration(&mut self, ruleset: &str, iteration: IterationReport) {
237        self.union(RunReport::singleton(ruleset, iteration));
238    }
239
240    /// Merge two reports.
241    pub fn union(&mut self, other: Self) {
242        self.iterations.extend(other.iterations);
243        self.updated |= other.updated;
244        RunReport::union_times(
245            &mut self.search_and_apply_time_per_rule,
246            other.search_and_apply_time_per_rule,
247        );
248        RunReport::union_counts(&mut self.num_matches_per_rule, other.num_matches_per_rule);
249        RunReport::union_times(
250            &mut self.search_and_apply_time_per_ruleset,
251            other.search_and_apply_time_per_ruleset,
252        );
253        RunReport::union_times(
254            &mut self.merge_time_per_ruleset,
255            other.merge_time_per_ruleset,
256        );
257        RunReport::union_times(
258            &mut self.rebuild_time_per_ruleset,
259            other.rebuild_time_per_ruleset,
260        );
261    }
262}