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