chisel/stats.rs
1// stats.rs — Maintenance layer (layer 7). A plain snapshot struct returned
2// by Chisel::stats() for observability: handle count, page count, and raw
3// file size. Defined as its own module so that lib.rs and the public API
4// don't have to pull in transaction.rs just to expose these three numbers.
5//
6// This is a snapshot, not a live view — callers should not cache it across
7// commits. Values reflect the state at the time stats() was called.
8
9/// Read-only summary of database size/usage. Populated by the transaction
10/// manager; no methods here because there are no invariants to enforce.
11///
12/// I36 (ISSUES.md, 2026-05-22): `#[non_exhaustive]` so adding a fifth
13/// summary field (e.g. live-handle/total-handle ratio for retirement
14/// pressure) is not a breaking change. The companion `ChiselCounters`
15/// has carried the same attribute since the bench-suite series landed.
16#[non_exhaustive]
17#[derive(Debug, Clone)]
18pub struct Stats {
19 /// Number of live handles (u64 ids currently mapped in the handle table).
20 pub handle_count: u64,
21 /// Total allocated pages in the file, matching Superblock.total_pages.
22 pub total_pages: u64,
23 /// Raw size of the database file on disk. May exceed
24 /// `total_pages * PAGE_SIZE` when a previous crash left orphan
25 /// pages in the file tail — the last-durable superblock's
26 /// `total_pages` is authoritative, anything beyond it is dead
27 /// weight that the next allocation will overwrite (see I4).
28 /// Chisel is single-writer, so there is no concurrent commit
29 /// that could cause a transient divergence.
30 pub file_size_bytes: u64,
31 /// I74 (ISSUES.md, 2026-05-22): current spillway logical-bytes in
32 /// flight (`PAGE_SIZE` × LIVE resident spilled pages — a page read back
33 /// and respilled within a transaction is not double-counted). `None`
34 /// when the spillway has never been opened — it is lazily
35 /// constructed on the first overflow spill, so `None` means "no
36 /// overflow has happened yet on this handle." `Some(0)` means
37 /// "spillway exists but is empty (just truncated by commit /
38 /// rollback)." The distinction matters for monitoring: a
39 /// long-lived `None` says "this workload fits comfortably in
40 /// cache," whereas `Some(0)` says "we have spilled before, might
41 /// again."
42 pub spillway_logical_bytes: Option<u64>,
43 /// I74: the spillway's `max_bytes` cap. Same `None`-vs-`Some(0)`
44 /// distinction as `spillway_logical_bytes`. Operators predict
45 /// `SpillwayFull` by watching `spillway_logical_bytes / spillway_max_bytes`
46 /// climb across commits.
47 pub spillway_max_bytes: Option<u64>,
48}
49
50/// Cumulative engine-activity counters since `open()`.
51///
52/// Snapshot semantics: `Chisel::counters()` returns a value-type copy. The
53/// returned struct does NOT update as the engine continues to do work — read
54/// it again to observe new totals. Counters are cumulative from open; they
55/// reset implicitly on `close()` + reopen because the underlying `PageCache`
56/// and `PageIo` are reconstructed.
57///
58/// Intended use: the bench harness reads `counters()` before and after each
59/// measurement, reports the delta. General-purpose introspection (debugging,
60/// observability) is also supported — the counters are cheap (`Cell<u64>`
61/// increment in single-writer code paths).
62///
63/// Fields:
64/// - `cache_hits` — `PageCache::get` returned a cached page without disk I/O.
65/// - `cache_misses` — `PageCache::get` had to load from disk (and validate
66/// checksum). Hit rate is `hits / (hits + misses)`.
67/// - `pages_allocated` — page allocations, counting BOTH file extensions
68/// (`PageCache::new_page`, a new id past the high-water mark) AND freemap
69/// reuses (`PageCache::claim_page`, a page id freed by a prior committed
70/// transaction and handed back out). Reuse is the common case once the
71/// handle table / membership index allocate COW pages through the
72/// freemap-aware path, so a counter that ignored it would read ~0 for a
73/// steady-state mutating workload. The actual disk write happens on the
74/// next `flush()`.
75/// - `fsync_calls` — `PageIo::fsync` invocations that SUCCEEDED. Three per
76/// Chisel commit (pre-drain flush, data-page flush, then superblock fsync);
77/// zero between commits in a normal txn. A failed fsync poisons the engine (I1 / fsyncgate) and
78/// is not counted; `cache_misses` by contrast counts attempted misses
79/// even when the subsequent load fails. The asymmetry is intentional —
80/// see `PageIo::fsync` and I1 in ISSUES.md for the rationale.
81#[non_exhaustive]
82#[derive(Debug, Clone, Default, PartialEq, Eq)]
83pub struct ChiselCounters {
84 pub cache_hits: u64,
85 pub cache_misses: u64,
86 pub pages_allocated: u64,
87 pub fsync_calls: u64,
88}