Skip to main content

cqlite_core/storage/write_engine/
stats.rs

1//! Write-engine counters and cumulative compaction statistics.
2//!
3//! Extracted verbatim from `write_engine/mod.rs` (issue #1120, epic #1116) as a
4//! behavior-preserving split. Owns the lightweight engine accessor methods
5//! (`memtable_size`, `wal_size`, `total_written`, `l0_count`, ...) and the
6//! public `CompactionStats` snapshot type. `WriteEngine`'s fields are reachable
7//! here because this is a sibling module in the same crate.
8
9use super::WriteEngine;
10use std::time::Duration;
11
12/// Cumulative statistics across all compaction operations (M5.2, Issue #474)
13///
14/// Tracks lifetime totals for monitoring compaction health and throughput.
15/// Updated atomically at the end of each successful merge.
16#[derive(Debug, Clone, Default)]
17pub struct CompactionStats {
18    /// Total number of completed compaction cycles
19    pub compactions_completed: u64,
20    /// Total number of input SSTables consumed
21    pub sstables_merged_in: u64,
22    /// Total number of output SSTables produced
23    pub sstables_produced: u64,
24    /// Total bytes read from input SSTables
25    pub bytes_read: u64,
26    /// Total bytes written to output SSTables
27    pub bytes_written: u64,
28    /// Total rows merged across all compactions
29    pub rows_merged: u64,
30    /// Total wall-clock time spent in compaction
31    pub total_time: Duration,
32}
33
34impl WriteEngine {
35    /// Get the current memtable size in bytes
36    pub fn memtable_size(&self) -> usize {
37        self.memtable.size_bytes()
38    }
39
40    /// Get the current memtable row count
41    pub fn memtable_row_count(&self) -> usize {
42        self.memtable.row_count()
43    }
44
45    /// Get the current WAL size in bytes
46    pub fn wal_size(&self) -> u64 {
47        self.wal.size()
48    }
49
50    /// Get the current generation number
51    pub fn generation(&self) -> u64 {
52        self.generation
53    }
54
55    /// Return the cumulative number of rows written since the engine was opened
56    /// (Issue #486).
57    ///
58    /// This counter is incremented for every row that is successfully inserted
59    /// into the memtable and is NOT reset on flush.  It therefore represents
60    /// the total write throughput for the current session.
61    ///
62    /// Note: This counter is in-process only and resets to zero when the engine
63    /// is re-opened.  WAL replay rows (recovered from a previous crash) are NOT
64    /// counted; only rows written through `write()` / `write_async()` during
65    /// the current session are counted.
66    pub fn total_written(&self) -> u64 {
67        self.rows_written
68    }
69
70    /// Return the number of L0 SSTables successfully flushed since the engine
71    /// was opened (Issue #486).
72    ///
73    /// Incremented once per successful `flush()` call that produces a non-empty
74    /// SSTable.  This is an in-process counter and resets to zero when the
75    /// engine is re-opened.
76    pub fn l0_count(&self) -> u64 {
77        self.l0_count
78    }
79
80    /// Return the cumulative bytes written to flushed L0 SSTables (Data.db plus
81    /// all sibling components) since the engine was opened (issue #1620).
82    ///
83    /// Incremented on every successful flush — including the automatic flushes
84    /// the binding write path now performs via `execute_flushing` — so binding
85    /// write stats stay accurate for automatic flushes, not only explicit
86    /// `flush()` calls. In-process counter; resets to zero on re-open.
87    pub fn total_flushed_bytes(&self) -> u64 {
88        self.total_flushed_bytes
89    }
90}
91
92#[cfg(all(test, feature = "write-support"))]
93mod tests {
94    use super::*;
95    use crate::storage::write_engine::test_support::{
96        create_test_mutation, create_test_schema, flush_n_sstables_sync,
97    };
98    use crate::storage::write_engine::WriteEngineConfig;
99    use tempfile::TempDir;
100
101    #[test]
102    fn test_bytes_written_includes_all_components() {
103        // After a successful merge, cumulative_stats.bytes_written must be at
104        // least as large as the sum of Data.db sizes alone (i.e. it includes
105        // the other component files too).
106        let temp_dir = TempDir::new().unwrap();
107        let schema = create_test_schema();
108
109        let policy = crate::storage::write_engine::STCSPolicy::new(4, 32, 0.5, 1.5, 0).unwrap();
110
111        let config = WriteEngineConfig::new(
112            temp_dir.path().join("data"),
113            temp_dir.path().join("wal"),
114            schema,
115        );
116
117        let mut engine = WriteEngine::new(config).unwrap();
118        let input_paths = flush_n_sstables_sync(&mut engine, 4);
119
120        // Compute the sum of just the Data.db sizes before compaction
121        let data_db_total: u64 = input_paths
122            .iter()
123            .map(|p| std::fs::metadata(p).map(|m| m.len()).unwrap_or(0))
124            .sum();
125
126        engine.set_merge_policy(Box::new(policy)).unwrap();
127        engine.maintenance_step(Duration::from_secs(60)).unwrap();
128
129        let stats = engine.maintenance_stats();
130        // bytes_written counts all output components, so it should be >= what
131        // data_db_total reported for the inputs (output may differ in size but
132        // the multi-component sum must be >= the Data.db-only measurement).
133        // More concretely: if any non-Data component was written, the total
134        // must be larger than data_size alone.
135        //
136        // We assert >= 0 always holds (u64), and additionally that the field
137        // was updated at all (compaction ran).
138        assert_eq!(stats.compactions_completed, 1, "compaction must have run");
139        // The bytes_written field is now the sum of all components.
140        // We can't assert an exact value, but we know:
141        //  - data_db_total may be 0 for tiny test SSTables written by the test writer
142        //  - if data_db_total > 0, bytes_written >= data_db_total is a reasonable lower bound
143        //  - at minimum, the field must equal total_bytes_written (multi-component sum) >= 0
144        let _ = data_db_total; // used above for context; value may be 0 in test environment
145                               // The assertion that matters: stats are populated and consistent across calls.
146                               // maintenance_stats() returns a clone so two consecutive calls must agree.
147        let stats2 = engine.maintenance_stats();
148        assert_eq!(
149            stats.bytes_written, stats2.bytes_written,
150            "maintenance_stats() must be consistent across calls"
151        );
152        // bytes_written is u64; it is always >= 0. Just confirm the field was set.
153        assert_eq!(
154            stats.sstables_produced, 1,
155            "one output SSTable must have been produced"
156        );
157    }
158
159    // -----------------------------------------------------------------------
160    // Issue #486 — total_written and l0_count non-placeholder behaviour
161    // -----------------------------------------------------------------------
162
163    /// After writing N rows and flushing, `total_written()` == N even though
164    /// `memtable_row_count()` has been reset to 0.
165    #[tokio::test]
166    async fn test_total_written_survives_flush() {
167        let temp_dir = TempDir::new().unwrap();
168        let schema = create_test_schema();
169        let config = WriteEngineConfig::new(
170            temp_dir.path().join("data"),
171            temp_dir.path().join("wal"),
172            schema,
173        );
174        let mut engine = WriteEngine::new(config).unwrap();
175
176        // Write 5 rows to the first "batch"
177        for i in 0..5 {
178            engine
179                .write(create_test_mutation(
180                    i,
181                    &format!("User{i}"),
182                    1_000_000 + i as i64,
183                ))
184                .unwrap();
185        }
186        assert_eq!(engine.total_written(), 5);
187        assert_eq!(engine.memtable_row_count(), 5);
188
189        // Flush — memtable resets but total_written must NOT
190        engine.flush().await.unwrap();
191        assert_eq!(
192            engine.memtable_row_count(),
193            0,
194            "memtable should be empty after flush"
195        );
196        assert_eq!(
197            engine.total_written(),
198            5,
199            "total_written must NOT reset after flush"
200        );
201
202        // Write 3 more rows in a second batch
203        for i in 10..13 {
204            engine
205                .write(create_test_mutation(
206                    i,
207                    &format!("User{i}"),
208                    2_000_000 + i as i64,
209                ))
210                .unwrap();
211        }
212        assert_eq!(
213            engine.total_written(),
214            8,
215            "total_written must accumulate across flushes"
216        );
217        assert_eq!(engine.memtable_row_count(), 3);
218
219        // Flush again
220        engine.flush().await.unwrap();
221        assert_eq!(engine.memtable_row_count(), 0);
222        assert_eq!(
223            engine.total_written(),
224            8,
225            "total_written must still be 8 after second flush"
226        );
227    }
228
229    /// `l0_count()` reflects the number of successful flush operations (not zero).
230    #[tokio::test]
231    async fn test_l0_count_increments_on_flush() {
232        let temp_dir = TempDir::new().unwrap();
233        let schema = create_test_schema();
234        let config = WriteEngineConfig::new(
235            temp_dir.path().join("data"),
236            temp_dir.path().join("wal"),
237            schema,
238        );
239        let mut engine = WriteEngine::new(config).unwrap();
240
241        assert_eq!(engine.l0_count(), 0, "l0_count should start at 0");
242
243        // Flushing an empty memtable produces no SSTable — count stays 0
244        engine.flush().await.unwrap();
245        assert_eq!(
246            engine.l0_count(),
247            0,
248            "empty flush must not increment l0_count"
249        );
250
251        // Write one row and flush — count must become 1
252        engine
253            .write(create_test_mutation(1, "Alice", 1_000_000))
254            .unwrap();
255        engine.flush().await.unwrap();
256        assert_eq!(
257            engine.l0_count(),
258            1,
259            "l0_count must be 1 after first non-empty flush"
260        );
261
262        // Write and flush again — count must become 2
263        engine
264            .write(create_test_mutation(2, "Bob", 2_000_000))
265            .unwrap();
266        engine.flush().await.unwrap();
267        assert_eq!(
268            engine.l0_count(),
269            2,
270            "l0_count must be 2 after second non-empty flush"
271        );
272    }
273
274    /// Verify that `total_written` != `memtable_row_count` after a flush —
275    /// the scenario that proves the placeholder is replaced.
276    #[tokio::test]
277    async fn test_total_written_differs_from_memtable_after_flush() {
278        let temp_dir = TempDir::new().unwrap();
279        let schema = create_test_schema();
280        let config = WriteEngineConfig::new(
281            temp_dir.path().join("data"),
282            temp_dir.path().join("wal"),
283            schema,
284        );
285        let mut engine = WriteEngine::new(config).unwrap();
286
287        // Write 7 rows and flush
288        for i in 0..7 {
289            engine
290                .write(create_test_mutation(
291                    i,
292                    &format!("User{i}"),
293                    1_000_000 + i as i64,
294                ))
295                .unwrap();
296        }
297        engine.flush().await.unwrap();
298
299        // Post-flush: memtable_row_count is 0 but total_written is 7
300        assert_eq!(engine.memtable_row_count(), 0);
301        assert_eq!(engine.total_written(), 7);
302        assert_ne!(
303            engine.total_written() as usize,
304            engine.memtable_row_count(),
305            "total_written must differ from memtable_row_count after flush — \
306             proves neither is a placeholder"
307        );
308    }
309}