rudb_common/slow.rs
1//! How many times this thread took a path written to be correct rather than fast.
2//!
3//! There are two of these paths and they are the same admission. One is [`Cause::Flatten`], where
4//! something was handed a compact column and asked for a plain one because it did not know how to
5//! read the compact form. The others are the kernels, where a loop that has a hand written version
6//! for three pairs of forms met a fourth pair and fell through to reading a value at a time. Both
7//! are correct and both are the reason a query is slower than it should be, so the engine counts
8//! them rather than guessing about them later.
9//!
10//! This lives at rank 0 because of who has to reach it. The flatten is in `rudb-vector` at rank 1,
11//! the kernels are at rank 3, and the thing that has to read the number and say which operator it
12//! belongs to is the instrumentation shim at rank 4. Rank 1 cannot see rank 3 and neither can see
13//! rank 4, so the only place all three can see is the bottom.
14//!
15//! The count is per thread and it is a plain [`Cell`] rather than an atomic. Both of those are the
16//! point rather than an optimisation. The shim reads the count before an operator call and after
17//! it, and the difference is what that call did. With one process wide counter, two threads running
18//! the same pipeline at the same time would each read the other's work into their own difference,
19//! and the per operator attribution this exists to produce would be noise. A thread cannot race
20//! with itself, so the number a thread reads is exactly what that thread did, and it stays exact
21//! when F4 puts eight of them on the same pipeline.
22//!
23//! Nothing here resets itself. The counter runs for the life of the thread and every reader takes a
24//! difference, because a reader that reset would be taking the count away from whoever else was
25//! reading it.
26
27use std::cell::Cell;
28
29/// What made a call take the slow path.
30///
31/// Not exhaustive, because the whole reason this exists is that the list of forms is going to grow.
32/// F1 adds bit packed, run length and FSST columns, and each of them arrives with its own set of
33/// kernels that do not handle it yet.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35#[non_exhaustive]
36pub enum Cause {
37 /// A compact column was copied out into a plain one.
38 Flatten,
39 /// A comparison read a value at a time.
40 Compare,
41 /// A scalar function read a value at a time.
42 Scalar,
43 /// Three valued logic read a value at a time.
44 Logic,
45 /// A conversion read a value at a time.
46 Cast,
47 /// An aggregate read a value at a time.
48 Aggregate,
49 /// Turning a vector of flags into the rows it keeps read a value at a time.
50 Select,
51}
52
53/// How many causes there are, which is how wide a [`Tally`] is.
54const KINDS: usize = 7;
55
56impl Cause {
57 /// Every cause, in the order a tally prints them.
58 pub const ALL: [Self; KINDS] = [
59 Self::Flatten,
60 Self::Compare,
61 Self::Scalar,
62 Self::Logic,
63 Self::Cast,
64 Self::Aggregate,
65 Self::Select,
66 ];
67
68 /// The name in the document and in the report.
69 #[must_use]
70 pub const fn name(self) -> &'static str {
71 match self {
72 Self::Flatten => "flatten",
73 Self::Compare => "compare",
74 Self::Scalar => "scalar",
75 Self::Logic => "logic",
76 Self::Cast => "cast",
77 Self::Aggregate => "aggregate",
78 Self::Select => "select",
79 }
80 }
81
82 /// Where this cause sits in an array with one slot per cause.
83 ///
84 /// Public because a tally is not the only thing that wants one slot per cause. The operator
85 /// counters keep an atomic per cause and index it with this, which beats a map on a path that
86 /// is taken once per chunk.
87 #[must_use]
88 pub const fn slot(self) -> usize {
89 match self {
90 Self::Flatten => 0,
91 Self::Compare => 1,
92 Self::Scalar => 2,
93 Self::Logic => 3,
94 Self::Cast => 4,
95 Self::Aggregate => 5,
96 Self::Select => 6,
97 }
98 }
99}
100
101/// How many slow paths of each kind were taken.
102///
103/// Copy, and small enough that copying it is cheaper than borrowing it. The shim holds one of these
104/// across an operator call and the document holds one per operator.
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106pub struct Tally {
107 counts: [u64; KINDS],
108}
109
110impl Tally {
111 /// No slow path taken at all, which is what every operator is meant to end up reporting.
112 #[must_use]
113 pub const fn none() -> Self {
114 Self { counts: [0; KINDS] }
115 }
116
117 /// How many of this one.
118 #[must_use]
119 pub const fn get(&self, cause: Cause) -> u64 {
120 self.counts[cause.slot()]
121 }
122
123 /// All of them.
124 #[must_use]
125 pub fn total(&self) -> u64 {
126 self.counts.iter().fold(0, |sum, count| sum.saturating_add(*count))
127 }
128
129 /// Whether nothing fell back at all.
130 #[must_use]
131 pub fn is_empty(&self) -> bool {
132 self.total() == 0
133 }
134
135 /// Every cause that happened at least once, in the order [`Cause::ALL`] lists them.
136 ///
137 /// Listed in a fixed order rather than largest first, because this is what the document is
138 /// written from and a document whose keys move around depending on the numbers is a document
139 /// that is hard to diff. Whoever wants the largest asks [`Self::worst`] for it.
140 pub fn taken(&self) -> impl Iterator<Item = (Cause, u64)> + '_ {
141 Cause::ALL.into_iter().map(|cause| (cause, self.get(cause))).filter(|(_, seen)| *seen > 0)
142 }
143
144 /// The cause with the most against it, or none if nothing fell back.
145 ///
146 /// Ties go to whichever comes first in [`Cause::ALL`], which makes the answer stable across
147 /// runs. A tie between two causes is not a case anybody is deciding anything from anyway.
148 #[must_use]
149 pub fn worst(&self) -> Option<(Cause, u64)> {
150 self.taken().max_by_key(|(cause, seen)| (*seen, std::cmp::Reverse(cause.slot())))
151 }
152
153 /// What happened between `before` and this reading.
154 ///
155 /// Saturating, so a reader that somehow gets the two the wrong way round reports nothing rather
156 /// than reporting a number near `u64::MAX`.
157 #[must_use]
158 pub fn since(&self, before: Self) -> Self {
159 let mut counts = [0; KINDS];
160 for (slot, (now, then)) in
161 counts.iter_mut().zip(self.counts.iter().zip(before.counts.iter()))
162 {
163 *slot = now.saturating_sub(*then);
164 }
165 Self { counts }
166 }
167
168 /// Adds another tally into this one.
169 pub fn add(&mut self, other: Self) {
170 for (slot, more) in self.counts.iter_mut().zip(other.counts.iter()) {
171 *slot = slot.saturating_add(*more);
172 }
173 }
174
175 /// A tally with one count in it, for a caller that has a number rather than a running total.
176 #[must_use]
177 pub fn of(cause: Cause, times: u64) -> Self {
178 let mut tally = Self::none();
179 tally.counts[cause.slot()] = times;
180 tally
181 }
182}
183
184thread_local! {
185 /// What this thread has fallen back to so far.
186 static TAKEN: Cell<Tally> = const { Cell::new(Tally::none()) };
187}
188
189/// Records that this thread took a slow path.
190pub fn took(cause: Cause) {
191 took_many(cause, 1);
192}
193
194/// Records that this thread took a slow path more than once.
195///
196/// For a caller that does the falling back in a loop it wrote itself and would rather add at the
197/// end than on every turn.
198pub fn took_many(cause: Cause, times: u64) {
199 TAKEN.with(|taken| {
200 let mut tally = taken.get();
201 tally.add(Tally::of(cause, times));
202 taken.set(tally);
203 });
204}
205
206/// What this thread has fallen back to so far, for taking a difference against later.
207#[must_use]
208pub fn here() -> Tally {
209 TAKEN.with(Cell::get)
210}
211
212/// Sets this thread's count back to nothing.
213///
214/// For tests, and for a harness that runs one query per thread and would rather read a total than
215/// take a difference. Everything inside the engine takes a difference.
216pub fn reset() {
217 TAKEN.with(|taken| taken.set(Tally::none()));
218}
219
220#[cfg(test)]
221mod tests {
222 use super::{Cause, Tally, here, reset, took, took_many};
223
224 #[test]
225 fn a_fall_back_lands_against_its_own_cause_and_leaves_the_rest_alone() {
226 reset();
227 took(Cause::Flatten);
228 took(Cause::Flatten);
229 took(Cause::Compare);
230 let tally = here();
231 assert_eq!(tally.get(Cause::Flatten), 2);
232 assert_eq!(tally.get(Cause::Compare), 1);
233 assert_eq!(tally.get(Cause::Cast), 0);
234 assert_eq!(tally.total(), 3);
235 reset();
236 }
237
238 #[test]
239 fn a_difference_is_what_happened_between_the_two_readings_and_nothing_before_them() {
240 reset();
241 took_many(Cause::Cast, 5);
242 let before = here();
243 took(Cause::Select);
244 took(Cause::Select);
245 let during = here().since(before);
246 assert_eq!(during.get(Cause::Select), 2);
247 assert_eq!(during.get(Cause::Cast), 0, "what happened before the reading is not in it");
248 assert_eq!(during.total(), 2);
249 reset();
250 }
251
252 #[test]
253 fn a_difference_taken_backwards_reports_nothing_rather_than_an_enormous_number() {
254 let later = Tally::of(Cause::Logic, 3);
255 assert!(Tally::none().since(later).is_empty());
256 }
257
258 #[test]
259 fn the_worst_cause_is_the_one_to_go_and_write_a_specialisation_for() {
260 let mut tally = Tally::of(Cause::Flatten, 2);
261 tally.add(Tally::of(Cause::Scalar, 90));
262 tally.add(Tally::of(Cause::Logic, 11));
263 assert_eq!(tally.worst(), Some((Cause::Scalar, 90)));
264 assert_eq!(tally.taken().count(), 3);
265 assert_eq!(Tally::none().worst(), None);
266 }
267
268 #[test]
269 fn the_causes_are_listed_in_one_order_however_big_the_numbers_are() {
270 let mut tally = Tally::of(Cause::Select, 1);
271 tally.add(Tally::of(Cause::Flatten, 1000));
272 let listed: Vec<&str> = tally.taken().map(|(cause, _)| cause.name()).collect();
273 assert_eq!(listed, ["flatten", "select"]);
274 }
275
276 #[test]
277 fn one_thread_counting_is_invisible_to_another() {
278 reset();
279 took_many(Cause::Aggregate, 4);
280 let elsewhere = std::thread::spawn(|| {
281 took(Cause::Aggregate);
282 here()
283 })
284 .join()
285 .expect("no counting thread panics");
286 assert_eq!(elsewhere.get(Cause::Aggregate), 1, "the other thread starts from nothing");
287 assert_eq!(here().get(Cause::Aggregate), 4, "and does not add to this one");
288 reset();
289 }
290
291 #[test]
292 fn every_cause_has_its_own_slot_and_its_own_name() {
293 let mut seen: Vec<&str> = Cause::ALL.iter().map(|cause| cause.name()).collect();
294 seen.sort_unstable();
295 seen.dedup();
296 assert_eq!(seen.len(), Cause::ALL.len());
297 for cause in Cause::ALL {
298 assert_eq!(Tally::of(cause, 7).total(), 7);
299 assert_eq!(Tally::of(cause, 7).get(cause), 7);
300 }
301 }
302}