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 eight physical forms, so sixty four form pairs per kernel, and
5//! writing a hand tuned loop for all of them is both a lot of code and a lot of places for a wrong
6//! answer to hide. Writing the handful a real query hits and a correct slow path for the rest is
7//! the right amount of code, but only if there is a way to find out that one of the rest is on the
8//! hot path of a real 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//!
20//! There is a second counter and [`record`] bumps both. This one answers which form pair to go and
21//! write a specialization for, which is a question about a build rather than about a query, so it is
22//! process wide and has no idea which operator was running. [`rudb_common::slow`] answers which
23//! operator in this query is the one paying, which needs the count to be per thread so that the
24//! instrumentation shim can take a difference around a call. Neither number can be worked out from
25//! the other, they cost an add each, and the alternative to having both is reading one of them and
26//! guessing the other.
27
28use std::sync::atomic::{AtomicU64, Ordering};
29
30use rudb_common::{Cause, slow};
31use rudb_vector::Form;
32
33/// Which kernel fell through.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Kernel {
36    /// The comparisons, in `compare`.
37    Compare,
38    /// The scalar functions, in `scalar`.
39    Scalar,
40    /// Three-valued logic, in `logic`.
41    Logic,
42    /// The conversions, in `cast`.
43    Cast,
44    /// The aggregates.
45    Aggregate,
46    /// Turning a vector of flags into the rows it keeps, in `select`.
47    Select,
48    /// The `IN` lists, in `membership`.
49    Membership,
50}
51
52impl Kernel {
53    /// Every kernel that reports, in the order the table prints them.
54    const ALL: [Self; 7] = [
55        Self::Compare,
56        Self::Scalar,
57        Self::Logic,
58        Self::Cast,
59        Self::Aggregate,
60        Self::Select,
61        Self::Membership,
62    ];
63
64    /// The name used in the report.
65    #[must_use]
66    pub fn name(self) -> &'static str {
67        match self {
68            Self::Compare => "compare",
69            Self::Scalar => "scalar",
70            Self::Logic => "logic",
71            Self::Cast => "cast",
72            Self::Aggregate => "aggregate",
73            Self::Select => "select",
74            Self::Membership => "membership",
75        }
76    }
77
78    fn index(self) -> usize {
79        match self {
80            Self::Compare => 0,
81            Self::Scalar => 1,
82            Self::Logic => 2,
83            Self::Cast => 3,
84            Self::Aggregate => 4,
85            Self::Select => 5,
86            Self::Membership => 6,
87        }
88    }
89
90    /// The same kernel as the metrics document names it.
91    ///
92    /// Two enums for one list of kernels is not ideal and it is the layer rule rather than a
93    /// preference. The document is written at rank 4 and this crate is at rank 3, so the vocabulary
94    /// the document is spelled in has to be somewhere both can see, which is rank 0. The test below
95    /// is what keeps the two lists the same list.
96    const fn cause(self) -> Cause {
97        match self {
98            Self::Compare => Cause::Compare,
99            Self::Scalar => Cause::Scalar,
100            Self::Logic => Cause::Logic,
101            Self::Cast => Cause::Cast,
102            Self::Aggregate => Cause::Aggregate,
103            Self::Select => Cause::Select,
104            Self::Membership => Cause::Membership,
105        }
106    }
107}
108
109/// Every physical form, in the order the table prints them.
110const FORMS: [Form; 8] = [
111    Form::Flat,
112    Form::Constant,
113    Form::Sequence,
114    Form::Dictionary,
115    Form::Rle,
116    Form::BitPacked,
117    Form::StringView,
118    Form::Fsst,
119];
120
121/// The name of a form, for the report.
122fn form_name(form: Form) -> &'static str {
123    match form {
124        Form::Flat => "flat",
125        Form::Constant => "constant",
126        Form::Sequence => "sequence",
127        Form::Dictionary => "dictionary",
128        Form::Rle => "rle",
129        Form::BitPacked => "bit-packed",
130        Form::StringView => "string-view",
131        Form::Fsst => "fsst",
132        Form::Gathered => "gathered",
133        // `Form` is not exhaustive as far as this crate is concerned, and more encodings are coming
134        // to it. A name rather than a panic means the day one lands is a day the report says
135        // `other` for a while, not a day the report aborts the process.
136        _ => "other",
137    }
138}
139
140/// The position of a form in [`FORMS`], or the slot past the end for one this build does not know.
141fn form_index(form: Form) -> usize {
142    FORMS.iter().position(|&known| known == form).unwrap_or(FORMS.len())
143}
144
145/// One counter per kernel per form pair, plus a row and a column for a form added later.
146const WIDTH: usize = FORMS.len() + 1;
147const CELLS: usize = Kernel::ALL.len() * WIDTH * WIDTH;
148
149#[cfg(not(test))]
150static COUNTS: [AtomicU64; CELLS] = [const { AtomicU64::new(0) }; CELLS];
151
152// One table per thread in a test build, and one table for the process everywhere else.
153//
154// The counts a harness wants are the counts for a run, so the table the library keeps is process
155// wide. The counts a test wants are its own, and the test harness runs tests in parallel in one
156// process, so under `cfg(test)` every thread gets a table of its own and a test sees nothing but
157// what it recorded. A test binary here is a hundred and twenty tests of which fourteen read these
158// counters and the rest call kernels, so with one shared table the fourteen fail whenever one of
159// the other hundred happens to fall through at the same moment. That is what took the 0.2.12
160// release down and it did it by failing on a machine nobody was watching.
161//
162// A lock is the other way to write this and it was the way this was written. It does not work,
163// because it only serializes the tests that take it, and the test that has to take it is every
164// test that calls a kernel rather than the ones that read the counters.
165#[cfg(test)]
166thread_local! {
167    static COUNTS: [AtomicU64; CELLS] = const { [const { AtomicU64::new(0) }; CELLS] };
168}
169
170/// Reads the table this thread counts into.
171#[cfg(not(test))]
172fn with_counts<T>(read: impl FnOnce(&[AtomicU64; CELLS]) -> T) -> T {
173    read(&COUNTS)
174}
175
176/// Reads the table this thread counts into.
177#[cfg(test)]
178fn with_counts<T>(read: impl FnOnce(&[AtomicU64; CELLS]) -> T) -> T {
179    COUNTS.with(read)
180}
181
182/// Where a kernel and a form pair live in the table.
183fn cell(kernel: Kernel, left: Form, right: Form) -> usize {
184    kernel.index() * WIDTH * WIDTH + form_index(left) * WIDTH + form_index(right)
185}
186
187/// Records that a kernel took the row at a time path on this pair of forms.
188///
189/// `Relaxed` because nothing reads this to make a decision while a query is running. It is a
190/// diagnostic that is read once, after, by a harness, and paying for ordering on it would be
191/// paying for a guarantee nobody uses.
192pub fn record(kernel: Kernel, left: Form, right: Form) {
193    with_counts(|counts| counts[cell(kernel, left, right)].fetch_add(1, Ordering::Relaxed));
194    slow::took(kernel.cause());
195}
196
197/// How many times a kernel fell through on this pair of forms.
198#[must_use]
199pub fn count(kernel: Kernel, left: Form, right: Form) -> u64 {
200    with_counts(|counts| counts[cell(kernel, left, right)].load(Ordering::Relaxed))
201}
202
203/// Every combination that has fallen through at least once, most frequent first.
204#[must_use]
205pub fn hot() -> Vec<(Kernel, Form, Form, u64)> {
206    let mut out = Vec::new();
207    for kernel in Kernel::ALL {
208        for left in FORMS {
209            for right in FORMS {
210                let seen = count(kernel, left, right);
211                if seen > 0 {
212                    out.push((kernel, left, right, seen));
213                }
214            }
215        }
216    }
217    out.sort_by_key(|entry| std::cmp::Reverse(entry.3));
218    out
219}
220
221/// Sets every counter back to zero.
222///
223/// For a harness that wants the counts for one query rather than for a process, and for the tests
224/// below. It is not synchronized against a running query, because a diagnostic that took a lock
225/// would be a diagnostic that changed what it measures.
226pub fn reset() {
227    with_counts(|counts| {
228        for counter in counts {
229            counter.store(0, Ordering::Relaxed);
230        }
231    });
232}
233
234/// The counts as a table, or a line saying there are none.
235#[must_use]
236pub fn report() -> String {
237    let hot = hot();
238    if hot.is_empty() {
239        return "every kernel call took a specialized path".to_owned();
240    }
241    let mut out = String::from("kernel calls that fell through to the row at a time path\n");
242    for (kernel, left, right, seen) in hot {
243        out.push_str(&format!(
244            "  {:<10} {:<10} against {:<10} {seen}\n",
245            kernel.name(),
246            form_name(left),
247            form_name(right)
248        ));
249    }
250    out
251}
252
253#[cfg(test)]
254mod tests {
255    use rudb_common::slow;
256
257    use super::{Cause, Form, Kernel, count, hot, record, report, reset};
258
259    #[test]
260    fn a_fall_through_is_counted_by_form_pair_here_and_by_kernel_where_the_document_reads_it() {
261        reset();
262        slow::reset();
263        record(Kernel::Select, Form::Dictionary, Form::Flat);
264        record(Kernel::Select, Form::Constant, Form::Flat);
265        assert_eq!(count(Kernel::Select, Form::Dictionary, Form::Flat), 1);
266        assert_eq!(count(Kernel::Select, Form::Constant, Form::Flat), 1);
267        // The other counter does not split by form, because the question it answers is which
268        // operator is paying rather than which specialization is missing.
269        assert_eq!(slow::here().get(Cause::Select), 2);
270        assert_eq!(slow::here().total(), 2);
271        reset();
272        slow::reset();
273    }
274
275    #[test]
276    fn every_kernel_names_a_cause_of_its_own() {
277        let mut named: Vec<&str> = Kernel::ALL.iter().map(|kernel| kernel.cause().name()).collect();
278        named.sort_unstable();
279        named.dedup();
280        assert_eq!(named.len(), Kernel::ALL.len());
281        for kernel in Kernel::ALL {
282            assert_eq!(kernel.name(), kernel.cause().name(), "one kernel, one name");
283        }
284    }
285
286    #[test]
287    fn a_fall_through_lands_in_the_cell_for_its_own_form_pair() {
288        reset();
289        record(Kernel::Compare, Form::Sequence, Form::Constant);
290        record(Kernel::Compare, Form::Sequence, Form::Constant);
291        record(Kernel::Cast, Form::Dictionary, Form::Flat);
292        assert_eq!(count(Kernel::Compare, Form::Sequence, Form::Constant), 2);
293        assert_eq!(count(Kernel::Cast, Form::Dictionary, Form::Flat), 1);
294        assert_eq!(count(Kernel::Compare, Form::Constant, Form::Sequence), 0);
295        assert_eq!(count(Kernel::Compare, Form::Flat, Form::Flat), 0);
296        reset();
297    }
298
299    #[test]
300    fn the_report_names_the_combination_rather_than_a_number_on_its_own() {
301        reset();
302        assert!(report().contains("every kernel call took a specialized path"));
303        for _ in 0..7 {
304            record(Kernel::Compare, Form::Sequence, Form::Constant);
305        }
306        record(Kernel::Logic, Form::Flat, Form::Dictionary);
307        let text = report();
308        assert!(text.contains("compare"), "{text}");
309        assert!(text.contains("sequence"), "{text}");
310        assert!(text.contains('7'), "{text}");
311        // Most frequent first, because the point of the table is to say what to specialize next.
312        assert_eq!(hot().first().map(|entry| entry.3), Some(7));
313        reset();
314    }
315}