1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
//! Per-SCAN read-phase wall-time accumulator (issue #1707, AI7 of epic #1686).
//!
//! # Why this exists
//!
//! [`catalog::READ_DURATION`](super::catalog::READ_DURATION) says a read was slow.
//! Nothing said WHERE the time went, so "why was this query slow?" could not be
//! answered from metrics at all — it needed a profiler on the box. This module
//! accumulates ONE scan's wall time into four buckets — io / decompress / decode /
//! merge — at the read path's EXISTING function seams, and the owning
//! `ReadOpMeter` (`super::read_metrics`) emits them as exactly ONE
//! sample per phase when the scan completes.
//!
//! # Why it is a sibling of [`super::stream_subphase`] and not an extension of it
//!
//! `stream_subphase` looks almost identical, and reusing it was considered and
//! rejected for two structural reasons:
//!
//! * its six variants are PINNED by a `cqlite-flight` test asserting the set is
//! exactly six, because they map 1:1 onto the documented bounded cardinality of
//! [`catalog::RPC_PHASE_DURATION`](super::catalog::RPC_PHASE_DURATION)'s `phase`
//! attribute — adding a variant silently widens a metric dimension; and
//! * its sink is installed ONLY by the Flight `do_get` path, so a core scan (CLI,
//! embedded, query engine) installs nothing and every sample would be dropped.
//!
//! The two accumulators therefore coexist and can both be installed on one thread:
//! a Flight read attributes the same work to `cqlite.rpc.phase.duration` and to
//! `cqlite.read.phase.*` independently, which is correct — they are two accountings
//! of one pipeline, not two halves of one accounting.
//!
//! # Ownership, and why emission cannot double-count
//!
//! The `Arc<ReadPhaseTimings>` is created by `ReadOpMeter::start` and owned by that
//! meter, so it inherits the meter's whole lifecycle for free: `finish()` is
//! idempotent, `Drop` calls it, and `ReadOpMeter::inert()` (every sub-scan of a
//! fan-out merge, and the per-row → batch re-chunker) has no accumulator at all.
//! That is what makes "ONE sample per phase per completed scan" a property of the
//! design rather than of a convention every call site must remember.
//!
//! # Thread propagation is EXPLICIT
//!
//! Thread-locals are not inherited across a spawn, and the phases physically happen
//! on threads that never see the meter: a `spawn_blocking` IO feed thread, a
//! `spawn_blocking` parse thread, and a merge producer thread. So the `Arc` is
//! passed to each of those closures at its SPAWN SITE and re-[`install`]ed there —
//! the same shape `stream_subphase` uses, and deliberately not a "walk up some
//! ambient context" scheme, which cannot work across a thread boundary.
//!
//! # Coverage boundary — which read surfaces record phases, and which do NOT
//!
//! A sink only reaches the code that does the work if it is INSTALLED on the thread
//! doing it, and installation happens at SPAWN SITES (see above). So coverage is
//! exactly the set of surfaces whose work runs on a thread this crate spawns for
//! them:
//!
//! **Measured** — the windowed scan driver, reached by both streaming surfaces
//! (`scan_stream` per-row and `scan_stream_batched`) for a chunk-stitching reader
//! (io + decompress + decode); and `generation_merge::stream_generations_for_read`,
//! the streaming cross-generation reconciling merge (merge, plus the DECOMPRESS its
//! per-input producer threads perform).
//!
//! **PARTIALLY measured, and the omission is named rather than implied**: that
//! cross-generation merge route records NO `io` sample. The sink IS propagated into
//! both producer-thread spawn sites (`merge::from_readers`, `merge::producer_iter`),
//! so the work those threads do through the SHARED chunk-decode plane
//! (`reader::chunk_source`) is attributed — that is where `decompress` comes from.
//! But the `io` seam itself exists ONLY in the windowed scan's read helpers
//! (`scan_stream_windowed_read`), and a merge producer reads through
//! `stream_all_partitions_for_compaction` / `_for_query` instead, which has no io
//! seam at any depth. So io on this route is unmeasured for want of a SEAM, not for
//! want of propagation, and closing it means instrumenting a second read route —
//! deliberately not smuggled in here. An earlier version of this paragraph claimed
//! the route recorded "the io/decompress its producer thread performs", which was
//! false in its io half and, under the rule stated below, would have taught an
//! operator to read an absent `io` as "io was free" on exactly the path where io is
//! most likely the problem.
//!
//! **NOT measured — these emit `read.duration` with NO `read.phase.*` series at
//! all**: the materializing `SSTableManager::scan` / `scan_with_meter` and the
//! materializing `merge_generations_for_read` beneath it; the BIG reverse-clustering
//! scan (`reverse_scan.rs`); the BTI trie walk (`stream_bti_scan`); the
//! non-chunk-stitching block-by-block branch; point reads (`get`, the manager point
//! read); and compaction reads.
//!
//! **An absent phase series from one of those surfaces means NOT MEASURED — never
//! "fast".** The rule for distinguishing the two cases is the surface, not the
//! metric: a measured surface's absent phase is a real absence (an uncompressed
//! SSTable decompresses nothing), while these surfaces are silent about every phase
//! at once. If you see `read.duration` rising with no phase breakdown, you are
//! looking at one of them.
//!
//! # What ABSENCE and `0.0` each mean, and why they are tracked separately
//!
//! Within a MEASURED surface, absence of a phase series means the phase DID NOT RUN
//! — that is the whole content of "no `decompress` means uncompressed", "no `merge`
//! means a single generation". A `0.0` sample means something else: the phase RAN
//! and measured zero.
//!
//! Those two are only distinguishable because [`ReadPhaseTimings`] tracks phase
//! ENTRY separately from accumulated duration. Deriving absence from `nanos == 0`
//! — which emission used to do — collapses them, and the collapse is not academic:
//! [`timed_merge_excluding_recv_wait`] SATURATES to `0` whenever the recv-wait it
//! subtracts exceeds the step's wall time, so a real multi-generation merge whose
//! producers starved recorded `0`, was skipped, and told the operator "single
//! generation" (issue #1707). The mechanism that exists to keep merge honest was
//! manufacturing a false statement.
//!
//! ## Why they are not simply instrumented too (issue #1707)
//!
//! Their phase work sits BELOW `.await` points, on the async worker threads, reached
//! through the shared seams that read this thread-local (`chunk_source`,
//! `block_io`). Two consequences:
//!
//! * installing a sink around such a call would hold the guard ACROSS an `.await`,
//! and a parked task's worker thread runs OTHER tasks — so another scan's decode
//! would be attributed to this one's counters. Cross-attribution is worse than no
//! data, because it is indistinguishable from data.
//! * installing it only around the SYNCHRONOUS prologue instead would be worse
//! still: it would produce phase samples that systematically UNDERSTATE the read
//! (an `io` of microseconds for a read that spent tens of milliseconds in io),
//! which an operator cannot tell from a genuinely fast read. Absence they can at
//! least look up; a plausible wrong number they cannot.
//!
//! Covering them needs async-safe propagation (a task-local carried across awaits,
//! or the sink threaded explicitly through `SSTableReader::scan` and the reverse
//! walk) — a read-path design change, deliberately not smuggled in here.
//!
//! # Why the decode timer is scoped to the parse call (issue #1707)
//!
//! The `Decode` seam in `scan_stream_windowed` wraps
//! `parse_one_partition_with_timestamps` and NOTHING ELSE, by a block expression.
//! That tightness is load-bearing rather than tidiness: bound at loop-iteration
//! scope — which it was — the timer also covered `window.consume`, the
//! `scratch.drain`/`batch.push` re-chunking, the batch `Vec` allocation, and
//! decisively `tx.blocking_send`, which PARKS the parse thread whenever the consumer
//! is slow.
//!
//! A client that pages slowly would then make `read.phase.decode` dominated by
//! waiting for the CONSUMER. The operator follows the runbook — "decode dominant →
//! wide partitions, many collection/UDT cells" — investigates the schema and finds
//! nothing, because the schema was never the problem. It would also contradict the
//! catalogued definition of the phase ("decode out of already-resident decompressed
//! bytes") and invert the care taken for [`ReadPhase::Merge`], which deliberately
//! SUBTRACTS its recv-wait for exactly this reason. Any future phase seam gets the
//! same treatment: a timer's scope must contain only work the phase names, never a
//! blocking handoff to someone else.
//!
//! # Zero cost when off
//!
//! `ReadOpMeter::start` consults [`obs::metrics_active`](super::metrics_active)
//! ONCE and builds NO accumulator when metrics are not being collected, so no sink
//! is installed, [`current`] returns `None`, and [`timed`] runs the closure with a
//! single thread-local peek — no `Instant::now()`, no atomic write. With the
//! `observability` feature off, `metrics_active()` is a compile-time `false`, so
//! the whole thing degenerates to that one branch.
use ;
use ;
use Arc;
use Instant;
/// One of the four coarse read phases (issue #1707).
///
/// Deliberately COARSE and deliberately four: each is measured at a function seam
/// the read path already has, and none is per-row or per-cell. The row/cell decoder
/// is the hottest loop in the read path and is never instrumented — `Decode` is
/// accumulated once per PARTITION, at the parse boundary above it.
/// Per-scan accumulator of read-phase wall time, in nanoseconds.
///
/// Four `AtomicU64`s so the concurrent pipeline threads (IO feed, blocking parse,
/// merge producer) all `fetch_update` into the same shared instance lock-free. The
/// phases OVERLAP in wall-clock — the pipeline is concurrent — so they are NOT
/// expected to sum to the scan's `read.duration`; the load-bearing signal is which
/// phase dominates and how that moves between runs.
thread_local!
/// RAII guard restoring the previous sink on drop — panic-safe, so a reused
/// blocking-pool thread never leaks one scan's sink into the next.
/// Install `sink` as this thread's read-phase sink for the lifetime of the returned
/// guard, restoring the previous value on drop. Passing `None` installs "no sink"
/// (what a spawn site propagates when its parent had none) — still restoring the
/// prior value on drop, so nesting is sound.
/// Whether a read-phase sink is installed on this thread — a cheap `Cell<bool>`
/// load (no `RefCell` borrow, no `Arc` clone), for a hot caller that wants to skip
/// `Instant::now()` entirely when unmetered.
/// This thread's installed sink, if any. A spawn site calls this on the PARENT
/// thread and re-[`install`]s the captured value on the CHILD thread, so the child's
/// io/decompress/decode/merge reach the scan's accumulator.
/// Clamped nanoseconds elapsed since `start` — the ONE place the `Instant`→`u64`
/// clamp lives, reused by every timing site here. A scan long enough to overflow
/// `u64` nanoseconds (~584 years) is unreachable.
/// Time `f` and, IF a sink is installed on this thread, attribute its elapsed wall
/// time to `phase`. When no sink is installed this is a single thread-local peek
/// plus the bare closure — no `Instant::now()`, no atomic write.
/// Time `f` (a k-way MERGE step) and attribute its elapsed wall time to
/// [`ReadPhase::Merge`] MINUS the merge-input recv-wait accrued inside it.
///
/// Raw wall time around a merge step is mostly BLOCKING RECV on the merge inputs —
/// producer starvation, i.e. io happening on another thread — so charging it to
/// `merge` would make every disk-bound read look merge-bound. The recv sites already
/// accumulate that wait per thread for the #2819 Flight sub-phases
/// ([`super::stream_subphase::pull_wait_nanos`]); this reads the SAME accumulator's
/// delta and subtracts it, rather than duplicating a second thread-local and a
/// second call site at every recv — two accumulators of one quantity is exactly the
/// "two statements of one fact can disagree" shape.
///
/// Saturating: if a nested/foreign recv were somehow attributed a longer wait than
/// this step's wall time, the phase gets 0 rather than a wrapped enormous value.
/// That 0 is still an OBSERVATION and is emitted as a `0.0` sample, not swallowed:
/// entry is recorded by the `add_nanos` call below independently of the value, so a
/// merge that ran cannot be reported as a scan with no merge at all (#1707).
///
/// Carries the EXACT cfg of its only call site, `generation_merge::
/// stream_generations_for_read`, which needs BOTH conditions to exist:
/// `write-support` gates the whole `generation_merge` module at its `mod`
/// declaration in `storage/sstable/mod.rs`, and `not(tombstones)` gates the
/// streaming function inside it. Either one off and there is no merge step to time,
/// so an ungated definition is provably dead code — which is exactly what the
/// minimal-features build (`--no-default-features --features all-compression`, i.e.
/// write-support OFF) turns into a `-D dead-code` hard error. Keep this cfg in sync
/// with the call site rather than silencing the lint: if the last real caller ever
/// disappears, the build SHOULD say so.
/// RAII timer recording elapsed wall time into `phase` on drop — the tight-scope
/// counterpart of [`timed`] for a region a sync closure cannot wrap (an `.await`ed
/// read, or a region with early returns).
///
/// It CAPTURES the sink `Arc` at construction, never re-resolving the thread-local
/// at drop time, so it stays correct even if it is held across an `.await` that
/// resumes the future on a DIFFERENT executor thread.
/// A [`ReadPhaseTimer`] for `phase`, or `None` (and zero `Instant::now`) when no
/// sink is installed.
///
/// This module deliberately exposes a SMALLER entry surface than its
/// [`super::stream_subphase`] twin: the twin's `record_nanos` and `scoped_captured`
/// have no counterpart here because nothing in this crate calls them, and the module
/// is `pub(crate)` so nothing outside can either (issue #1707). Dead code kept alive
/// "for symmetry" is still dead code; the twin's versions remain where they have real
/// callers (`data_access`). Add them back the day a seam needs them.
/// TEST-ONLY artificial delay inside the io phase (issue #1707).
///
/// # Why an injected delay is the only honest way to pin the io phase
///
/// The property under test is ATTRIBUTION: "time spent reading `Data.db` is charged
/// to `read.phase.io`". On a warm page cache over a small committed fixture the real
/// io time is microseconds, so any assertion about its share would be a wall-clock
/// race (#2642) — the test would be measuring the host, not the code. Injecting a
/// known, dominant delay AT THE READ makes the assertion STRUCTURAL instead: with
/// milliseconds of deliberate delay per read, io must dominate unless the seam is
/// mis-wired, and no timing luck can change that verdict.
///
/// Compiled out entirely unless `observability-testing` (the feature that already
/// gates the in-memory metric capture these tests need) or `cfg(test)` is on: a
/// production build has no arming surface, no atomic, and no branch. Same shape as
/// `storage::producer_fault` — a test-only seam that is a compile-time no-op.
/// Production no-op twin of [`io_delay::sleep_if_armed`] — no atomic, no branch.
pub
/// Unit tests live in a sibling file so this module stays inside the campsite-rule
/// source target (#1116); they are logically the `tests` submodule.