Skip to main content

rudb_kernels/
fallback.rs

1//! How often a kernel took the row at a time path, and for which shape of input.
2//!
3//! `spec/engine/03-data-plane.md` asks for this by name, and the reason is that the alternative to
4//! counting is guessing. There are four physical forms, so sixteen form pairs per kernel, and
5//! writing a hand tuned loop for all sixteen is both a lot of code and a lot of places for a wrong
6//! answer to hide. Writing three of them and a correct slow path for the rest is the right amount
7//! of code, but only if there is a way to find out that the fourth is on the hot path of a real
8//! query. That way is this.
9//!
10//! What gets counted is the fall through, not the fast path. A counter on the fast path would cost
11//! an atomic increment per vector on the loop this whole layer exists to make fast, and it would
12//! measure something nobody needs to know. A counter on the slow path costs an atomic increment on
13//! a loop that is already allocating a `Value` per row, which is not measurable next to what it
14//! sits on.
15//!
16//! The counts are process wide and never reset by the library. A benchmark harness reads them at
17//! the end of a run and prints the ones that are not zero, which turns "we should probably
18//! specialize sequence against constant" into either a number or silence.
19
20use std::sync::atomic::{AtomicU64, Ordering};
21
22use rudb_vector::Form;
23
24/// Which kernel fell through.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub enum Kernel {
27    /// The comparisons, in `compare`.
28    Compare,
29    /// The scalar functions, in `scalar`.
30    Scalar,
31    /// Three-valued logic, in `logic`.
32    Logic,
33    /// The conversions, in `cast`.
34    Cast,
35    /// The aggregates.
36    Aggregate,
37    /// Turning a vector of flags into the rows it keeps, in `select`.
38    Select,
39}
40
41impl Kernel {
42    /// Every kernel that reports, in the order the table prints them.
43    const ALL: [Self; 6] =
44        [Self::Compare, Self::Scalar, Self::Logic, Self::Cast, Self::Aggregate, Self::Select];
45
46    /// The name used in the report.
47    #[must_use]
48    pub fn name(self) -> &'static str {
49        match self {
50            Self::Compare => "compare",
51            Self::Scalar => "scalar",
52            Self::Logic => "logic",
53            Self::Cast => "cast",
54            Self::Aggregate => "aggregate",
55            Self::Select => "select",
56        }
57    }
58
59    fn index(self) -> usize {
60        match self {
61            Self::Compare => 0,
62            Self::Scalar => 1,
63            Self::Logic => 2,
64            Self::Cast => 3,
65            Self::Aggregate => 4,
66            Self::Select => 5,
67        }
68    }
69}
70
71/// Every physical form, in the order the table prints them.
72const FORMS: [Form; 4] = [Form::Flat, Form::Constant, Form::Sequence, Form::Dictionary];
73
74/// The name of a form, for the report.
75fn form_name(form: Form) -> &'static str {
76    match form {
77        Form::Flat => "flat",
78        Form::Constant => "constant",
79        Form::Sequence => "sequence",
80        Form::Dictionary => "dictionary",
81        // `Form` is not exhaustive as far as this crate is concerned, and layer three adds
82        // `Encoded` to it. A name rather than a panic means the day that lands is a day the report
83        // says `other` for a while, not a day the report aborts the process.
84        _ => "other",
85    }
86}
87
88/// The position of a form in [`FORMS`], or four for one this build does not know about.
89fn form_index(form: Form) -> usize {
90    FORMS.iter().position(|&known| known == form).unwrap_or(FORMS.len())
91}
92
93/// One counter per kernel per form pair, plus a row and a column for a form added later.
94const WIDTH: usize = FORMS.len() + 1;
95const CELLS: usize = Kernel::ALL.len() * WIDTH * WIDTH;
96
97static COUNTS: [AtomicU64; CELLS] = [const { AtomicU64::new(0) }; CELLS];
98
99/// Where a kernel and a form pair live in the table.
100fn cell(kernel: Kernel, left: Form, right: Form) -> usize {
101    kernel.index() * WIDTH * WIDTH + form_index(left) * WIDTH + form_index(right)
102}
103
104/// Records that a kernel took the row at a time path on this pair of forms.
105///
106/// `Relaxed` because nothing reads this to make a decision while a query is running. It is a
107/// diagnostic that is read once, after, by a harness, and paying for ordering on it would be
108/// paying for a guarantee nobody uses.
109pub fn record(kernel: Kernel, left: Form, right: Form) {
110    COUNTS[cell(kernel, left, right)].fetch_add(1, Ordering::Relaxed);
111}
112
113/// How many times a kernel fell through on this pair of forms.
114#[must_use]
115pub fn count(kernel: Kernel, left: Form, right: Form) -> u64 {
116    COUNTS[cell(kernel, left, right)].load(Ordering::Relaxed)
117}
118
119/// Every combination that has fallen through at least once, most frequent first.
120#[must_use]
121pub fn hot() -> Vec<(Kernel, Form, Form, u64)> {
122    let mut out = Vec::new();
123    for kernel in Kernel::ALL {
124        for left in FORMS {
125            for right in FORMS {
126                let seen = count(kernel, left, right);
127                if seen > 0 {
128                    out.push((kernel, left, right, seen));
129                }
130            }
131        }
132    }
133    out.sort_by_key(|entry| std::cmp::Reverse(entry.3));
134    out
135}
136
137/// Sets every counter back to zero.
138///
139/// For a harness that wants the counts for one query rather than for a process, and for the tests
140/// below. It is not synchronized against a running query, because a diagnostic that took a lock
141/// would be a diagnostic that changed what it measures.
142pub fn reset() {
143    for counter in &COUNTS {
144        counter.store(0, Ordering::Relaxed);
145    }
146}
147
148/// The lock every test that resets the counters holds while it does.
149///
150/// The counters are process wide and the test harness runs tests in parallel, so two tests that
151/// both reset would otherwise pass alone and fail together, which is the worst kind of test to own.
152/// It lives here rather than in the test module below because the kernel tests in the other files
153/// reset the counters too and they need the same lock, not a second one.
154#[cfg(test)]
155pub(crate) static TURN: std::sync::Mutex<()> = std::sync::Mutex::new(());
156
157/// The counts as a table, or a line saying there are none.
158#[must_use]
159pub fn report() -> String {
160    let hot = hot();
161    if hot.is_empty() {
162        return "every kernel call took a specialized path".to_owned();
163    }
164    let mut out = String::from("kernel calls that fell through to the row at a time path\n");
165    for (kernel, left, right, seen) in hot {
166        out.push_str(&format!(
167            "  {:<10} {:<10} against {:<10} {seen}\n",
168            kernel.name(),
169            form_name(left),
170            form_name(right)
171        ));
172    }
173    out
174}
175
176#[cfg(test)]
177mod tests {
178    use super::{Form, Kernel, TURN, count, hot, record, report, reset};
179
180    #[test]
181    fn a_fall_through_lands_in_the_cell_for_its_own_form_pair() {
182        let _turn = TURN.lock().expect("no test panics while holding this");
183        reset();
184        record(Kernel::Compare, Form::Sequence, Form::Constant);
185        record(Kernel::Compare, Form::Sequence, Form::Constant);
186        record(Kernel::Cast, Form::Dictionary, Form::Flat);
187        assert_eq!(count(Kernel::Compare, Form::Sequence, Form::Constant), 2);
188        assert_eq!(count(Kernel::Cast, Form::Dictionary, Form::Flat), 1);
189        assert_eq!(count(Kernel::Compare, Form::Constant, Form::Sequence), 0);
190        assert_eq!(count(Kernel::Compare, Form::Flat, Form::Flat), 0);
191        reset();
192    }
193
194    #[test]
195    fn the_report_names_the_combination_rather_than_a_number_on_its_own() {
196        let _turn = TURN.lock().expect("no test panics while holding this");
197        reset();
198        assert!(report().contains("every kernel call took a specialized path"));
199        for _ in 0..7 {
200            record(Kernel::Compare, Form::Sequence, Form::Constant);
201        }
202        record(Kernel::Logic, Form::Flat, Form::Dictionary);
203        let text = report();
204        assert!(text.contains("compare"), "{text}");
205        assert!(text.contains("sequence"), "{text}");
206        assert!(text.contains('7'), "{text}");
207        // Most frequent first, because the point of the table is to say what to specialize next.
208        assert_eq!(hot().first().map(|entry| entry.3), Some(7));
209        reset();
210    }
211}