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}