Skip to main content

rudb_common/
stage.rs

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