Skip to main content

rudb_common/
stage.rs

1//! Where an operator's time went, split by the phase inside it that spent it.
2//!
3//! A ClickBench run says the file scan is more than half of everything the engine charges, and one
4//! number for a scan is not a number anybody can act on. A scan reads bytes off a file, hands them
5//! to a codec, decodes a page into values, builds a dictionary and copies pieces of pages into the
6//! chunk an operator sees. Those are five different pieces of code with five different fixes, and
7//! working on any of them without knowing which one holds the time is guessing.
8//!
9//! A grouped aggregate is the same story. It folds rows into a hash table, splits that table across
10//! radix partitions, merges one instance's table into another and turns the finished tables into
11//! rows, and on ClickBench at a million rows the last two are a third of the query and neither of
12//! them showed up anywhere. The threads that do them are started by the aggregate rather than taken
13//! from the pool, so their CPU reaches neither the pipeline counters nor the worker total, and the
14//! only trace they left was wall time nobody could account for.
15//!
16//! This is [`crate::slow`] with a clock instead of a count, and it is here for the same reason that
17//! one is here. The stages happen in `rudb-parquet` at rank 5, the thing that has to say which
18//! operator they belong to is the instrumentation shim in `rudb-pipeline` at rank 4, and neither can
19//! see the other. The bottom is where both can reach.
20//!
21//! Per thread and a plain [`Cell`], again for the reason that one is. The shim takes a reading
22//! before an operator call and after it and the difference is what that call did, which is only true
23//! if no other thread is counting into the same place. F4 puts several threads on one scan and this
24//! keeps meaning the same thing on the day it does.
25//!
26//! The clock runs once per page and once per chunk, never once per value. A page is thousands of
27//! values, so a pair of clock readings around it is not measurable next to what it measures. A pair
28//! of readings per value would be the measurement rather than the thing measured.
29
30use std::cell::Cell;
31use std::time::Instant;
32
33/// A named phase inside one operator, small enough that knowing it holds the time says what to fix.
34///
35/// The first five are the stages of reading a column, in the order the bytes go through them. The
36/// rest are the phases of a grouped aggregate, which needs the same split for the same reason: one
37/// number for an aggregate says it is slow and nothing about which of folding rows, splitting a
38/// table by radix bits, merging one instance's table into another or turning a finished table into
39/// rows is the part that is slow.
40///
41/// Not exhaustive because an operator this does not measure yet has phases this list does not name,
42/// and one added later should be able to say where its time went without every match on this
43/// breaking.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum Stage {
47    /// Getting the bytes off the file, which is the part an operating system does.
48    Read,
49    /// Turning the compressed body of a page into its bytes.
50    Decompress,
51    /// Turning the bytes of a page into values, levels included.
52    Decode,
53    /// Building the dictionary a chunk's pages refer to.
54    Dictionary,
55    /// Cutting pages to the chunk boundary and putting the columns side by side.
56    Assemble,
57    /// Folding a chunk of rows into a hash table, which is the probe and the accumulator update.
58    Fold,
59    /// Splitting a table across the radix partitions, or folding rows straight into them.
60    Scatter,
61    /// Folding one instance's table into another, one probe per group rather than per row.
62    Merge,
63    /// Turning a finished table into the chunks it answers for.
64    Emit,
65}
66
67/// How many stages there are, which is how wide a [`Spent`] is.
68const STAGES: usize = 9;
69
70impl Stage {
71    /// Every stage, in the order the work goes through them.
72    pub const ALL: [Self; STAGES] = [
73        Self::Read,
74        Self::Decompress,
75        Self::Decode,
76        Self::Dictionary,
77        Self::Assemble,
78        Self::Fold,
79        Self::Scatter,
80        Self::Merge,
81        Self::Emit,
82    ];
83
84    /// The name in the document and in the report.
85    #[must_use]
86    pub const fn name(self) -> &'static str {
87        match self {
88            Self::Read => "read",
89            Self::Decompress => "decompress",
90            Self::Decode => "decode",
91            Self::Dictionary => "dictionary",
92            Self::Assemble => "assemble",
93            Self::Fold => "fold",
94            Self::Scatter => "scatter",
95            Self::Merge => "merge",
96            Self::Emit => "emit",
97        }
98    }
99
100    /// Where this stage sits in an array with one slot per stage.
101    #[must_use]
102    pub const fn slot(self) -> usize {
103        match self {
104            Self::Read => 0,
105            Self::Decompress => 1,
106            Self::Decode => 2,
107            Self::Dictionary => 3,
108            Self::Assemble => 4,
109            Self::Fold => 5,
110            Self::Scatter => 6,
111            Self::Merge => 7,
112            Self::Emit => 8,
113        }
114    }
115}
116
117/// How long each stage took and how many bytes went through it.
118///
119/// The bytes are here rather than worked out later because a rate is the number that says whether a
120/// stage is slow. Two hundred milliseconds of decompression is a fact about a query and two hundred
121/// megabytes a second is a fact about the decompressor, and only the second one can be compared
122/// against anything.
123#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
124pub struct Spent {
125    nanos: [u64; STAGES],
126    bytes: [u64; STAGES],
127}
128
129impl Spent {
130    /// Nothing measured, which is what every operator that is not a scan reports.
131    #[must_use]
132    pub const fn none() -> Self {
133        Self { nanos: [0; STAGES], bytes: [0; STAGES] }
134    }
135
136    /// How long this stage took.
137    #[must_use]
138    pub const fn nanos(&self, stage: Stage) -> u64 {
139        self.nanos[stage.slot()]
140    }
141
142    /// How many bytes went through it.
143    #[must_use]
144    pub const fn bytes(&self, stage: Stage) -> u64 {
145        self.bytes[stage.slot()]
146    }
147
148    /// Every stage, added up.
149    #[must_use]
150    pub fn total(&self) -> u64 {
151        self.nanos.iter().fold(0, |sum, nanos| sum.saturating_add(*nanos))
152    }
153
154    /// Whether no stage recorded anything.
155    #[must_use]
156    pub fn is_empty(&self) -> bool {
157        self.nanos.iter().all(|nanos| *nanos == 0) && self.bytes.iter().all(|bytes| *bytes == 0)
158    }
159
160    /// Every stage that did something, in the order [`Stage::ALL`] lists them.
161    ///
162    /// The order is the order the bytes go through the stages rather than largest first, because
163    /// this is what the document is written from and a document whose keys move with its numbers is
164    /// one nobody can diff. Whoever wants the largest asks [`Self::worst`].
165    pub fn taken(&self) -> impl Iterator<Item = (Stage, u64, u64)> + '_ {
166        Stage::ALL
167            .into_iter()
168            .map(|stage| (stage, self.nanos(stage), self.bytes(stage)))
169            .filter(|(_, nanos, bytes)| *nanos > 0 || *bytes > 0)
170    }
171
172    /// The stage holding the most time, or none if nothing was measured.
173    #[must_use]
174    pub fn worst(&self) -> Option<(Stage, u64)> {
175        self.taken()
176            .map(|(stage, nanos, _)| (stage, nanos))
177            .filter(|(_, nanos)| *nanos > 0)
178            .max_by_key(|(stage, nanos)| (*nanos, std::cmp::Reverse(stage.slot())))
179    }
180
181    /// What happened between `before` and this reading.
182    ///
183    /// Saturating, so a reader that takes the two the wrong way round reports nothing rather than
184    /// most of a century.
185    #[must_use]
186    pub fn since(&self, before: Self) -> Self {
187        let mut out = Self::none();
188        for slot in 0..STAGES {
189            out.nanos[slot] = self.nanos[slot].saturating_sub(before.nanos[slot]);
190            out.bytes[slot] = self.bytes[slot].saturating_sub(before.bytes[slot]);
191        }
192        out
193    }
194
195    /// Adds another reading into this one.
196    pub fn add(&mut self, other: Self) {
197        for slot in 0..STAGES {
198            self.nanos[slot] = self.nanos[slot].saturating_add(other.nanos[slot]);
199            self.bytes[slot] = self.bytes[slot].saturating_add(other.bytes[slot]);
200        }
201    }
202
203    /// One stage's worth, for a caller that has a number rather than a running total.
204    #[must_use]
205    pub fn of(stage: Stage, nanos: u64, bytes: u64) -> Self {
206        let mut spent = Self::none();
207        spent.nanos[stage.slot()] = nanos;
208        spent.bytes[stage.slot()] = bytes;
209        spent
210    }
211}
212
213thread_local! {
214    /// What this thread has spent in each stage so far.
215    static SPENT: Cell<Spent> = const { Cell::new(Spent::none()) };
216}
217
218/// Records time and bytes against a stage on this thread.
219pub fn took(stage: Stage, nanos: u64, bytes: u64) {
220    SPENT.with(|spent| {
221        let mut now = spent.get();
222        now.add(Spent::of(stage, nanos, bytes));
223        spent.set(now);
224    });
225}
226
227/// Adds what another thread spent to this thread's total.
228///
229/// For work an operator hands to threads of its own rather than to the pool. The instrumentation
230/// shim takes its reading on the thread that called the operator, so a thread the operator started
231/// is invisible to it, and the aggregate's finalize is exactly that: it closes sixteen partitions
232/// on threads it scopes itself and then joins them. Each of those threads reads its own total when
233/// it finishes and the one that started them adds the readings here, so the phases come out against
234/// the operator that did them and nothing is lost.
235///
236/// The time is a sum over threads and not an elapsed time, the same as every other stage number,
237/// because that is the one that compares against the CPU an operator charged.
238pub fn gained(spent: Spent) {
239    SPENT.with(|slot| {
240        let mut now = slot.get();
241        now.add(spent);
242        slot.set(now);
243    });
244}
245
246/// What this thread has spent so far, for taking a difference against later.
247#[must_use]
248pub fn here() -> Spent {
249    SPENT.with(Cell::get)
250}
251
252/// Sets this thread's reading back to nothing.
253///
254/// For tests, and for a harness that runs one query per thread. Everything inside the engine takes
255/// a difference instead.
256pub fn reset() {
257    SPENT.with(|spent| spent.set(Spent::none()));
258}
259
260/// A clock started at one stage, charging what it measured when it stops.
261///
262/// The pair of calls is a type rather than two lines because the second line is the one that gets
263/// forgotten, and a stage that starts a clock and never stops it is a stage that reads as free.
264#[derive(Debug)]
265pub struct Timing {
266    stage: Stage,
267    at: Instant,
268}
269
270impl Timing {
271    /// Starts the clock for a stage.
272    #[must_use]
273    pub fn start(stage: Stage) -> Self {
274        Self { stage, at: Instant::now() }
275    }
276
277    /// Stops it and charges the time, along with the bytes that went through.
278    ///
279    /// A caller with no meaningful byte count passes zero, which keeps the stage out of the rate
280    /// column rather than putting a nought in it.
281    pub fn stop(self, bytes: u64) {
282        let nanos = u64::try_from(self.at.elapsed().as_nanos()).unwrap_or(u64::MAX);
283        took(self.stage, nanos, bytes);
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::{Spent, Stage, here, reset, took};
290
291    #[test]
292    fn time_lands_against_its_own_stage_and_leaves_the_rest_alone() {
293        reset();
294        took(Stage::Read, 100, 4096);
295        took(Stage::Read, 50, 1024);
296        took(Stage::Decompress, 700, 8192);
297        let spent = here();
298        assert_eq!(spent.nanos(Stage::Read), 150);
299        assert_eq!(spent.bytes(Stage::Read), 5120);
300        assert_eq!(spent.nanos(Stage::Decompress), 700);
301        assert_eq!(spent.nanos(Stage::Decode), 0);
302        assert_eq!(spent.total(), 850);
303        reset();
304    }
305
306    #[test]
307    fn a_difference_is_what_happened_between_the_two_readings_and_nothing_before_them() {
308        reset();
309        took(Stage::Decode, 900, 16);
310        let before = here();
311        took(Stage::Assemble, 12, 0);
312        let during = here().since(before);
313        assert_eq!(during.nanos(Stage::Assemble), 12);
314        assert_eq!(during.nanos(Stage::Decode), 0, "what happened before the reading is not in it");
315        assert_eq!(during.total(), 12);
316        reset();
317    }
318
319    #[test]
320    fn a_difference_taken_backwards_reports_nothing_rather_than_most_of_a_century() {
321        let later = Spent::of(Stage::Read, 900, 900);
322        assert!(Spent::none().since(later).is_empty());
323    }
324
325    #[test]
326    fn the_worst_stage_is_the_one_worth_working_on() {
327        let mut spent = Spent::of(Stage::Read, 40, 0);
328        spent.add(Spent::of(Stage::Decompress, 4000, 0));
329        spent.add(Spent::of(Stage::Decode, 900, 0));
330        assert_eq!(spent.worst(), Some((Stage::Decompress, 4000)));
331        assert_eq!(spent.taken().count(), 3);
332        assert_eq!(Spent::none().worst(), None);
333    }
334
335    #[test]
336    fn a_stage_that_only_moved_bytes_is_listed_and_is_not_the_worst() {
337        let mut spent = Spent::of(Stage::Read, 0, 8192);
338        spent.add(Spent::of(Stage::Decode, 5, 0));
339        let listed: Vec<&str> = spent.taken().map(|(stage, _, _)| stage.name()).collect();
340        assert_eq!(listed, ["read", "decode"]);
341        assert_eq!(spent.worst(), Some((Stage::Decode, 5)));
342    }
343
344    #[test]
345    fn one_thread_timing_is_invisible_to_another() {
346        reset();
347        took(Stage::Dictionary, 44, 0);
348        let elsewhere = std::thread::spawn(|| {
349            took(Stage::Dictionary, 1, 0);
350            here()
351        })
352        .join()
353        .expect("no timing thread panics");
354        assert_eq!(elsewhere.nanos(Stage::Dictionary), 1, "the other thread starts from nothing");
355        assert_eq!(here().nanos(Stage::Dictionary), 44, "and does not add to this one");
356        reset();
357    }
358
359    #[test]
360    fn every_stage_has_its_own_slot_and_its_own_name() {
361        let mut seen: Vec<&str> = Stage::ALL.iter().map(|stage| stage.name()).collect();
362        seen.sort_unstable();
363        seen.dedup();
364        assert_eq!(seen.len(), Stage::ALL.len());
365        for stage in Stage::ALL {
366            assert_eq!(Spent::of(stage, 7, 3).total(), 7);
367            assert_eq!(Spent::of(stage, 7, 3).bytes(stage), 3);
368        }
369    }
370}