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}