macrame-db 0.13.0

A Bitemporal Graph Ledger on libSQL · Embedded knowledge database
Documentation
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
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
//! What the write actor knows about its own latency (T1.4, D-079).
//!
//! # Why this exists
//!
//! [`crate::CHUNK_BUDGET`] is 3 ms and the crate has, until now, had exactly one
//! way to find out whether that bound holds: run `benches/budgets.rs` on a
//! synthetic fixture. That is a statement about a laptop, not about a database
//! in use. D-059 already established that the bound does **not** hold on a large
//! file, by a factor of 15, and it took a benchmark rewrite to notice — because
//! nothing in the running system was counting.
//!
//! Tier 1's other three items are all "make the tail bounded". None of them can
//! be validated in the field without something that measures the tail, which is
//! why this is a precondition for them rather than a nice-to-have.
//!
//! # What is recorded, and what is deliberately not
//!
//! Four things, all of them per **actor turn** — one command, start to finish:
//!
//! - **queue depth** on both channels, sampled *before* the turn begins;
//! - **hold duration**, bucketed, per command kind;
//! - **holds over budget**, counted separately per kind;
//! - **the longest hold since open**, with the kind that caused it.
//!
//! The hold is the whole turn, not the `execute` call's SQL. That is the
//! quantity the budget is about: the SQLite write lock is not preemptible, so an
//! interactive assertion arriving mid-turn waits for the turn, whatever the turn
//! spent its time on.
//!
//! There is no per-command timestamp trail and no sampling of individual slow
//! commands. That would be a tracing problem, and `tracing` is already a
//! dependency — spans belong there. This module answers one question ("is the
//! bound holding, and if not, which kind breaks it") in fixed memory, with no
//! allocation on the actor's path.
//!
//! # The feature gate
//!
//! Behind `metrics`, which has been a **default** feature since 0.12.11
//! (D-154): a crate whose contract is a latency bound must not ship a default
//! build that cannot report whether the bound is met. `--no-default-features`
//! still removes it. With the feature off, [`ActorMetrics`] is a
//! zero-sized type whose methods compile away and [`HoldTimer::start`] does not
//! read the clock — so the actor loop has **one** shape either way. That
//! matters more than the nanoseconds: a `#[cfg]` in the loop body is how the
//! instrumented and uninstrumented paths drift until only one of them is the one
//! that runs.

use std::time::Duration;

/// The command kinds the actor can spend a turn on.
///
/// One flat enum across both channels rather than one per channel. The question
/// this exists to answer is "which command broke the budget", and a reader
/// looking at a 400 ms hold does not first want to know which queue it came off.
/// Priority is a property of scheduling; kind is a property of cost.
///
/// # `#[non_exhaustive]`, added while it was still free (0.12.8, W4.2)
///
/// Adding a variant here is a **breaking change** without this attribute,
/// because a downstream `match` on `CommandKind` would stop compiling. That is
/// not hypothetical for this enum: [`crate::metrics::CommandKind::Rehydrate`]
/// did not exist until 0.12.9 precisely because adding it was a break, and
/// rehydration reported as `Archive` for several releases as a result. The
/// codebase has already paid this cost once, which is the argument for paying
/// the attribute now rather than deciding it at 1.0 when the cost is permanent.
///
/// Callers must therefore include a `_ =>` arm. In exchange, this enum can grow
/// a variant for a command kind that does not exist yet without a major version.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
#[non_exhaustive]
pub enum CommandKind {
    AssertEdge,
    RetireEdge,
    UpsertConcept,
    WriteBulkAtomic,
    RebuildCurrent,
    RegisterModel,
    Shutdown,
    BulkImportChunk,
    WriteConceptsChunk,
    WriteAnalyticsChunk,
    UpsertEmbeddingChunk,
    Archive,
    RebuildFts,
    /// One step of a chunked shadow rebuild (T1.2). Its own kind rather than
    /// folded into `RebuildCurrent`, because the two have opposite latency
    /// profiles and the whole point of the chunked path is that its turns are
    /// short — averaging them together would hide exactly the improvement.
    ShadowRebuild,
    /// Refreshing the query planner's statistics (0.12.4, D-149).
    ///
    /// Its own kind rather than folded into `RebuildFts`, though both are
    /// maintenance on derived state: this one is bounded by
    /// `PRAGMA analysis_limit` and that one is bounded by the size of the
    /// concept table, so averaging their holds together would describe neither.
    Analyze,
    /// Moving archived rows back into the hot file (0.12.9, W4.3, D-152).
    ///
    /// Its own kind at last. Through 0.12.8 this reported as
    /// [`CommandKind::Archive`], on the stated ground that rehydration is the
    /// archive path run backwards and shares its budget — true of the *budget*
    /// and false of the *attribution*, which is what a metrics surface is for.
    /// An operator reading a long `archive` hold could not tell whether the
    /// database had archived anything at all, and the two move rows in opposite
    /// directions.
    ///
    /// The real reason it stayed folded was that adding a variant was a
    /// breaking change. `#[non_exhaustive]` (W4.2) is what removed that
    /// obstacle, and this variant is the first thing it bought — which is also
    /// the evidence that the attribute was worth adding rather than a
    /// precaution against a hypothetical.
    ///
    /// **Appended at the end**, per [`CommandKind::index`]: the position of
    /// every existing variant is a persisted contract in two languages.
    Rehydrate,
    /// An explicit `PRAGMA wal_checkpoint` (0.12.13, W5.2, D-156).
    ///
    /// Its own kind because it is the one actor turn that is **not** a
    /// transaction: it moves frames from the WAL back into the main database
    /// file, and its duration is a function of how much WAL has accumulated
    /// rather than of anything the caller passed. Folding it into any existing
    /// kind would make that kind's hold distribution bimodal for a reason no
    /// dashboard could recover.
    ///
    /// **Appended at the end**, per [`CommandKind::index`].
    Checkpoint,
}

impl CommandKind {
    /// Every kind, in declaration order. Indexing into the per-kind arrays is by
    /// position in this slice, so the two must not drift — which is why the
    /// arrays are sized from `ALL.len()` rather than from a hand-written count.
    pub const ALL: &'static [CommandKind] = &[
        CommandKind::AssertEdge,
        CommandKind::RetireEdge,
        CommandKind::UpsertConcept,
        CommandKind::WriteBulkAtomic,
        CommandKind::RebuildCurrent,
        CommandKind::RegisterModel,
        CommandKind::Shutdown,
        CommandKind::BulkImportChunk,
        CommandKind::WriteConceptsChunk,
        CommandKind::WriteAnalyticsChunk,
        CommandKind::UpsertEmbeddingChunk,
        CommandKind::Archive,
        CommandKind::RebuildFts,
        CommandKind::ShadowRebuild,
        CommandKind::Analyze,
        CommandKind::Rehydrate,
        CommandKind::Checkpoint,
    ];

    pub const COUNT: usize = CommandKind::ALL.len();

    /// This kind's slot in the per-kind arrays.
    ///
    /// # Declaration order is a persisted contract (0.12.8, W4.2)
    ///
    /// `self as usize` means the **order of the variants above** is the order of
    /// every per-kind array in this module, and the compiler cannot catch a
    /// change to it. Reordering the enum silently reassigns every counter to a
    /// different command: the code compiles, the tests pass, and a histogram
    /// read after the change attributes `archive`'s holds to `rebuild_fts`.
    ///
    /// **New variants go at the end**, always — including at the end of
    /// [`CommandKind::ALL`], whose order is what `as_str()` and the Python
    /// surface enumerate. This binds Python too: `BUCKET_BOUNDS_MICROS` is a
    /// module constant there and `KindMetrics` is built by position, so a
    /// reorder here relabels axes in a language the Rust compiler is not
    /// looking at.
    ///
    /// `#[repr(u8)]` is on the enum for the same reason — it pins the
    /// discriminants to the declaration order rather than leaving them to the
    /// compiler — but it pins them to whatever the order *is*, so it does not
    /// make a reorder safe. Only this rule does.
    pub const fn index(self) -> usize {
        self as usize
    }

    pub const fn as_str(self) -> &'static str {
        match self {
            CommandKind::AssertEdge => "assert_edge",
            CommandKind::RetireEdge => "retire_edge",
            CommandKind::UpsertConcept => "upsert_concept",
            CommandKind::WriteBulkAtomic => "write_bulk_atomic",
            CommandKind::RebuildCurrent => "rebuild_current",
            CommandKind::RegisterModel => "register_model",
            CommandKind::Shutdown => "shutdown",
            CommandKind::BulkImportChunk => "bulk_import_chunk",
            CommandKind::WriteConceptsChunk => "write_concepts_chunk",
            CommandKind::WriteAnalyticsChunk => "write_analytics_chunk",
            CommandKind::UpsertEmbeddingChunk => "upsert_embedding_chunk",
            CommandKind::Archive => "archive",
            CommandKind::RebuildFts => "rebuild_fts",
            CommandKind::ShadowRebuild => "shadow_rebuild",
            CommandKind::Analyze => "analyze",
            CommandKind::Rehydrate => "rehydrate",
            CommandKind::Checkpoint => "checkpoint",
        }
    }

    /// Whether this kind is exempt from [`crate::CHUNK_BUDGET`] by contract.
    ///
    /// The exemptions are the table in `CHUNK_BUDGET`'s own rustdoc, and they
    /// are carried here so a dashboard can separate "the budget is being
    /// broken" from "the budget does not apply and never claimed to". Counting
    /// an `archive` as a budget violation would make the violation count useless
    /// on any database that archives.
    ///
    /// The two lists must agree, and since 0.12.9 they are tied together in
    /// both directions by `the_budget_exemptions_and_their_documented_table_agree`
    /// — the extra-row direction being the one worth having, since a table row
    /// with no code behind it promises a caller an exemption the violation
    /// counter is about to disagree with.
    ///
    /// [`CommandKind::ShadowRebuild`] is deliberately **not** exempt. Its fill
    /// chunks are meant to fit the budget and its swap turn is not going to —
    /// the swap rebuilds three indexes under the lock, which is the residual
    /// cost T1.2 could not remove. Both facts are worth seeing, and exempting
    /// the kind would hide the first to excuse the second.
    ///
    /// # `Rehydrate` is exempt, and splitting it out is what made that a
    /// decision rather than an accident (0.12.9, W4.3, D-152)
    ///
    /// Until 0.12.8 rehydration reported as [`CommandKind::Archive`] and was
    /// therefore exempt **by inheritance** — nobody had decided it, it fell out
    /// of the borrowed kind. Giving it its own variant would have silently
    /// flipped it to non-exempt, and since a rehydrate is one unchunked
    /// transaction moving rows back across the file boundary, every single one
    /// would have counted as a budget violation. The violation count would then
    /// have become useless on any database that rehydrates, which is precisely
    /// the failure the `Archive` exemption exists to prevent, arriving by the
    /// back door of a change made for attribution.
    ///
    /// So it is exempt, on the merits and now on the record: rehydration is the
    /// archive path run backwards and makes the same claim about its hold —
    /// that it is bulk movement with no latency bound, and that the caller asked
    /// for it explicitly.
    ///
    /// # [`CommandKind::Analyze`] is **not** exempt, and that is a decision
    /// (0.12.25, D-168)
    ///
    /// It looks like it belongs here. `ANALYZE` is one indivisible statement —
    /// there is no smaller unit to chunk into — and its cost is set by data
    /// volume, so it cannot meet the budget on a populated ledger. Measured
    /// (`examples/analyze_hold.rs`): **5.26 ms at 10,000 edges, 19.1 ms at
    /// 40,000**, against 3 ms. Every call is a violation and always will be.
    ///
    /// It stays counted for two reasons.
    ///
    /// **The table has a `Bound` column, and this kind cannot fill it in.**
    /// `Checkpoint`'s bound is frames accumulated since the last one;
    /// `Archive`'s is the session's row count. The honest entry here would be
    /// "the size of the table, damped 3–4× by `analysis_limit`", which is not a
    /// bound but the absence of one. A row that cannot state its bound is this
    /// table admitting the thing it exists to prevent.
    ///
    /// **And this kind is two callers wearing one name.** `Analyze` covers
    /// [`crate::Database::optimize`] as well as [`crate::Database::analyze`],
    /// and `close()` calls `optimize()` unconditionally. Exempting the kind
    /// would silence the *automatic* path — every handle close on a large
    /// ledger holding ~19 ms with nothing reporting it — which is exactly the
    /// call nobody chose to make. That is [`CommandKind::Rehydrate`]'s lesson
    /// above, arriving from the other direction: there, a shared kind granted
    /// an exemption nobody had decided; here, a shared kind would launder one.
    ///
    /// **So the violation is expected, permanent, and must not be "fixed" by
    /// lowering [`crate::schema::ddl::ANALYSIS_LIMIT`].** That would buy the
    /// number by sampling too little to separate the two `source_id`-leading
    /// indices, which is the entire purpose of having statistics
    /// ([D-149](../docs/architecture/s13-decision-register.md#d-149)).
    ///
    /// Splitting the kind is scheduled as **W10.5, 0.14.0**; the exemption
    /// question is answerable per-caller once it is split, and not before.
    pub const fn exempt_from_budget(self) -> bool {
        matches!(
            self,
            CommandKind::WriteBulkAtomic
                | CommandKind::Archive
                | CommandKind::RebuildCurrent
                | CommandKind::Rehydrate
                | CommandKind::Checkpoint
        )
    }
}

impl std::fmt::Display for CommandKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Upper bounds of the hold-duration histogram, in microseconds.
///
/// `3_000` is [`crate::CHUNK_BUDGET`] exactly, so the bucket boundary and the
/// bound are the same number and a reader does not have to interpolate to answer
/// "what fraction of turns fit". The tail runs to 1 s because D-059's measured
/// worst case was 45 ms and `rebuild_current` at 40K rows is 318 ms (D-077) —
/// a range this has to cover without saturating.
///
/// Anything above the last bound lands in the overflow bucket, which is why
/// [`KindSnapshot::buckets`] is one longer than this slice.
pub const BUCKET_BOUNDS_MICROS: &[u64] = &[
    100, 300, 1_000, 3_000, 10_000, 30_000, 100_000, 300_000, 1_000_000,
];

/// Number of histogram buckets, including the overflow bucket.
pub const BUCKET_COUNT: usize = BUCKET_BOUNDS_MICROS.len() + 1;

#[allow(dead_code)] // used by `imp` under `metrics`, and by the tests always
fn bucket_of(micros: u64) -> usize {
    // Linear scan over nine bounds. A binary search here would be slower in
    // practice and this runs once per actor turn, against a turn measured in
    // microseconds at best.
    BUCKET_BOUNDS_MICROS
        .iter()
        .position(|&bound| micros <= bound)
        .unwrap_or(BUCKET_BOUNDS_MICROS.len())
}

/// Times one actor turn.
///
/// # This clock is no longer optional (0.12.0, W1)
///
/// Until 0.11.0 the field was `#[cfg(feature = "metrics")]` and `elapsed()`
/// returned `Duration::ZERO` in a default build: the reading existed only to
/// feed [`ActorMetrics::record_hold`]'s histogram, so a build that did not keep
/// the histogram had no reason to read a clock.
///
/// The chunk loop changed what the reading is *for*. A chunk's measured hold is
/// now the input to the next chunk's size (`connection::next_chunk_size`, named
/// in prose because it is private — D-144), which means it is a control signal
/// in every build and not an observation in some of them. Left gated, `bulk_import` would have sized its chunks off
/// `Duration::ZERO` — a value that reads as "comfortably under budget" — and
/// grown every chunk to the ceiling, in exactly the builds nobody was measuring.
///
/// So the clock is unconditional and **only the histogram is still gated**:
/// `record_hold` remains a no-op without the feature. What that costs is one
/// `Instant::now()` pair per actor turn — tens of nanoseconds against a turn
/// measured in microseconds at best, and the same reasoning §5.1.5 uses to
/// decide that a channel hop is free beside a chunk.
///
/// It stays a type rather than a bare `Instant::now()` in the loop because the
/// ordering guarantee in [`crate::connection`]'s `Turn` is attached to it.
pub struct HoldTimer {
    start: std::time::Instant,
}

impl HoldTimer {
    #[inline]
    pub fn start() -> Self {
        Self {
            start: std::time::Instant::now(),
        }
    }

    #[inline]
    pub fn elapsed(&self) -> Duration {
        self.start.elapsed()
    }
}

// ---------------------------------------------------------------------------
// Instrumented implementation
// ---------------------------------------------------------------------------

#[cfg(feature = "metrics")]
mod imp {
    use super::{bucket_of, CommandKind, BUCKET_COUNT};
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::time::Duration;

    /// One kind's counters. All `Relaxed`: these are statistics, and ordering
    /// them against each other would buy a consistency no reader needs and cost
    /// fences on the write path.
    #[derive(Debug, Default)]
    struct Kind {
        turns: AtomicU64,
        total_micros: AtomicU64,
        over_budget: AtomicU64,
        /// This kind's own high-water mark, in µs.
        ///
        /// Not redundant with the global `longest`. That one names a single
        /// command, so on any real database it names whichever kind is slowest
        /// overall — and the question "did windowing shrink the archive's worst
        /// hold" cannot be answered by a counter that a bulk import wins. No
        /// packing needed here: the kind is the array index.
        longest_micros: AtomicU64,
        buckets: [AtomicU64; BUCKET_COUNT],
    }

    /// Live counters, shared between the actor and the handle.
    ///
    /// Fixed size, no allocation, no lock. The actor updates; anyone may read.
    #[derive(Debug, Default)]
    pub struct ActorMetrics {
        kinds: [Kind; CommandKind::COUNT],
        /// Packed `micros << 8 | kind`, so the longest hold and the kind that
        /// caused it are read and written as **one** value. Two atomics would
        /// let a reader see a duration from one turn beside a kind from
        /// another — a rare wrong answer to exactly the question this field
        /// exists to answer. 2^56 µs is over two thousand years.
        ///
        /// **The duration must occupy the high bits.** The update is a
        /// `fetch_max` on the packed word, so whichever field is packed high is
        /// the one being compared. The first version of this had the kind up
        /// there, which made the "longest hold" the hold with the largest
        /// *enum index* — a 3 ms `write_concepts_chunk` beat a 10 ms
        /// `rebuild_current` because its variant is declared later. It was
        /// `actor_metrics_tests` that caught it, not the unit tests, because
        /// nothing in the arithmetic is wrong: the packing is only incorrect in
        /// the presence of the atomic operation it exists to serve.
        longest: AtomicU64,
        /// Loop iterations, which is **not** the number of turns taken.
        ///
        /// The depth sample happens at the top of the loop, before `select!`
        /// blocks — so an idle actor has already counted the iteration for a
        /// command that has not arrived. That is right for depth (the sample is
        /// "what was queued when I went looking") and wrong for turns, which is
        /// why [`MetricsSnapshot::turns`] is the sum of the per-kind counters
        /// instead. Conflating the two made `turns` permanently one too high and
        /// disagree with its own breakdown.
        depth_samples: AtomicU64,
        high_depth_sum: AtomicU64,
        high_depth_max: AtomicU64,
        low_depth_sum: AtomicU64,
        low_depth_max: AtomicU64,
        /// Turns where the actor took high-priority work while low-priority
        /// work was already queued (0.12.10, W4.4, D-153).
        ///
        /// The `biased` `select!` in `run_writer_actor` has **no floor**:
        /// sustained high-priority traffic can hold the low tier off
        /// indefinitely, and nothing has ever said whether that happens. This
        /// is the numerator of that question — how often the choice went
        /// against the low tier at all.
        low_starved_turns: AtomicU64,
        /// The current unbroken run of such turns. Reset to zero the moment
        /// low-priority work is taken.
        ///
        /// Not exposed; it is the state [`Self::low_starved_run_max`] is a
        /// high-water mark of. A live value would be read at an arbitrary point
        /// in a run and mean nothing.
        low_starved_run: AtomicU64,
        /// The longest such run since open, which is the number that answers the
        /// question.
        ///
        /// A large `low_starved_turns` on a busy database is unremarkable — it
        /// says the high tier is being used, which is what the tier is for. A
        /// large *run* says one specific low-priority command waited that many
        /// turns, and it is the only one of the two that can distinguish
        /// "prioritised" from "starved".
        low_starved_run_max: AtomicU64,
    }

    const MICROS_SHIFT: u32 = 8;
    const KIND_MASK: u64 = (1 << MICROS_SHIFT) - 1;

    impl ActorMetrics {
        pub fn new() -> Self {
            Self::default()
        }

        /// Sample both queue depths. Called before the turn, not after: after
        /// the turn the queue reflects what arrived *during* it, which is a
        /// different and much less useful quantity.
        #[inline]
        pub fn record_turn(&self, high_depth: usize, low_depth: usize) {
            self.depth_samples.fetch_add(1, Ordering::Relaxed);
            for (sum, max, depth) in [
                (
                    &self.high_depth_sum,
                    &self.high_depth_max,
                    high_depth as u64,
                ),
                (&self.low_depth_sum, &self.low_depth_max, low_depth as u64),
            ] {
                sum.fetch_add(depth, Ordering::Relaxed);
                max.fetch_max(depth, Ordering::Relaxed);
            }
        }

        /// Record which tier the `select!` chose, and what was waiting.
        ///
        /// `low_queued` is the depth sampled *before* the `select!`, so it is
        /// the backlog the turn found on arrival. By the time a high-priority
        /// arm fires the low queue may have grown; using the pre-select reading
        /// keeps this consistent with every other depth figure in this module
        /// and makes the counter conservative — it never invents starvation
        /// from work that arrived after the choice was made.
        ///
        /// A low-priority turn resets the run rather than decrementing it: the
        /// question is "how many turns did one low-priority command wait", and
        /// that is a run length, not a balance.
        #[inline]
        pub fn record_priority_choice(&self, took_high: bool, low_queued: usize) {
            if took_high && low_queued > 0 {
                self.low_starved_turns.fetch_add(1, Ordering::Relaxed);
                let run = self.low_starved_run.fetch_add(1, Ordering::Relaxed) + 1;
                self.low_starved_run_max.fetch_max(run, Ordering::Relaxed);
            } else if !took_high {
                self.low_starved_run.store(0, Ordering::Relaxed);
            }
        }

        #[inline]
        pub fn record_hold(&self, kind: CommandKind, held: Duration) {
            let micros = held.as_micros().min(super::MICROS_CEILING as u128) as u64;
            let k = &self.kinds[kind.index()];
            k.turns.fetch_add(1, Ordering::Relaxed);
            k.total_micros.fetch_add(micros, Ordering::Relaxed);
            k.buckets[bucket_of(micros)].fetch_add(1, Ordering::Relaxed);
            k.longest_micros.fetch_max(micros, Ordering::Relaxed);
            if !kind.exempt_from_budget() && held > crate::CHUNK_BUDGET {
                k.over_budget.fetch_add(1, Ordering::Relaxed);
            }
            self.longest.fetch_max(
                (micros << MICROS_SHIFT) | kind.index() as u64,
                Ordering::Relaxed,
            );
        }

        /// A consistent-enough picture for a dashboard.
        ///
        /// Not a torn-read-free snapshot, and it does not pretend to be: the
        /// actor keeps running while this walks the array, so two kinds may be
        /// read one turn apart. Locking the actor to produce a report would make
        /// the observer a source of the latency it is measuring.
        pub fn snapshot(&self) -> super::MetricsSnapshot {
            let samples = self.depth_samples.load(Ordering::Relaxed);
            let mean = |sum: &AtomicU64| {
                if samples == 0 {
                    0.0
                } else {
                    sum.load(Ordering::Relaxed) as f64 / samples as f64
                }
            };

            let packed = self.longest.load(Ordering::Relaxed);
            let longest_micros = packed >> MICROS_SHIFT;
            let longest = (longest_micros > 0)
                .then(|| {
                    let idx = (packed & KIND_MASK) as usize;
                    CommandKind::ALL
                        .get(idx)
                        .map(|&kind| (kind, Duration::from_micros(longest_micros)))
                })
                .flatten();

            let kinds: Vec<_> = CommandKind::ALL
                .iter()
                .map(|&kind| {
                    let k = &self.kinds[kind.index()];
                    let turns = k.turns.load(Ordering::Relaxed);
                    let total = k.total_micros.load(Ordering::Relaxed);
                    super::KindSnapshot {
                        kind,
                        turns,
                        over_budget: k.over_budget.load(Ordering::Relaxed),
                        mean: total
                            .checked_div(turns)
                            .map_or(Duration::ZERO, Duration::from_micros),
                        longest: Duration::from_micros(k.longest_micros.load(Ordering::Relaxed)),
                        buckets: std::array::from_fn(|i| k.buckets[i].load(Ordering::Relaxed)),
                    }
                })
                .collect();

            super::MetricsSnapshot {
                // Summed, not counted separately — see `depth_samples`.
                turns: kinds.iter().map(|k| k.turns).sum(),
                depth_samples: samples,
                high_depth_mean: mean(&self.high_depth_sum),
                high_depth_max: self.high_depth_max.load(Ordering::Relaxed),
                low_depth_mean: mean(&self.low_depth_sum),
                low_depth_max: self.low_depth_max.load(Ordering::Relaxed),
                low_starved_turns: self.low_starved_turns.load(Ordering::Relaxed),
                low_starved_run_max: self.low_starved_run_max.load(Ordering::Relaxed),
                longest,
                kinds,
            }
        }
    }
}

// ---------------------------------------------------------------------------
// No-op implementation
// ---------------------------------------------------------------------------

#[cfg(not(feature = "metrics"))]
mod imp {
    use super::CommandKind;
    use std::time::Duration;

    /// The `metrics`-off shape: zero-sized, and every method is nothing.
    #[derive(Debug, Default)]
    pub struct ActorMetrics;

    impl ActorMetrics {
        pub fn new() -> Self {
            Self
        }
        #[inline]
        pub fn record_turn(&self, _high_depth: usize, _low_depth: usize) {}
        #[inline]
        pub fn record_priority_choice(&self, _took_high: bool, _low_queued: usize) {}
        #[inline]
        pub fn record_hold(&self, _kind: CommandKind, _held: Duration) {}
    }
}

pub use imp::ActorMetrics;

/// Saturation point for a recorded hold, in microseconds (~2,000 years).
///
/// Exists so the packed `longest` field cannot have a pathological duration
/// overflow into the kind bits. A hold this long is not a measurement, it is a
/// hang — and the counter should stay readable rather than start reporting the
/// wrong command.
///
/// Kept out of the `metrics` cfg so the invariant test below runs in the default
/// build too: the packing is a property of the layout, and a build that does not
/// record is exactly the build where nobody would notice it break.
#[allow(dead_code)]
const MICROS_CEILING: u64 = (1u64 << 56) - 1;

/// One command kind's holds, as of the moment [`ActorMetrics::snapshot`] read it.
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct KindSnapshot {
    pub kind: CommandKind,
    /// Turns spent on this kind.
    pub turns: u64,
    /// Turns that exceeded [`crate::CHUNK_BUDGET`]. Always 0 for the three
    /// kinds [`CommandKind::exempt_from_budget`] names — see there for why.
    pub over_budget: u64,
    pub mean: Duration,
    /// This kind's longest hold. Distinct from [`MetricsSnapshot::longest`],
    /// which names one command across all kinds and so tends to be permanently
    /// whichever kind is slowest overall.
    pub longest: Duration,
    /// Counts per [`BUCKET_BOUNDS_MICROS`], plus a final overflow bucket.
    ///
    /// Private behind [`Self::buckets`] since 0.12.8 (W4.2). A public array
    /// field publishes `BUCKET_COUNT` as part of the type's shape, so adding a
    /// bucket bound would break every caller that named the length — and the
    /// bounds are exactly the thing a latency histogram is likely to want to
    /// re-cut. The accessor returns a slice and the length becomes an
    /// observation rather than a signature. Python already did it this way.
    buckets: [u64; BUCKET_COUNT],
}

#[cfg(feature = "metrics")]
impl KindSnapshot {
    /// Counts per [`BUCKET_BOUNDS_MICROS`], plus a final overflow bucket.
    ///
    /// Pair it with `BUCKET_BOUNDS_MICROS` to label the axis rather than
    /// hard-coding the bounds; the slice is one longer than that constant,
    /// and the extra trailing element is the overflow bucket.
    pub fn buckets(&self) -> &[u64] {
        &self.buckets
    }
}

/// What the actor has done since the database was opened.
#[cfg(feature = "metrics")]
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct MetricsSnapshot {
    /// Commands executed, i.e. the sum of [`KindSnapshot::turns`]. The two agree
    /// by construction rather than by coincidence.
    pub turns: u64,
    /// Loop iterations that took a queue-depth reading. Always at least
    /// `turns + 1` on a live actor, because the reading is taken on the way in
    /// to a `select!` that has not resolved yet. This is the denominator of the
    /// two means below, and it is exposed so the difference is visible rather
    /// than looking like drift.
    pub depth_samples: u64,
    pub high_depth_mean: f64,
    pub high_depth_max: u64,
    pub low_depth_mean: f64,
    pub low_depth_max: u64,
    /// The longest hold since open and what caused it. `None` before the first
    /// turn, and — honestly — also when every turn so far took under a
    /// microsecond, which on this path does not happen.
    pub longest: Option<(CommandKind, Duration)>,
    /// Turns spent on high-priority work while low-priority work was already
    /// queued (0.12.10, W4.4, D-153).
    ///
    /// The actor's `select!` is `biased` and has **no floor**, so this is the
    /// measurement of a bound the design has always had and never observed.
    /// On its own it is not alarming: a busy database *should* prefer
    /// interactive writes, and this counter rising is that working. Read it
    /// beside [`Self::low_starved_run_max`], which is the number with teeth.
    pub low_starved_turns: u64,
    /// The longest unbroken run of the above — i.e. the most turns any single
    /// low-priority command has waited (0.12.10, W4.4, D-153).
    ///
    /// This is the one that answers "can low-priority work be starved". A large
    /// `low_starved_turns` spread over a long session says the tiers are doing
    /// their job; a large *run* says one specific chunk, rebuild or archive sat
    /// behind that many interactive writes in a row.
    ///
    /// **There is deliberately no forced-yield policy attached to this.**
    /// Adding one now would be fixing a bound nobody has observed being hit,
    /// and the counter exists precisely to find out whether it is. See D-153.
    pub low_starved_run_max: u64,
    pub kinds: Vec<KindSnapshot>,
}

#[cfg(feature = "metrics")]
impl MetricsSnapshot {
    /// Kinds that broke the budget, worst first. The one-line answer to "is the
    /// 3 ms bound holding?".
    pub fn budget_violations(&self) -> Vec<&KindSnapshot> {
        let mut v: Vec<_> = self.kinds.iter().filter(|k| k.over_budget > 0).collect();
        v.sort_by_key(|k| std::cmp::Reverse(k.over_budget));
        v
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn every_kind_indexes_to_its_own_slot() {
        for (i, &kind) in CommandKind::ALL.iter().enumerate() {
            assert_eq!(kind.index(), i, "{kind} is out of order in ALL");
        }
        assert_eq!(CommandKind::COUNT, CommandKind::ALL.len());
    }

    /// The budget is a bucket boundary, not a value inside one — so "fits in the
    /// budget" is a prefix sum and needs no interpolation.
    #[test]
    fn the_chunk_budget_is_exactly_a_bucket_boundary() {
        let budget = crate::CHUNK_BUDGET.as_micros() as u64;
        assert!(
            BUCKET_BOUNDS_MICROS.contains(&budget),
            "CHUNK_BUDGET is {budget} µs, which is not a bucket bound: \
             {BUCKET_BOUNDS_MICROS:?}"
        );
        assert_eq!(bucket_of(budget), bucket_of(budget - 1));
        assert_eq!(bucket_of(budget + 1), bucket_of(budget) + 1);
    }

    #[test]
    fn the_overflow_bucket_catches_everything_past_the_last_bound() {
        let last = *BUCKET_BOUNDS_MICROS.last().unwrap();
        assert_eq!(bucket_of(last), BUCKET_BOUNDS_MICROS.len() - 1);
        assert_eq!(bucket_of(last + 1), BUCKET_COUNT - 1);
        assert_eq!(bucket_of(u64::MAX), BUCKET_COUNT - 1);
    }

    /// The packing is the reason `longest` is one atomic: duration high, kind
    /// low, so a `fetch_max` on the word compares the duration.
    #[test]
    fn the_packing_leaves_room_for_both_fields() {
        assert!(
            (CommandKind::COUNT as u64) <= 0xFF,
            "the kind index must fit in the low 8 bits"
        );
        // The ceiling must survive being shifted up by the kind's width.
        assert_eq!(MICROS_CEILING.checked_shl(8), Some(MICROS_CEILING << 8));
        assert_eq!((MICROS_CEILING << 8) >> 8, MICROS_CEILING);
    }

    #[cfg(feature = "metrics")]
    #[test]
    fn the_longest_hold_names_the_command_that_caused_it() {
        let m = ActorMetrics::new();
        m.record_hold(CommandKind::AssertEdge, Duration::from_micros(500));
        m.record_hold(CommandKind::Archive, Duration::from_millis(40));
        m.record_hold(CommandKind::UpsertConcept, Duration::from_micros(900));

        let snap = m.snapshot();
        assert_eq!(
            snap.longest,
            Some((CommandKind::Archive, Duration::from_millis(40)))
        );
    }

    /// The regression the packing bug produced: a *short* hold of a
    /// later-declared kind must not outrank a long hold of an earlier one.
    ///
    /// The test above does not catch it, because `Archive` happens to be both
    /// the longest hold and a high enum index — which is exactly why the first
    /// version of the packing shipped past it. Here the two orderings disagree.
    #[cfg(feature = "metrics")]
    #[test]
    fn a_later_declared_kind_does_not_outrank_a_longer_hold() {
        let long = CommandKind::AssertEdge; // index 0
        let short = CommandKind::RebuildFts; // last index
        assert!(short.index() > long.index(), "the fixture needs the gap");

        let m = ActorMetrics::new();
        m.record_hold(long, Duration::from_millis(40));
        m.record_hold(short, Duration::from_micros(1));

        assert_eq!(
            m.snapshot().longest,
            Some((long, Duration::from_millis(40))),
            "the max is being taken over the kind index, not the duration"
        );
    }

    /// The three contractual exemptions must not show up as violations, or the
    /// violation count is noise on any database that archives.
    #[cfg(feature = "metrics")]
    #[test]
    fn an_exempt_kind_over_budget_is_not_a_violation() {
        let m = ActorMetrics::new();
        m.record_hold(CommandKind::Archive, Duration::from_millis(40));
        m.record_hold(CommandKind::AssertEdge, Duration::from_millis(40));

        let snap = m.snapshot();
        let violations = snap.budget_violations();
        assert_eq!(violations.len(), 1);
        assert_eq!(violations[0].kind, CommandKind::AssertEdge);
        assert_eq!(violations[0].over_budget, 1);

        // But the hold is still *recorded* — exempt means "not a violation",
        // not "not measured". A 40 ms archive is exactly what T1.1 exists to
        // shrink, and it cannot be shrunk if it is not counted.
        let archive = snap
            .kinds
            .iter()
            .find(|k| k.kind == CommandKind::Archive)
            .unwrap();
        assert_eq!(archive.turns, 1);
        assert_eq!(archive.mean, Duration::from_millis(40));
    }

    #[cfg(feature = "metrics")]
    #[test]
    fn queue_depth_is_a_mean_and_a_high_water_mark() {
        let m = ActorMetrics::new();
        m.record_turn(0, 4);
        m.record_turn(10, 0);

        let snap = m.snapshot();
        // No command ran, so `turns` is 0 while `depth_samples` is 2. The two
        // counters are different facts and this is the case that shows it.
        assert_eq!(snap.turns, 0);
        assert_eq!(snap.depth_samples, 2);
        assert_eq!(snap.high_depth_mean, 5.0);
        assert_eq!(snap.high_depth_max, 10);
        assert_eq!(snap.low_depth_mean, 2.0);
        assert_eq!(snap.low_depth_max, 4);
    }
}