Skip to main content

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}