Skip to main content

trine_kv/
stats.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2
3/// Snapshot of live database, storage, maintenance, cache, and read-path stats.
4#[derive(Debug, Clone, Default, PartialEq, Eq)]
5pub struct DbStats {
6    /// Number of bucket states currently loaded.
7    pub live_buckets: usize,
8    /// Number of pinned snapshots.
9    pub active_snapshots: usize,
10    /// Oldest read sequence still pinned by an active snapshot (or the current
11    /// visible sequence when none are pinned).
12    pub oldest_snapshot_seq: u64,
13    /// Version-debt held by the oldest snapshot: visible sequence minus
14    /// `oldest_snapshot_seq`. Larger values keep more obsolete versions alive.
15    pub oldest_snapshot_lag: u64,
16    /// Internal version records merged by range/prefix scans since open.
17    pub scan_internal_records: u64,
18    /// User keys returned by range/prefix scans since open. The scan
19    /// read-amplification ratio is `scan_internal_records / scan_user_keys`.
20    pub scan_user_keys: u64,
21    /// User-key groups hidden by a point or range delete during scans.
22    pub scan_tombstone_hidden_keys: u64,
23    /// Approximate bytes held by active memtables.
24    pub memtable_bytes: u64,
25    /// Number of immutable memtables waiting for flush.
26    pub immutable_memtables: usize,
27    /// Number of level-0 table files.
28    pub l0_tables: usize,
29    /// Total number of table files across all levels.
30    pub total_tables: usize,
31    /// Per-level table counts and byte sizes.
32    pub level_tables: Vec<LevelStats>,
33    /// Per-level filter counters summed over each level's table files, for
34    /// layered (Monkey-style) filter allocation analysis.
35    pub level_filters: Vec<LevelFilterStats>,
36    /// Total bytes held by table files.
37    pub table_bytes: u64,
38    /// WAL bytes accepted but not yet synced according to the selected durability.
39    pub wal_bytes_pending_sync: u64,
40    /// Number of blob files referenced by live records.
41    pub live_blob_files: usize,
42    /// Bytes in blob files referenced by live records.
43    pub live_blob_bytes: u64,
44    /// Number of blob files with discardable bytes.
45    pub stale_blob_files: usize,
46    /// Discardable bytes in stale blob files.
47    pub stale_blob_bytes: u64,
48    /// Number of blob files no longer referenced by live records.
49    pub obsolete_blob_files: usize,
50    /// Bytes in obsolete blob files.
51    pub obsolete_blob_bytes: u64,
52    /// Number of blob garbage-collection runs.
53    pub blob_gc_runs: u64,
54    /// Input bytes scanned by blob garbage collection.
55    pub blob_gc_input_bytes: u64,
56    /// Output bytes written by blob garbage collection.
57    pub blob_gc_output_bytes: u64,
58    /// Bytes discarded by blob garbage collection.
59    pub blob_gc_discarded_bytes: u64,
60    /// Number of blob value reads.
61    pub blob_read_count: u64,
62    /// Bytes returned by blob value reads.
63    pub blob_read_bytes: u64,
64    /// Number of compaction runs.
65    pub compaction_runs: u64,
66    /// Table files read by compaction.
67    pub compaction_input_tables: u64,
68    /// Table files written by compaction.
69    pub compaction_output_tables: u64,
70    /// Table bytes read by compaction.
71    pub compaction_input_bytes: u64,
72    /// Table bytes written by compaction.
73    pub compaction_output_bytes: u64,
74    /// Per-level table and byte counts read and written by compaction.
75    pub compaction_levels: Vec<CompactionLevelStats>,
76    /// Per-trigger table and byte counts read and written by compaction.
77    pub compaction_triggers: Vec<CompactionTriggerStats>,
78    /// Per-reason counts for compactions the picker deliberately did not run.
79    pub compaction_skips: Vec<CompactionSkipStats>,
80    /// Commit sequences allocated by writers.
81    pub commit_sequences_allocated: u64,
82    /// Highest commit sequence visible to readers.
83    pub commit_visible_sequence: u64,
84    /// Commit slots allocated but not yet published or skipped.
85    pub commit_open_slots: usize,
86    /// Commit slots skipped after failed publication.
87    pub commit_skipped_slots: u64,
88    /// Number of WAL shards configured for the database.
89    pub wal_shards: usize,
90    /// Number of WAL shards currently open.
91    pub wal_open_shards: usize,
92    /// Per-shard WAL queue capacity.
93    pub wal_queue_capacity: usize,
94    /// WAL records accepted by the writer path.
95    pub wal_records_accepted: u64,
96    /// WAL bytes accepted by the writer path.
97    pub wal_bytes_accepted: u64,
98    /// Whether storage work is using the runtime's sync adapter.
99    pub storage_uses_sync_adapter: bool,
100    /// Whether native storage work is routed through the platform I/O driver.
101    ///
102    /// This reports the selected storage driver. It can be `true` on targets
103    /// whose current operations are thread-pool managed; use
104    /// [`Self::storage_uses_platform_async_io`] and the platform task counters
105    /// to distinguish native async work from managed async work.
106    pub storage_uses_platform_io_driver: bool,
107    /// Whether storage work is using platform-io asynchronous completion.
108    pub storage_uses_platform_async_io: bool,
109    /// Blocking storage tasks accepted by the sync adapter.
110    pub storage_sync_adapter_tasks: u64,
111    /// Sync-adapter queue capacity.
112    pub storage_sync_adapter_queue_capacity: usize,
113    /// Sync-adapter tasks currently queued.
114    pub storage_sync_adapter_queued_tasks: usize,
115    /// Sync-adapter tasks submitted.
116    pub storage_sync_adapter_submitted_tasks: u64,
117    /// Sync-adapter tasks completed.
118    pub storage_sync_adapter_completed_tasks: u64,
119    /// Sync-adapter tasks rejected because the queue was full or unavailable.
120    pub storage_sync_adapter_rejected_tasks: u64,
121    /// Total runtime spent by sync-adapter tasks.
122    pub storage_sync_adapter_total_runtime_micros: u64,
123    /// Storage tasks completed through true or partial native platform async I/O.
124    pub storage_platform_async_io_tasks: u64,
125    /// Storage tasks completed by platform-io's managed thread-pool path.
126    pub storage_platform_thread_pool_managed_async_tasks: u64,
127    /// Storage tasks that used a synchronous fallback path.
128    pub storage_platform_sync_fallback_tasks: u64,
129    /// Per-operation platform I/O capability-class counters.
130    ///
131    /// These counters are filled only when native storage work is routed
132    /// through the `platform-io` driver. They explain how each Trine storage
133    /// operation completed at the platform boundary. For example, a target can
134    /// report true platform async reads while directory listing reports
135    /// thread-pool managed async.
136    pub storage_platform_io_operations: PlatformIoOperationStats,
137    /// Storage tasks completed inline.
138    pub storage_inline_tasks: u64,
139    /// Per-operation storage request counters and latency totals.
140    pub storage_operations: StorageOperationStats,
141    /// Cooperative maintenance yields.
142    pub maintenance_cooperative_yields: u64,
143    /// Maintenance runs stopped after exhausting their budget.
144    pub maintenance_budget_exhaustions: u64,
145    /// Block-cache hits.
146    pub block_cache_hits: u64,
147    /// Block-cache misses.
148    pub block_cache_misses: u64,
149    /// Point-read path counters.
150    pub read_path: ReadPathStats,
151    /// Filter hit, miss, and false-positive counters.
152    pub filters: FilterStats,
153}
154
155/// Request count and total latency for one storage operation.
156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
157pub struct StorageOperationMetric {
158    /// Number of operation requests.
159    pub requests: u64,
160    /// Total operation latency in microseconds.
161    pub total_latency_micros: u64,
162}
163
164/// Per-operation storage metrics.
165#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
166pub struct StorageOperationStats {
167    /// Opens of readable storage objects.
168    pub open_read: StorageOperationMetric,
169    /// Length queries for readable storage objects.
170    pub len: StorageOperationMetric,
171    /// Borrowed-buffer positioned reads.
172    pub read_exact_at: StorageOperationMetric,
173    /// Owned-buffer positioned reads.
174    pub read_exact_at_owned: StorageOperationMetric,
175    /// Whole-object byte reads.
176    pub read_object_bytes: StorageOperationMetric,
177    /// Opens of appendable storage objects.
178    pub open_append: StorageOperationMetric,
179    /// Appends to storage objects.
180    pub append: StorageOperationMetric,
181    /// Persistence requests for storage objects.
182    pub persist: StorageOperationMetric,
183    /// WAL rewrite operations.
184    pub rewrite_wal: StorageOperationMetric,
185    /// Writer-lease acquisition requests.
186    pub acquire_writer_lease: StorageOperationMetric,
187    /// Directory creation requests.
188    pub create_directory_all: StorageOperationMetric,
189    /// Directory file-list requests.
190    pub list_directory_files: StorageOperationMetric,
191    /// Directory sync requests after atomic renames.
192    pub sync_directory_after_renames: StorageOperationMetric,
193    /// Reads of the current manifest pointer.
194    pub read_current_manifest: StorageOperationMetric,
195    /// Manifest publish operations.
196    pub publish_manifest: StorageOperationMetric,
197    /// Whole-object writes.
198    pub write_object: StorageOperationMetric,
199    /// Object delete requests.
200    pub delete_object: StorageOperationMetric,
201    /// Object list requests.
202    pub list_objects: StorageOperationMetric,
203}
204
205/// Platform I/O capability-class counters for one Trine storage operation.
206///
207/// Each field is a task count. A single operation request increments exactly one
208/// field when it goes through the `platform-io` driver. Operations that use the
209/// normal bounded sync adapter or inline native file path do not increment this
210/// structure. A zero value usually means the operation has not run through the
211/// platform driver during the stats interval, not that the target lacks support.
212///
213/// Use [`Self::total`] to count all platform-driver completions for the
214/// operation, and [`Self::non_true_platform_async_total`] to count completions
215/// that were not end-to-end true platform async.
216///
217/// # Examples
218///
219/// ```
220/// use trine_kv::PlatformIoClassCounters;
221///
222/// let counters = PlatformIoClassCounters {
223///     true_platform_async: 2,
224///     thread_pool_managed_async: 1,
225///     ..PlatformIoClassCounters::default()
226/// };
227///
228/// assert_eq!(counters.total(), 3);
229/// assert_eq!(counters.non_true_platform_async_total(), 1);
230/// assert!(counters.uses_true_platform_async());
231/// assert!(counters.uses_non_true_platform_async());
232/// ```
233#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
234pub struct PlatformIoClassCounters {
235    /// Completions that used a real platform async completion mechanism.
236    ///
237    /// These are operation requests whose selected backend class is true
238    /// platform async for the whole Trine storage operation. On the current
239    /// backend matrix this is expected on Linux for the operations audited as
240    /// true async.
241    pub true_platform_async: u64,
242    /// Completions that used native async primitives but still needed
243    /// non-native work.
244    ///
245    /// This class means the platform has audited native async evidence for part
246    /// of the operation, but at least one user-visible step such as open,
247    /// metadata, sync, rename, delete, directory work, or lease handling is not
248    /// yet true platform async.
249    pub platform_native_async_but_partial: u64,
250    /// Completions handled by platform-io's managed thread-pool path.
251    ///
252    /// This class is used when the target or operation has no complete native
253    /// async file completion, but platform-io still returns an asynchronous
254    /// completion by running the blocking work on its managed thread pool. WASM
255    /// and other targets without native threads must not use this class.
256    pub thread_pool_managed_async: u64,
257    /// Completions handled by an explicit platform-driver blocking path.
258    ///
259    /// This class is reserved for operations that cannot currently be
260    /// completed by native async primitives or platform-io's managed
261    /// thread-pool path. Native targets should prefer
262    /// [`Self::thread_pool_managed_async`] when platform-io owns the blocking
263    /// work and returns an asynchronous completion to the caller.
264    pub blocking_fallback: u64,
265    /// Completions rejected because the target cannot support the operation.
266    pub unsupported: u64,
267}
268
269impl PlatformIoClassCounters {
270    /// Returns the total number of platform-driver completions in these class
271    /// counters.
272    ///
273    /// The result is a task count. It is the saturating sum of all class fields,
274    /// so it stays at `u64::MAX` instead of wrapping if counters are combined
275    /// after a very long process lifetime.
276    #[must_use]
277    pub fn total(self) -> u64 {
278        self.true_platform_async
279            .saturating_add(self.platform_native_async_but_partial)
280            .saturating_add(self.thread_pool_managed_async)
281            .saturating_add(self.blocking_fallback)
282            .saturating_add(self.unsupported)
283    }
284
285    /// Returns whether no platform-driver completion has been recorded.
286    ///
287    /// A zero result means this counter set is empty for the observed stats
288    /// snapshot. It does not by itself prove that the operation is unsupported;
289    /// use [`Self::has_unsupported`] after the operation has been attempted to
290    /// check unsupported completions.
291    #[must_use]
292    pub fn is_empty(self) -> bool {
293        self.total() == 0
294    }
295
296    /// Returns the number of platform-driver completions that were not true
297    /// platform async for the whole Trine operation.
298    ///
299    /// This includes partial native async, thread-pool managed async, blocking
300    /// fallback, and unsupported completions. It excludes
301    /// [`Self::true_platform_async`].
302    #[must_use]
303    pub fn non_true_platform_async_total(self) -> u64 {
304        self.platform_native_async_but_partial
305            .saturating_add(self.thread_pool_managed_async)
306            .saturating_add(self.blocking_fallback)
307            .saturating_add(self.unsupported)
308    }
309
310    /// Returns the number of platform-driver completions that were not true
311    /// platform async for the whole Trine operation.
312    ///
313    /// Prefer [`Self::non_true_platform_async_total`] in new code; this helper
314    /// remains as a compatibility alias.
315    #[must_use]
316    pub fn fallback_total(self) -> u64 {
317        self.non_true_platform_async_total()
318    }
319
320    /// Returns whether at least one completion was true platform async for the
321    /// whole Trine operation.
322    #[must_use]
323    pub fn uses_true_platform_async(self) -> bool {
324        self.true_platform_async > 0
325    }
326
327    /// Returns whether at least one completion was not true platform async for
328    /// the whole Trine operation.
329    #[must_use]
330    pub fn uses_non_true_platform_async(self) -> bool {
331        self.non_true_platform_async_total() > 0
332    }
333
334    /// Returns whether at least one completion was not true platform async for
335    /// the whole Trine operation.
336    ///
337    /// Prefer [`Self::uses_non_true_platform_async`] in new code; this helper
338    /// remains as a compatibility alias.
339    #[must_use]
340    pub fn uses_fallback(self) -> bool {
341        self.uses_non_true_platform_async()
342    }
343
344    /// Returns whether at least one completion reached an unsupported platform
345    /// operation.
346    #[must_use]
347    pub fn has_unsupported(self) -> bool {
348        self.unsupported > 0
349    }
350
351    fn saturating_add_assign(&mut self, other: Self) {
352        self.true_platform_async = self
353            .true_platform_async
354            .saturating_add(other.true_platform_async);
355        self.platform_native_async_but_partial = self
356            .platform_native_async_but_partial
357            .saturating_add(other.platform_native_async_but_partial);
358        self.thread_pool_managed_async = self
359            .thread_pool_managed_async
360            .saturating_add(other.thread_pool_managed_async);
361        self.blocking_fallback = self
362            .blocking_fallback
363            .saturating_add(other.blocking_fallback);
364        self.unsupported = self.unsupported.saturating_add(other.unsupported);
365    }
366}
367
368/// Per-operation platform I/O capability-class counters.
369///
370/// This table uses Trine operation names instead of OS API names. It lets
371/// diagnostics distinguish, for example, random reads from directory listing
372/// even when both are routed through the same selected platform driver.
373///
374/// Use [`Self::total`] to summarize all operations into one class counter set
375/// for dashboards, health checks, or tests that only need to know whether any
376/// platform I/O work used true async, partial native async, thread-pool
377/// managed async, blocking fallback, or unsupported classes.
378///
379/// # Examples
380///
381/// ```
382/// use trine_kv::{PlatformIoClassCounters, PlatformIoOperationStats};
383///
384/// let stats = PlatformIoOperationStats {
385///     random_read: PlatformIoClassCounters {
386///         true_platform_async: 4,
387///         ..PlatformIoClassCounters::default()
388///     },
389///     directory_listing: PlatformIoClassCounters {
390///         thread_pool_managed_async: 1,
391///         ..PlatformIoClassCounters::default()
392///     },
393///     ..PlatformIoOperationStats::default()
394/// };
395///
396/// let total = stats.total();
397/// assert_eq!(total.total(), 5);
398/// assert!(total.uses_true_platform_async());
399/// assert!(total.uses_non_true_platform_async());
400/// ```
401#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
402pub struct PlatformIoOperationStats {
403    /// Length lookups for readable native-file objects.
404    pub length_lookup: PlatformIoClassCounters,
405    /// Owned-buffer positioned reads.
406    pub random_read: PlatformIoClassCounters,
407    /// Whole-object reads, including optional reads for manifests and tables.
408    pub whole_object_read: PlatformIoClassCounters,
409    /// Temporary-file writes followed by rename publish.
410    pub temp_write_rename_publish: PlatformIoClassCounters,
411    /// Opens of appendable WAL objects.
412    pub append_open: PlatformIoClassCounters,
413    /// Appends to WAL or appendable storage objects.
414    pub append: PlatformIoClassCounters,
415    /// Persistence requests such as flush, data sync, or full sync.
416    pub persist: PlatformIoClassCounters,
417    /// WAL rewrite operations that publish a replacement WAL file.
418    pub wal_rewrite: PlatformIoClassCounters,
419    /// Object delete requests.
420    pub delete: PlatformIoClassCounters,
421    /// Directory creation requests.
422    pub directory_create: PlatformIoClassCounters,
423    /// Directory sync requests after rename publication.
424    pub directory_sync: PlatformIoClassCounters,
425    /// Directory or object listing requests.
426    pub directory_listing: PlatformIoClassCounters,
427    /// Writer-lease acquisition requests.
428    pub writer_lease: PlatformIoClassCounters,
429}
430
431impl PlatformIoOperationStats {
432    /// Returns class counters summed across every Trine platform I/O operation.
433    ///
434    /// Each field in the returned value is a task count. The sums saturate at
435    /// `u64::MAX`, matching [`PlatformIoClassCounters::total`], so callers can
436    /// safely aggregate snapshots from long-lived processes without wrapping.
437    #[must_use]
438    pub fn total(self) -> PlatformIoClassCounters {
439        let mut total = PlatformIoClassCounters::default();
440        total.saturating_add_assign(self.length_lookup);
441        total.saturating_add_assign(self.random_read);
442        total.saturating_add_assign(self.whole_object_read);
443        total.saturating_add_assign(self.temp_write_rename_publish);
444        total.saturating_add_assign(self.append_open);
445        total.saturating_add_assign(self.append);
446        total.saturating_add_assign(self.persist);
447        total.saturating_add_assign(self.wal_rewrite);
448        total.saturating_add_assign(self.delete);
449        total.saturating_add_assign(self.directory_create);
450        total.saturating_add_assign(self.directory_sync);
451        total.saturating_add_assign(self.directory_listing);
452        total.saturating_add_assign(self.writer_lease);
453        total
454    }
455}
456
457#[derive(Debug, Default)]
458pub(crate) struct BlobReadMetrics {
459    count: AtomicU64,
460    bytes: AtomicU64,
461}
462
463impl BlobReadMetrics {
464    pub(crate) fn record(&self, bytes: u64) {
465        self.count.fetch_add(1, Ordering::Relaxed);
466        self.bytes.fetch_add(bytes, Ordering::Relaxed);
467    }
468
469    pub(crate) fn snapshot(&self) -> (u64, u64) {
470        (
471            self.count.load(Ordering::Acquire),
472            self.bytes.load(Ordering::Acquire),
473        )
474    }
475}
476
477/// Range/prefix scan GC-waste counters: how many internal version records the
478/// merge had to handle relative to the user keys it returned, and how many
479/// user-key groups were hidden by a delete. The north-star read-amplification
480/// ratio is `internal_records / user_keys`.
481#[derive(Debug, Default)]
482pub(crate) struct ScanWasteMetrics {
483    internal_records: AtomicU64,
484    user_keys: AtomicU64,
485    tombstone_hidden_keys: AtomicU64,
486}
487
488impl ScanWasteMetrics {
489    /// Records one resolved user-key version group: `group_records` versions
490    /// merged, and the outcome (a visible row, a delete-hidden row, or nothing).
491    pub(crate) fn record_group(&self, group_records: u64, outcome: ScanGroupOutcome) {
492        self.internal_records
493            .fetch_add(group_records, Ordering::Relaxed);
494        match outcome {
495            ScanGroupOutcome::Visible => {
496                self.user_keys.fetch_add(1, Ordering::Relaxed);
497            }
498            ScanGroupOutcome::HiddenByDelete => {
499                self.tombstone_hidden_keys.fetch_add(1, Ordering::Relaxed);
500            }
501            ScanGroupOutcome::NoVisibleVersion => {}
502        }
503    }
504
505    pub(crate) fn snapshot(&self) -> ScanWasteSnapshot {
506        ScanWasteSnapshot {
507            internal_records: self.internal_records.load(Ordering::Acquire),
508            user_keys: self.user_keys.load(Ordering::Acquire),
509            tombstone_hidden_keys: self.tombstone_hidden_keys.load(Ordering::Acquire),
510        }
511    }
512}
513
514/// Outcome of resolving one user-key version group during a scan.
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub(crate) enum ScanGroupOutcome {
517    /// A visible row was returned for the user key.
518    Visible,
519    /// The user key existed but was hidden by a point or range delete.
520    HiddenByDelete,
521    /// No version of the user key was visible to the read sequence.
522    NoVisibleVersion,
523}
524
525#[derive(Debug, Clone, Copy, Default)]
526pub(crate) struct ScanWasteSnapshot {
527    pub(crate) internal_records: u64,
528    pub(crate) user_keys: u64,
529    pub(crate) tombstone_hidden_keys: u64,
530}
531
532/// Table count and byte size for one LSM level.
533#[derive(Debug, Clone, Default, PartialEq, Eq)]
534pub struct LevelStats {
535    /// LSM level number.
536    pub level: u32,
537    /// Number of table files in the level.
538    pub tables: usize,
539    /// Total bytes in the level's table files.
540    pub bytes: u64,
541}
542
543/// Per-level table and byte totals read and written by compaction.
544#[derive(Debug, Clone, Default, PartialEq, Eq)]
545pub struct CompactionLevelStats {
546    /// LSM level number.
547    pub level: u32,
548    /// Number of input table files read from this level.
549    pub input_tables: u64,
550    /// Number of output table files written to this level.
551    pub output_tables: u64,
552    /// Input table bytes read from this level.
553    pub input_bytes: u64,
554    /// Output table bytes written to this level.
555    pub output_bytes: u64,
556}
557
558/// Reason the compaction picker selected a compaction input.
559#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
560pub enum CompactionTrigger {
561    /// Level-0 tables overlapped each other or the next level and needed to be
562    /// closed into a safe output range.
563    L0Overlap,
564    /// A non-level-0 table level was above its configured byte target.
565    LevelSize,
566    /// No higher-priority trigger existed, but a shallow non-level-0 level had
567    /// multiple tables in the requested range that could be merged downward.
568    MultiTableLevel,
569    /// A non-bottom table carried range tombstones with overlapping lower-level
570    /// data, so it was compacted downward to let the tombstone meet and drop the
571    /// data it covers instead of lingering on the read path.
572    TombstoneDebt,
573}
574
575/// Per-trigger table and byte totals read and written by compaction.
576#[derive(Debug, Clone, PartialEq, Eq)]
577pub struct CompactionTriggerStats {
578    /// Picker reason shared by the compaction inputs counted in this row.
579    pub trigger: CompactionTrigger,
580    /// Number of compaction inputs selected for this reason.
581    pub runs: u64,
582    /// Number of input table files read for this reason.
583    pub input_tables: u64,
584    /// Number of output table files written for this reason.
585    pub output_tables: u64,
586    /// Input table bytes read for this reason.
587    pub input_bytes: u64,
588    /// Output table bytes written for this reason.
589    pub output_bytes: u64,
590}
591
592/// Reason the compaction picker deliberately left a level un-compacted.
593///
594/// This is the "did not run" complement to [`CompactionTrigger`]: it explains a
595/// non-uniform per-level policy decision rather than a compaction that happened.
596#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
597pub enum CompactionSkip {
598    /// A deeper non-level-0 level had enough non-overlapping tables that a
599    /// uniform picker would merge one downward, but the non-uniform policy left
600    /// it lazy. The level was within its depth-scaled file budget and no size,
601    /// tombstone, or blob trigger justified rewriting it. Because non-level-0
602    /// levels are non-overlapping, the extra tables add no point-read candidate
603    /// depth, so leaving them avoids write amplification without regressing
604    /// reads.
605    LowerLevelLazy,
606}
607
608/// Per-reason counts for compactions the picker deliberately did not run.
609#[derive(Debug, Clone, PartialEq, Eq)]
610pub struct CompactionSkipStats {
611    /// Policy reason shared by the skipped compaction decisions in this row.
612    pub skip: CompactionSkip,
613    /// Number of times the picker chose this lazy decision.
614    pub occurrences: u64,
615}
616
617/// Filter counters for table-level and block-level filters.
618#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
619pub struct FilterStats {
620    /// Table key-filter positive results for point reads.
621    pub table_point_hits: u64,
622    /// Table key-filter negative results for point reads.
623    pub table_point_misses: u64,
624    /// Table key-filter positives that did not contain the key.
625    pub table_point_false_positives: u64,
626    /// Table prefix-filter positive results for prefix reads.
627    pub table_prefix_hits: u64,
628    /// Table prefix-filter negative results for prefix reads.
629    pub table_prefix_misses: u64,
630    /// Table prefix-filter positives that did not contain the prefix.
631    pub table_prefix_false_positives: u64,
632    /// Block key-filter positive results for point reads.
633    pub block_point_hits: u64,
634    /// Block key-filter negative results for point reads.
635    pub block_point_misses: u64,
636    /// Block key-filter positives that did not contain the key.
637    pub block_point_false_positives: u64,
638    /// Block prefix-filter positive results for prefix reads.
639    pub block_prefix_hits: u64,
640    /// Block prefix-filter negative results for prefix reads.
641    pub block_prefix_misses: u64,
642    /// Block prefix-filter positives that did not contain the prefix.
643    pub block_prefix_false_positives: u64,
644}
645
646impl FilterStats {
647    /// Observed point false-positive rate: filter-allowed probes that turned out
648    /// not to contain the key, over all filter-allowed absent probes (false
649    /// positives plus correct negatives). Returns `None` when no absent probe
650    /// exercised the table point filter, so callers do not divide by zero.
651    #[must_use]
652    pub fn table_point_false_positive_rate(&self) -> Option<f64> {
653        let allowed_absent = self
654            .table_point_false_positives
655            .saturating_add(self.table_point_misses);
656        if allowed_absent == 0 {
657            return None;
658        }
659        #[allow(clippy::cast_precision_loss)]
660        Some(self.table_point_false_positives as f64 / allowed_absent as f64)
661    }
662}
663
664/// Per-level aggregation of table filter counters.
665///
666/// Filter counters are recorded per table; this rolls them up by LSM level so
667/// layered (Monkey-style) filter allocation can see where false positives
668/// actually concentrate before any per-level `bits_per_key` change.
669#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
670pub struct LevelFilterStats {
671    /// LSM level these filter counters belong to.
672    pub level: u32,
673    /// Number of table files on this level that contributed counters.
674    pub tables: usize,
675    /// Table and block filter counters summed over this level's tables.
676    pub filters: FilterStats,
677    /// Resident Bloom filter bytes held in memory for this level's tables
678    /// (table-level plus resident data-block filters). This is the layered
679    /// (Monkey-style) allocation memory metric; deeper levels with a lower
680    /// per-key budget hold fewer filter bytes per key.
681    pub filter_resident_bytes: u64,
682}
683
684/// Read-path counters that describe how far reads travel through table metadata.
685#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
686pub struct ReadPathStats {
687    /// Table files considered by point reads.
688    pub point_table_probes: u64,
689    /// Level-0 table files considered by point reads.
690    pub point_l0_table_probes: u64,
691    /// Non-level-0 table files considered by point reads.
692    pub point_non_l0_table_probes: u64,
693    /// Point lookup keys that considered at least one level-0 table.
694    pub point_l0_lookup_keys: u64,
695    /// Extra level-0 table probes after the first level-0 probe for a point key.
696    pub point_l0_overlap_extra_table_probes: u64,
697    /// Input keys accepted by grouped point-batch reads.
698    pub batch_point_input_keys: u64,
699    /// Unique keys planned by grouped point-batch reads.
700    pub batch_point_unique_keys: u64,
701    /// Table groups visited by grouped point-batch reads.
702    pub batch_point_table_groups: u64,
703    /// Unique grouped point-batch keys that considered at least one level-0 table.
704    pub batch_point_l0_lookup_keys: u64,
705    /// Extra level-0 table probes after the first probe for grouped point-batch keys.
706    pub batch_point_l0_overlap_extra_table_probes: u64,
707    /// Index partitions considered by point reads.
708    pub point_index_partition_probes: u64,
709    /// Data-block metadata entries considered by point reads.
710    pub point_block_metadata_probes: u64,
711    /// Data blocks read by point reads.
712    pub point_data_block_reads: u64,
713    /// Point reads skipped because filters ruled out a table or block.
714    pub point_filter_misses: u64,
715    /// Table files considered by range scans.
716    pub range_table_probes: u64,
717    /// Level-0 table files considered by range scans.
718    pub range_l0_table_probes: u64,
719    /// Non-level-0 table files considered by range scans.
720    pub range_non_l0_table_probes: u64,
721    /// Table files inspected for range tombstones during range scans.
722    pub range_tombstone_table_probes: u64,
723    /// Table files considered by prefix scans.
724    pub prefix_table_probes: u64,
725    /// Table files inspected for range tombstones during prefix scans.
726    pub prefix_tombstone_table_probes: u64,
727    /// Data-block metadata entries considered by prefix scans.
728    pub prefix_block_metadata_probes: u64,
729    /// Data blocks read by prefix scans.
730    pub prefix_data_block_reads: u64,
731    /// Prefix scan work skipped because filters ruled out a table or block.
732    pub prefix_filter_misses: u64,
733}
734
735impl ReadPathStats {
736    pub(crate) fn saturating_add_assign(&mut self, other: Self) {
737        self.point_table_probes = self
738            .point_table_probes
739            .saturating_add(other.point_table_probes);
740        self.point_l0_table_probes = self
741            .point_l0_table_probes
742            .saturating_add(other.point_l0_table_probes);
743        self.point_non_l0_table_probes = self
744            .point_non_l0_table_probes
745            .saturating_add(other.point_non_l0_table_probes);
746        self.point_l0_lookup_keys = self
747            .point_l0_lookup_keys
748            .saturating_add(other.point_l0_lookup_keys);
749        self.point_l0_overlap_extra_table_probes = self
750            .point_l0_overlap_extra_table_probes
751            .saturating_add(other.point_l0_overlap_extra_table_probes);
752        self.batch_point_input_keys = self
753            .batch_point_input_keys
754            .saturating_add(other.batch_point_input_keys);
755        self.batch_point_unique_keys = self
756            .batch_point_unique_keys
757            .saturating_add(other.batch_point_unique_keys);
758        self.batch_point_table_groups = self
759            .batch_point_table_groups
760            .saturating_add(other.batch_point_table_groups);
761        self.batch_point_l0_lookup_keys = self
762            .batch_point_l0_lookup_keys
763            .saturating_add(other.batch_point_l0_lookup_keys);
764        self.batch_point_l0_overlap_extra_table_probes = self
765            .batch_point_l0_overlap_extra_table_probes
766            .saturating_add(other.batch_point_l0_overlap_extra_table_probes);
767        self.point_index_partition_probes = self
768            .point_index_partition_probes
769            .saturating_add(other.point_index_partition_probes);
770        self.point_block_metadata_probes = self
771            .point_block_metadata_probes
772            .saturating_add(other.point_block_metadata_probes);
773        self.point_data_block_reads = self
774            .point_data_block_reads
775            .saturating_add(other.point_data_block_reads);
776        self.point_filter_misses = self
777            .point_filter_misses
778            .saturating_add(other.point_filter_misses);
779        self.range_table_probes = self
780            .range_table_probes
781            .saturating_add(other.range_table_probes);
782        self.range_l0_table_probes = self
783            .range_l0_table_probes
784            .saturating_add(other.range_l0_table_probes);
785        self.range_non_l0_table_probes = self
786            .range_non_l0_table_probes
787            .saturating_add(other.range_non_l0_table_probes);
788        self.range_tombstone_table_probes = self
789            .range_tombstone_table_probes
790            .saturating_add(other.range_tombstone_table_probes);
791        self.prefix_table_probes = self
792            .prefix_table_probes
793            .saturating_add(other.prefix_table_probes);
794        self.prefix_tombstone_table_probes = self
795            .prefix_tombstone_table_probes
796            .saturating_add(other.prefix_tombstone_table_probes);
797        self.prefix_block_metadata_probes = self
798            .prefix_block_metadata_probes
799            .saturating_add(other.prefix_block_metadata_probes);
800        self.prefix_data_block_reads = self
801            .prefix_data_block_reads
802            .saturating_add(other.prefix_data_block_reads);
803        self.prefix_filter_misses = self
804            .prefix_filter_misses
805            .saturating_add(other.prefix_filter_misses);
806    }
807}
808
809impl FilterStats {
810    pub(crate) fn saturating_add_assign(&mut self, other: Self) {
811        self.table_point_hits = self.table_point_hits.saturating_add(other.table_point_hits);
812        self.table_point_misses = self
813            .table_point_misses
814            .saturating_add(other.table_point_misses);
815        self.table_point_false_positives = self
816            .table_point_false_positives
817            .saturating_add(other.table_point_false_positives);
818        self.table_prefix_hits = self
819            .table_prefix_hits
820            .saturating_add(other.table_prefix_hits);
821        self.table_prefix_misses = self
822            .table_prefix_misses
823            .saturating_add(other.table_prefix_misses);
824        self.table_prefix_false_positives = self
825            .table_prefix_false_positives
826            .saturating_add(other.table_prefix_false_positives);
827        self.block_point_hits = self.block_point_hits.saturating_add(other.block_point_hits);
828        self.block_point_misses = self
829            .block_point_misses
830            .saturating_add(other.block_point_misses);
831        self.block_point_false_positives = self
832            .block_point_false_positives
833            .saturating_add(other.block_point_false_positives);
834        self.block_prefix_hits = self
835            .block_prefix_hits
836            .saturating_add(other.block_prefix_hits);
837        self.block_prefix_misses = self
838            .block_prefix_misses
839            .saturating_add(other.block_prefix_misses);
840        self.block_prefix_false_positives = self
841            .block_prefix_false_positives
842            .saturating_add(other.block_prefix_false_positives);
843    }
844}
845
846#[cfg(test)]
847mod tests {
848    use super::{FilterStats, PlatformIoClassCounters, PlatformIoOperationStats};
849
850    #[test]
851    fn table_point_false_positive_rate_uses_allowed_absent_probes() {
852        // 1 false positive out of 1 false positive + 3 correct negatives = 0.25.
853        let stats = FilterStats {
854            table_point_false_positives: 1,
855            table_point_misses: 3,
856            ..FilterStats::default()
857        };
858        assert_eq!(stats.table_point_false_positive_rate(), Some(0.25));
859    }
860
861    #[test]
862    fn table_point_false_positive_rate_is_none_without_absent_probes() {
863        let stats = FilterStats {
864            table_point_hits: 10,
865            ..FilterStats::default()
866        };
867        assert_eq!(stats.table_point_false_positive_rate(), None);
868    }
869
870    #[test]
871    fn platform_io_class_counter_helpers_summarize_classes() {
872        let counters = PlatformIoClassCounters {
873            true_platform_async: 2,
874            platform_native_async_but_partial: 3,
875            thread_pool_managed_async: 5,
876            blocking_fallback: 7,
877            unsupported: 11,
878        };
879
880        assert_eq!(counters.total(), 28);
881        assert_eq!(counters.non_true_platform_async_total(), 26);
882        assert_eq!(counters.fallback_total(), 26);
883        assert!(!counters.is_empty());
884        assert!(counters.uses_true_platform_async());
885        assert!(counters.uses_non_true_platform_async());
886        assert!(counters.uses_fallback());
887        assert!(counters.has_unsupported());
888        assert!(PlatformIoClassCounters::default().is_empty());
889    }
890
891    #[test]
892    fn platform_io_operation_stats_total_saturates_by_class() {
893        let stats = PlatformIoOperationStats {
894            length_lookup: PlatformIoClassCounters {
895                true_platform_async: u64::MAX,
896                thread_pool_managed_async: 1,
897                ..PlatformIoClassCounters::default()
898            },
899            random_read: PlatformIoClassCounters {
900                true_platform_async: 1,
901                platform_native_async_but_partial: 2,
902                blocking_fallback: 3,
903                unsupported: 4,
904                ..PlatformIoClassCounters::default()
905            },
906            directory_listing: PlatformIoClassCounters {
907                blocking_fallback: 5,
908                ..PlatformIoClassCounters::default()
909            },
910            ..PlatformIoOperationStats::default()
911        };
912
913        let total = stats.total();
914        assert_eq!(total.true_platform_async, u64::MAX);
915        assert_eq!(total.platform_native_async_but_partial, 2);
916        assert_eq!(total.thread_pool_managed_async, 1);
917        assert_eq!(total.blocking_fallback, 8);
918        assert_eq!(total.unsupported, 4);
919    }
920}