Skip to main content

noxu_engine/
env_stats.rs

1//! Aggregated environment statistics.
2
3use noxu_cleaner::CleanerStatsSnapshot;
4use noxu_dbi::ThroughputStatsSnapshot;
5use noxu_evictor::EvictorStats;
6use noxu_log::LogManagerStats;
7use noxu_recovery::CheckpointStatsSnapshot;
8use noxu_txn::{LockStats, TxnStats};
9use std::sync::atomic::Ordering;
10
11/// Aggregated statistics for the environment.
12///
13/// Collects statistics from all subsystems into a single snapshot
14/// for convenient reporting and monitoring.  Mirrors 's
15/// `EnvironmentStats` grouping: Cache, Evictor, Log, Lock, Txn,
16/// Cleaner, Checkpoint, Throughput.
17#[derive(Debug, Clone)]
18pub struct EnvironmentStats {
19    // ── Cache ──────────────────────────────────────────────────────────────
20    /// Total cache size (budget) in bytes.
21    pub cache_size: u64,
22    /// Current cache usage in bytes.
23    pub cache_usage: u64,
24
25    // ── B-tree node counts ─────────────────────────────────────────────────
26    /// Number of currently open databases.
27    pub n_databases: u32,
28
29    // ── Evictor ───────────────────────────────────────────────────────────
30    pub evictor: EvictorStatsSnapshot,
31
32    // ── Log / FileManager / FsyncManager ──────────────────────────────────
33    pub log: LogStatsSnapshot,
34
35    // ── Lock manager ──────────────────────────────────────────────────────
36    pub lock: LockStatsSnapshot,
37
38    // ── Transaction manager ───────────────────────────────────────────────
39    pub txn: TxnStatsSnapshot,
40
41    // ── Cleaner ───────────────────────────────────────────────────────────
42    pub cleaner: CleanerStatsSnapshot,
43
44    // ── Checkpointer ──────────────────────────────────────────────────────
45    pub checkpoint: CheckpointStatsSnapshot,
46
47    // ── Throughput ────────────────────────────────────────────────────────
48    pub throughput: ThroughputStatsSnapshot,
49}
50
51impl EnvironmentStats {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    // ── Convenience aggregators ────────────────────────────────────────────
57
58    pub fn cache_utilization_percent(&self) -> f64 {
59        if self.cache_size == 0 {
60            0.0
61        } else {
62            (self.cache_usage as f64 / self.cache_size as f64) * 100.0
63        }
64    }
65
66    pub fn is_cache_over_budget(&self) -> bool {
67        self.cache_usage > self.cache_size
68    }
69
70    pub fn total_eviction_runs(&self) -> u64 {
71        self.evictor.eviction_runs
72    }
73
74    pub fn total_nodes_evicted(&self) -> u64 {
75        self.evictor.nodes_evicted
76    }
77
78    pub fn total_bytes_evicted(&self) -> u64 {
79        self.evictor.bytes_evicted
80    }
81
82    pub fn total_cleaning_runs(&self) -> u64 {
83        self.cleaner.runs
84    }
85
86    pub fn total_files_cleaned(&self) -> u64 {
87        self.cleaner.deletions
88    }
89
90    pub fn total_checkpoint_runs(&self) -> u64 {
91        self.checkpoint.checkpoints
92    }
93
94    pub fn total_full_ins_checkpointed(&self) -> u64 {
95        self.checkpoint.full_in_flush
96    }
97
98    /// Bin fetch miss ratio (0.0 – 1.0).
99    pub fn bin_fetch_miss_ratio(&self) -> f64 {
100        let total = self.evictor.bin_fetch;
101        if total == 0 {
102            0.0
103        } else {
104            self.evictor.bin_fetch_miss as f64 / total as f64
105        }
106    }
107}
108
109impl Default for EnvironmentStats {
110    fn default() -> Self {
111        Self {
112            cache_size: 0,
113            cache_usage: 0,
114            n_databases: 0,
115            evictor: EvictorStatsSnapshot::default(),
116            log: LogStatsSnapshot::default(),
117            lock: LockStatsSnapshot::default(),
118            txn: TxnStatsSnapshot::default(),
119            cleaner: CleanerStatsSnapshot {
120                runs: 0,
121                two_pass_runs: 0,
122                revisal_runs: 0,
123                deletions: 0,
124                entries_read: 0,
125                disk_reads: 0,
126                ins_cleaned: 0,
127                ins_dead: 0,
128                ins_migrated: 0,
129                ins_obsolete: 0,
130                lns_cleaned: 0,
131                lns_dead: 0,
132                lns_migrated: 0,
133                lns_obsolete: 0,
134                lns_locked: 0,
135                lns_marked: 0,
136                lns_expired: 0,
137                lnqueue_hits: 0,
138                pending_lns_processed: 0,
139                pending_lns_locked: 0,
140                cluster_lns_processed: 0,
141                marked_lns_processed: 0,
142                to_be_cleaned_lns_processed: 0,
143                bin_deltas_cleaned: 0,
144                bin_deltas_dead: 0,
145                bin_deltas_migrated: 0,
146                bin_deltas_obsolete: 0,
147                pending_ln_queue_size: 0,
148                total_log_size: 0,
149                active_log_size: 0,
150                reserved_log_size: 0,
151                protected_log_size: 0,
152                available_log_size: 0,
153                min_utilization: 0,
154                max_utilization: 0,
155                probe_runs: 0,
156                repeat_iterator_reads: 0,
157            },
158            checkpoint: CheckpointStatsSnapshot {
159                checkpoints: 0,
160                full_in_flush: 0,
161                full_bin_flush: 0,
162                delta_in_flush: 0,
163                last_ckpt_id: 0,
164                last_ckpt_start: 0,
165                last_ckpt_end: 0,
166                last_ckpt_interval: 0,
167            },
168            throughput: ThroughputStatsSnapshot::default(),
169        }
170    }
171}
172
173// ═══════════════════════════════════════════════════════════════════════════
174// EvictorStatsSnapshot — all fields from EvictorStats
175// ═══════════════════════════════════════════════════════════════════════════
176
177/// Full snapshot of evictor statistics.
178///
179/// EvictorStatDefinition.
180#[derive(Debug, Clone, Default)]
181pub struct EvictorStatsSnapshot {
182    // Eviction counts
183    pub eviction_runs: u64,
184    pub nodes_targeted: u64,
185    pub nodes_evicted: u64,
186    pub nodes_skipped: u64,
187    pub nodes_mutated: u64,
188    pub nodes_stripped: u64,
189    pub nodes_put_back: u64,
190    pub nodes_moved_to_pri2_lru: u64,
191    pub root_nodes_evicted: u64,
192    pub dirty_nodes_evicted: u64,
193    pub lns_evicted: u64,
194
195    // Bytes evicted by source
196    pub bytes_evicted_daemon: u64,
197    pub bytes_evicted_critical: u64,
198    pub bytes_evicted_manual: u64,
199    pub bytes_evicted_cachemode: u64,
200    /// Sum of all bytes_evicted_* fields.
201    pub bytes_evicted: u64,
202
203    // Fetch / miss statistics
204    pub bin_fetch: u64,
205    pub bin_fetch_miss: u64,
206    pub ln_fetch: u64,
207    pub ln_fetch_miss: u64,
208    pub upper_in_fetch: u64,
209    pub upper_in_fetch_miss: u64,
210    pub bin_delta_fetch_miss: u64,
211    pub full_bin_miss: u64,
212    pub bin_delta_blind_ops: u64,
213
214    // LRU sizes (instant)
215    pub pri1_lru_size: u64,
216    pub pri2_lru_size: u64,
217    /// Sum of pri1 + pri2.
218    pub lru_size: u64,
219
220    // Thread pool
221    /// Number of eviction tasks refused because all threads were busy.
222    pub thread_unavailable: u64,
223
224    // Cache composition (instant stats)
225    /// Number of upper INs in main cache.
226    pub cached_upper_ins: u64,
227    /// Number of BINs and BIN-deltas in main cache.
228    pub cached_bins: u64,
229    /// Number of BIN-deltas in main cache.
230    pub cached_bin_deltas: u64,
231    /// Number of INs using compact sparse array representation.
232    pub cached_in_sparse_target: u64,
233    /// Number of INs using compact no-child representation.
234    pub cached_in_no_target: u64,
235    /// Number of INs using compact key representation.
236    pub cached_in_compact_key: u64,
237}
238
239impl From<&EvictorStats> for EvictorStatsSnapshot {
240    fn from(stats: &EvictorStats) -> Self {
241        let d = stats.bytes_evicted_daemon.load(Ordering::Relaxed);
242        let c = stats.bytes_evicted_critical.load(Ordering::Relaxed);
243        let m = stats.bytes_evicted_manual.load(Ordering::Relaxed);
244        let cm = stats.bytes_evicted_cachemode.load(Ordering::Relaxed);
245        let p1 = stats.pri1_lru_size.load(Ordering::Relaxed);
246        let p2 = stats.pri2_lru_size.load(Ordering::Relaxed);
247        Self {
248            eviction_runs: stats.eviction_runs.load(Ordering::Relaxed),
249            nodes_targeted: stats.nodes_targeted.load(Ordering::Relaxed),
250            nodes_evicted: stats.nodes_evicted.load(Ordering::Relaxed),
251            nodes_skipped: stats.nodes_skipped.load(Ordering::Relaxed),
252            nodes_mutated: stats.nodes_mutated.load(Ordering::Relaxed),
253            nodes_stripped: stats.nodes_stripped.load(Ordering::Relaxed),
254            nodes_put_back: stats.nodes_put_back.load(Ordering::Relaxed),
255            nodes_moved_to_pri2_lru: stats
256                .nodes_moved_to_pri2_lru
257                .load(Ordering::Relaxed),
258            root_nodes_evicted: stats
259                .root_nodes_evicted
260                .load(Ordering::Relaxed),
261            dirty_nodes_evicted: stats
262                .dirty_nodes_evicted
263                .load(Ordering::Relaxed),
264            lns_evicted: stats.lns_evicted.load(Ordering::Relaxed),
265            bytes_evicted_daemon: d,
266            bytes_evicted_critical: c,
267            bytes_evicted_manual: m,
268            bytes_evicted_cachemode: cm,
269            bytes_evicted: d + c + m + cm,
270            bin_fetch: stats.bin_fetch.load(Ordering::Relaxed),
271            bin_fetch_miss: stats.bin_fetch_miss.load(Ordering::Relaxed),
272            ln_fetch: stats.ln_fetch.load(Ordering::Relaxed),
273            ln_fetch_miss: stats.ln_fetch_miss.load(Ordering::Relaxed),
274            upper_in_fetch: stats.upper_in_fetch.load(Ordering::Relaxed),
275            upper_in_fetch_miss: stats
276                .upper_in_fetch_miss
277                .load(Ordering::Relaxed),
278            bin_delta_fetch_miss: stats
279                .bin_delta_fetch_miss
280                .load(Ordering::Relaxed),
281            full_bin_miss: stats.full_bin_miss.load(Ordering::Relaxed),
282            bin_delta_blind_ops: stats
283                .bin_delta_blind_ops
284                .load(Ordering::Relaxed),
285            pri1_lru_size: p1,
286            pri2_lru_size: p2,
287            lru_size: p1 + p2,
288            thread_unavailable: stats
289                .thread_unavailable
290                .load(Ordering::Relaxed),
291            cached_upper_ins: stats.cached_upper_ins.load(Ordering::Relaxed),
292            cached_bins: stats.cached_bins.load(Ordering::Relaxed),
293            cached_bin_deltas: stats.cached_bin_deltas.load(Ordering::Relaxed),
294            cached_in_sparse_target: stats
295                .cached_in_sparse_target
296                .load(Ordering::Relaxed),
297            cached_in_no_target: stats
298                .cached_in_no_target
299                .load(Ordering::Relaxed),
300            cached_in_compact_key: stats
301                .cached_in_compact_key
302                .load(Ordering::Relaxed),
303        }
304    }
305}
306
307// ═══════════════════════════════════════════════════════════════════════════
308// LogStatsSnapshot — log manager + file manager + fsync manager
309// ═══════════════════════════════════════════════════════════════════════════
310
311/// Snapshot of log subsystem statistics.
312///
313/// LogStatDefinition (LOGMGR_*, FILEMGR_*, FSYNCMGR_*).
314#[derive(Debug, Clone, Default)]
315pub struct LogStatsSnapshot {
316    /// Current end-of-log LSN (as raw u64).
317    pub end_of_log: u64,
318    /// LSN of the last completed flush.
319    pub last_flush_lsn: u64,
320    /// Number of repeated fault reads (log entries re-read on cache miss).
321    pub n_repeat_fault_reads: u64,
322    /// Number of temporary-buffer writes (oversized entries).
323    pub n_temp_buffer_writes: u64,
324    /// Number of log buffers in the pool.
325    pub n_log_buffers: u64,
326    /// Total bytes across all log buffers.
327    pub n_log_buffer_bytes: u64,
328    /// Number of fdatasync calls completed (after group-commit coalescing).
329    pub n_log_fsyncs: u64,
330    /// Number of fdatasync requests (before coalescing).
331    pub n_fsync_requests: u64,
332    /// Number of log files opened (LRU cache miss).
333    pub n_file_opens: u64,
334    /// Number of sequential read operations.
335    pub n_sequential_reads: u64,
336    /// Total bytes read sequentially.
337    pub n_sequential_read_bytes: u64,
338    /// Number of sequential write operations.
339    pub n_sequential_writes: u64,
340    /// Total bytes written sequentially.
341    pub n_sequential_write_bytes: u64,
342    /// Number of random (point-lookup) read operations.
343    pub n_random_reads: u64,
344    /// Total bytes from random reads.
345    pub n_random_read_bytes: u64,
346    /// Number of fsync requests that timed out.
347    pub n_fsync_timeouts: u64,
348    /// Number of group-commit batches (leader served ≥1 waiter).
349    pub n_group_commits: u64,
350    /// Cumulative fsync duration in milliseconds.
351    pub fsync_time_ms: u64,
352    /// Sum of all group-commit batch sizes (total waiters served across all batches).
353    pub n_fsync_batch_size_sum: u64,
354}
355
356impl From<&LogManagerStats> for LogStatsSnapshot {
357    fn from(s: &LogManagerStats) -> Self {
358        Self {
359            end_of_log: s.end_of_log.as_u64(),
360            last_flush_lsn: s.last_flush_lsn.as_u64(),
361            n_repeat_fault_reads: s.n_repeat_fault_reads,
362            n_temp_buffer_writes: s.n_temp_buffer_writes,
363            n_log_buffers: s.buffer_pool_stats.num_buffers as u64,
364            n_log_buffer_bytes: (s.buffer_pool_stats.num_buffers as u64)
365                * (s.buffer_pool_stats.buffer_size as u64),
366            n_log_fsyncs: s.n_log_fsyncs,
367            n_fsync_requests: s.n_fsync_requests,
368            n_file_opens: s.n_file_opens,
369            n_sequential_reads: s.n_sequential_reads,
370            n_sequential_read_bytes: s.n_sequential_read_bytes,
371            n_sequential_writes: s.n_sequential_writes,
372            n_sequential_write_bytes: s.n_sequential_write_bytes,
373            n_random_reads: s.n_random_reads,
374            n_random_read_bytes: s.n_random_read_bytes,
375            n_fsync_timeouts: s.n_fsync_timeouts,
376            n_group_commits: s.n_group_commits,
377            fsync_time_ms: s.fsync_time_ms,
378            n_fsync_batch_size_sum: s.n_fsync_batch_size_sum,
379        }
380    }
381}
382
383// ═══════════════════════════════════════════════════════════════════════════
384// LockStatsSnapshot — lock manager statistics
385// ═══════════════════════════════════════════════════════════════════════════
386
387/// Snapshot of lock manager statistics.
388///
389/// LockStatDefinition.
390#[derive(Debug, Clone, Default)]
391pub struct LockStatsSnapshot {
392    /// Total lock requests.
393    pub n_requests: u64,
394    /// Number of requests that waited (blocked).
395    pub n_waits: u64,
396    /// Number of distinct lock objects held.
397    pub n_total_locks: u64,
398    /// Number of read-lock holders.
399    pub n_read_locks: u64,
400    /// Number of write-lock holders.
401    pub n_write_locks: u64,
402    /// Number of distinct lock owners.
403    pub n_owners: u64,
404    /// Number of lockers currently waiting.
405    pub n_waiters: u64,
406    /// Number of lock tables (shards).
407    pub n_lock_tables: u64,
408    /// Number of lock acquisitions that timed out.
409    pub n_lock_timeouts: u64,
410}
411
412impl From<&LockStats> for LockStatsSnapshot {
413    fn from(s: &LockStats) -> Self {
414        Self {
415            n_requests: s.lock_requests,
416            n_waits: s.lock_waits,
417            n_total_locks: s.n_total_locks,
418            n_read_locks: s.n_read_locks,
419            n_write_locks: s.n_write_locks,
420            n_owners: s.n_owners,
421            n_waiters: s.n_waiters,
422            n_lock_tables: 0, // filled in by engine.rs
423            n_lock_timeouts: s.n_lock_timeouts,
424        }
425    }
426}
427
428// ═══════════════════════════════════════════════════════════════════════════
429// TxnStatsSnapshot — transaction manager statistics
430// ═══════════════════════════════════════════════════════════════════════════
431
432/// Snapshot of transaction manager statistics.
433#[derive(Debug, Clone, Default)]
434pub struct TxnStatsSnapshot {
435    pub n_begins: u64,
436    pub n_commits: u64,
437    pub n_aborts: u64,
438    pub n_active: u64,
439}
440
441impl From<&TxnStats> for TxnStatsSnapshot {
442    fn from(s: &TxnStats) -> Self {
443        Self {
444            n_begins: s.n_begins,
445            n_commits: s.n_commits,
446            n_aborts: s.n_aborts,
447            n_active: s.n_active,
448        }
449    }
450}
451
452// ═══════════════════════════════════════════════════════════════════════════
453// Tests
454// ═══════════════════════════════════════════════════════════════════════════
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn test_new_stats() {
462        let stats = EnvironmentStats::new();
463        assert_eq!(stats.cache_size, 0);
464        assert_eq!(stats.cache_usage, 0);
465        assert_eq!(stats.n_databases, 0);
466    }
467
468    #[test]
469    fn test_cache_utilization() {
470        let mut stats = EnvironmentStats::new();
471        stats.cache_size = 1000;
472        stats.cache_usage = 500;
473        assert_eq!(stats.cache_utilization_percent(), 50.0);
474
475        stats.cache_usage = 1000;
476        assert_eq!(stats.cache_utilization_percent(), 100.0);
477
478        stats.cache_usage = 1200;
479        assert_eq!(stats.cache_utilization_percent(), 120.0);
480    }
481
482    #[test]
483    fn test_cache_utilization_zero_size() {
484        let mut stats = EnvironmentStats::new();
485        stats.cache_size = 0;
486        stats.cache_usage = 100;
487        assert_eq!(stats.cache_utilization_percent(), 0.0);
488    }
489
490    #[test]
491    fn test_is_cache_over_budget() {
492        let mut stats = EnvironmentStats::new();
493        stats.cache_size = 1000;
494        stats.cache_usage = 999;
495        assert!(!stats.is_cache_over_budget());
496        stats.cache_usage = 1001;
497        assert!(stats.is_cache_over_budget());
498    }
499
500    #[test]
501    fn test_evictor_aggregates() {
502        let mut stats = EnvironmentStats::new();
503        stats.evictor.eviction_runs = 10;
504        stats.evictor.nodes_evicted = 100;
505        stats.evictor.bytes_evicted = 10000;
506        assert_eq!(stats.total_eviction_runs(), 10);
507        assert_eq!(stats.total_nodes_evicted(), 100);
508        assert_eq!(stats.total_bytes_evicted(), 10000);
509    }
510
511    #[test]
512    fn test_cleaner_aggregates() {
513        let mut stats = EnvironmentStats::new();
514        stats.cleaner.runs = 5;
515        stats.cleaner.deletions = 20;
516        assert_eq!(stats.total_cleaning_runs(), 5);
517        assert_eq!(stats.total_files_cleaned(), 20);
518    }
519
520    #[test]
521    fn test_checkpoint_aggregates() {
522        let mut stats = EnvironmentStats::new();
523        stats.checkpoint.checkpoints = 3;
524        stats.checkpoint.full_in_flush = 50;
525        assert_eq!(stats.total_checkpoint_runs(), 3);
526        assert_eq!(stats.total_full_ins_checkpointed(), 50);
527    }
528
529    #[test]
530    fn test_log_stats_snapshot_default() {
531        let s = LogStatsSnapshot::default();
532        assert_eq!(s.n_log_fsyncs, 0);
533        assert_eq!(s.n_sequential_writes, 0);
534    }
535
536    #[test]
537    fn test_lock_stats_snapshot_default() {
538        let s = LockStatsSnapshot::default();
539        assert_eq!(s.n_requests, 0);
540        assert_eq!(s.n_total_locks, 0);
541    }
542
543    #[test]
544    fn test_txn_stats_snapshot_default() {
545        let s = TxnStatsSnapshot::default();
546        assert_eq!(s.n_begins, 0);
547        assert_eq!(s.n_active, 0);
548    }
549
550    #[test]
551    fn test_throughput_stats_snapshot_default() {
552        let s = ThroughputStatsSnapshot::default();
553        assert_eq!(s.n_pri_inserts, 0);
554        assert_eq!(s.n_pri_searches, 0);
555    }
556
557    #[test]
558    fn test_bin_fetch_miss_ratio_zero_denominator() {
559        let stats = EnvironmentStats::new();
560        assert_eq!(stats.bin_fetch_miss_ratio(), 0.0);
561    }
562
563    #[test]
564    fn test_bin_fetch_miss_ratio() {
565        let mut stats = EnvironmentStats::new();
566        stats.evictor.bin_fetch = 100;
567        stats.evictor.bin_fetch_miss = 25;
568        assert_eq!(stats.bin_fetch_miss_ratio(), 0.25);
569    }
570
571    #[test]
572    fn test_evictor_snapshot_all_fields() {
573        let snap = EvictorStatsSnapshot {
574            eviction_runs: 1,
575            nodes_targeted: 2,
576            nodes_evicted: 3,
577            nodes_skipped: 4,
578            nodes_mutated: 5,
579            nodes_stripped: 6,
580            nodes_put_back: 7,
581            nodes_moved_to_pri2_lru: 8,
582            root_nodes_evicted: 9,
583            dirty_nodes_evicted: 10,
584            lns_evicted: 11,
585            bytes_evicted_daemon: 100,
586            bytes_evicted_critical: 200,
587            bytes_evicted_manual: 300,
588            bytes_evicted_cachemode: 400,
589            bytes_evicted: 1000,
590            bin_fetch: 50,
591            bin_fetch_miss: 5,
592            ln_fetch: 40,
593            ln_fetch_miss: 4,
594            upper_in_fetch: 30,
595            upper_in_fetch_miss: 3,
596            bin_delta_fetch_miss: 2,
597            full_bin_miss: 1,
598            bin_delta_blind_ops: 20,
599            pri1_lru_size: 100,
600            pri2_lru_size: 200,
601            lru_size: 300,
602            thread_unavailable: 0,
603            cached_upper_ins: 10,
604            cached_bins: 20,
605            cached_bin_deltas: 5,
606            cached_in_sparse_target: 3,
607            cached_in_no_target: 2,
608            cached_in_compact_key: 1,
609        };
610        assert_eq!(snap.bytes_evicted, 1000);
611        assert_eq!(snap.lru_size, 300);
612        assert_eq!(snap.cached_bins, 20);
613    }
614
615    #[test]
616    fn test_default_stats() {
617        let stats = EnvironmentStats::default();
618        assert_eq!(stats.cache_size, 0);
619        assert_eq!(stats.n_databases, 0);
620        assert_eq!(stats.total_eviction_runs(), 0);
621        assert_eq!(stats.total_cleaning_runs(), 0);
622        assert_eq!(stats.total_checkpoint_runs(), 0);
623    }
624}