Skip to main content

icydb_core/db/diagnostics/
mod.rs

1//! Module: diagnostics
2//! Responsibility: read-only storage footprint and integrity snapshots.
3//! Does not own: recovery, write-path mutation, or query planning semantics.
4//! Boundary: consumes `Db`/store read APIs and returns DTO snapshots.
5
6mod execution_trace;
7#[cfg(test)]
8mod tests;
9
10use crate::{
11    db::{
12        Db, EntityName,
13        data::{DataKey, StorageKey},
14        index::IndexKey,
15    },
16    error::InternalError,
17    traits::CanisterKind,
18    value::Value,
19};
20use candid::CandidType;
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeMap;
23
24pub(crate) use execution_trace::ExecutionOptimizationCounter;
25pub(crate) use execution_trace::record_execution_optimization_hit_for_tests;
26#[cfg(test)]
27pub(crate) use execution_trace::take_execution_optimization_hits_for_tests;
28pub use execution_trace::{
29    ExecutionAccessPathVariant, ExecutionMetrics, ExecutionOptimization, ExecutionTrace,
30};
31
32///
33/// StorageReport
34/// Live storage snapshot report
35///
36
37#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
38pub struct StorageReport {
39    pub(crate) storage_data: Vec<DataStoreSnapshot>,
40    pub(crate) storage_index: Vec<IndexStoreSnapshot>,
41    pub(crate) entity_storage: Vec<EntitySnapshot>,
42    pub(crate) corrupted_keys: u64,
43    pub(crate) corrupted_entries: u64,
44}
45
46impl StorageReport {
47    /// Construct one storage report payload.
48    #[must_use]
49    pub const fn new(
50        storage_data: Vec<DataStoreSnapshot>,
51        storage_index: Vec<IndexStoreSnapshot>,
52        entity_storage: Vec<EntitySnapshot>,
53        corrupted_keys: u64,
54        corrupted_entries: u64,
55    ) -> Self {
56        Self {
57            storage_data,
58            storage_index,
59            entity_storage,
60            corrupted_keys,
61            corrupted_entries,
62        }
63    }
64
65    /// Borrow data-store snapshots.
66    #[must_use]
67    pub const fn storage_data(&self) -> &[DataStoreSnapshot] {
68        self.storage_data.as_slice()
69    }
70
71    /// Borrow index-store snapshots.
72    #[must_use]
73    pub const fn storage_index(&self) -> &[IndexStoreSnapshot] {
74        self.storage_index.as_slice()
75    }
76
77    /// Borrow entity-level storage snapshots.
78    #[must_use]
79    pub const fn entity_storage(&self) -> &[EntitySnapshot] {
80        self.entity_storage.as_slice()
81    }
82
83    /// Return count of corrupted decoded data keys.
84    #[must_use]
85    pub const fn corrupted_keys(&self) -> u64 {
86        self.corrupted_keys
87    }
88
89    /// Return count of corrupted index entries.
90    #[must_use]
91    pub const fn corrupted_entries(&self) -> u64 {
92        self.corrupted_entries
93    }
94}
95
96///
97/// DataStoreSnapshot
98/// Store-level snapshot metrics.
99///
100
101#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
102pub struct DataStoreSnapshot {
103    pub(crate) path: String,
104    pub(crate) entries: u64,
105    pub(crate) memory_bytes: u64,
106}
107
108impl DataStoreSnapshot {
109    /// Construct one data-store snapshot row.
110    #[must_use]
111    pub const fn new(path: String, entries: u64, memory_bytes: u64) -> Self {
112        Self {
113            path,
114            entries,
115            memory_bytes,
116        }
117    }
118
119    /// Borrow store path.
120    #[must_use]
121    pub const fn path(&self) -> &str {
122        self.path.as_str()
123    }
124
125    /// Return row count.
126    #[must_use]
127    pub const fn entries(&self) -> u64 {
128        self.entries
129    }
130
131    /// Return memory usage in bytes.
132    #[must_use]
133    pub const fn memory_bytes(&self) -> u64 {
134        self.memory_bytes
135    }
136}
137
138///
139/// IndexStoreSnapshot
140/// Index-store snapshot metrics
141///
142
143#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
144pub struct IndexStoreSnapshot {
145    pub(crate) path: String,
146    pub(crate) entries: u64,
147    pub(crate) user_entries: u64,
148    pub(crate) system_entries: u64,
149    pub(crate) memory_bytes: u64,
150}
151
152impl IndexStoreSnapshot {
153    /// Construct one index-store snapshot row.
154    #[must_use]
155    pub const fn new(
156        path: String,
157        entries: u64,
158        user_entries: u64,
159        system_entries: u64,
160        memory_bytes: u64,
161    ) -> Self {
162        Self {
163            path,
164            entries,
165            user_entries,
166            system_entries,
167            memory_bytes,
168        }
169    }
170
171    /// Borrow store path.
172    #[must_use]
173    pub const fn path(&self) -> &str {
174        self.path.as_str()
175    }
176
177    /// Return total entry count.
178    #[must_use]
179    pub const fn entries(&self) -> u64 {
180        self.entries
181    }
182
183    /// Return user-namespace entry count.
184    #[must_use]
185    pub const fn user_entries(&self) -> u64 {
186        self.user_entries
187    }
188
189    /// Return system-namespace entry count.
190    #[must_use]
191    pub const fn system_entries(&self) -> u64 {
192        self.system_entries
193    }
194
195    /// Return memory usage in bytes.
196    #[must_use]
197    pub const fn memory_bytes(&self) -> u64 {
198        self.memory_bytes
199    }
200}
201
202///
203/// EntitySnapshot
204/// Per-entity storage breakdown across stores
205///
206
207#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
208pub struct EntitySnapshot {
209    /// Store path (e.g., icydb_schema_tests::schema::TestDataStore)
210    pub(crate) store: String,
211
212    /// Entity path (e.g., icydb_schema_tests::canister::db::Index)
213    pub(crate) path: String,
214
215    /// Number of rows for this entity in the store
216    pub(crate) entries: u64,
217
218    /// Approximate bytes used (key + value)
219    pub(crate) memory_bytes: u64,
220
221    /// Minimum primary key for this entity (entity-local ordering)
222    pub(crate) min_key: Option<Value>,
223
224    /// Maximum primary key for this entity (entity-local ordering)
225    pub(crate) max_key: Option<Value>,
226}
227
228impl EntitySnapshot {
229    /// Construct one entity-storage snapshot row.
230    #[must_use]
231    pub const fn new(
232        store: String,
233        path: String,
234        entries: u64,
235        memory_bytes: u64,
236        min_key: Option<Value>,
237        max_key: Option<Value>,
238    ) -> Self {
239        Self {
240            store,
241            path,
242            entries,
243            memory_bytes,
244            min_key,
245            max_key,
246        }
247    }
248
249    /// Borrow store path.
250    #[must_use]
251    pub const fn store(&self) -> &str {
252        self.store.as_str()
253    }
254
255    /// Borrow entity path.
256    #[must_use]
257    pub const fn path(&self) -> &str {
258        self.path.as_str()
259    }
260
261    /// Return row count.
262    #[must_use]
263    pub const fn entries(&self) -> u64 {
264        self.entries
265    }
266
267    /// Return memory usage in bytes.
268    #[must_use]
269    pub const fn memory_bytes(&self) -> u64 {
270        self.memory_bytes
271    }
272
273    /// Borrow optional minimum primary key.
274    #[must_use]
275    pub const fn min_key(&self) -> Option<&Value> {
276        self.min_key.as_ref()
277    }
278
279    /// Borrow optional maximum primary key.
280    #[must_use]
281    pub const fn max_key(&self) -> Option<&Value> {
282        self.max_key.as_ref()
283    }
284}
285
286///
287/// EntityStats
288/// Internal struct for building per-entity stats before snapshotting.
289///
290
291#[derive(Default)]
292struct EntityStats {
293    entries: u64,
294    memory_bytes: u64,
295    min_key: Option<StorageKey>,
296    max_key: Option<StorageKey>,
297}
298
299impl EntityStats {
300    // Accumulate per-entity counters and keep min/max over entity-local storage keys.
301    fn update(&mut self, dk: &DataKey, value_len: u64) {
302        self.entries = self.entries.saturating_add(1);
303        self.memory_bytes = self
304            .memory_bytes
305            .saturating_add(DataKey::entry_size_bytes(value_len));
306
307        let k = dk.storage_key();
308
309        match &mut self.min_key {
310            Some(min) if k < *min => *min = k,
311            None => self.min_key = Some(k),
312            _ => {}
313        }
314
315        match &mut self.max_key {
316            Some(max) if k > *max => *max = k,
317            None => self.max_key = Some(k),
318            _ => {}
319        }
320    }
321}
322
323/// Build one deterministic storage snapshot with per-entity rollups.
324///
325/// This path is read-only and fail-closed on decode/validation errors by counting
326/// corrupted keys/entries instead of panicking.
327pub(crate) fn storage_report<C: CanisterKind>(
328    db: &Db<C>,
329    name_to_path: &[(&'static str, &'static str)],
330) -> Result<StorageReport, InternalError> {
331    db.ensure_recovered_state()?;
332    // Build name→path map once, reuse across stores.
333    let name_map: BTreeMap<&'static str, &str> = name_to_path.iter().copied().collect();
334    let mut data = Vec::new();
335    let mut index = Vec::new();
336    let mut entity_storage: Vec<EntitySnapshot> = Vec::new();
337    let mut corrupted_keys = 0u64;
338    let mut corrupted_entries = 0u64;
339
340    db.with_store_registry(|reg| {
341        // Keep diagnostics snapshots deterministic by traversing stores in path order.
342        let mut stores = reg.iter().collect::<Vec<_>>();
343        stores.sort_by_key(|(path, _)| *path);
344
345        for (path, store_handle) in stores {
346            // Phase 1: collect data-store snapshots and per-entity stats.
347            store_handle.with_data(|store| {
348                data.push(DataStoreSnapshot::new(
349                    path.to_string(),
350                    store.len(),
351                    store.memory_bytes(),
352                ));
353
354                // Track per-entity counts, memory, and min/max Keys (not DataKeys)
355                let mut by_entity: BTreeMap<EntityName, EntityStats> = BTreeMap::new();
356
357                for entry in store.iter() {
358                    let Ok(dk) = DataKey::try_from_raw(entry.key()) else {
359                        corrupted_keys = corrupted_keys.saturating_add(1);
360                        continue;
361                    };
362
363                    let value_len = entry.value().len() as u64;
364
365                    by_entity
366                        .entry(*dk.entity_name())
367                        .or_default()
368                        .update(&dk, value_len);
369                }
370
371                for (entity_name, stats) in by_entity {
372                    let path_name = name_map
373                        .get(entity_name.as_str())
374                        .copied()
375                        .unwrap_or(entity_name.as_str());
376                    entity_storage.push(EntitySnapshot::new(
377                        path.to_string(),
378                        path_name.to_string(),
379                        stats.entries,
380                        stats.memory_bytes,
381                        stats.min_key.map(|key| key.as_value()),
382                        stats.max_key.map(|key| key.as_value()),
383                    ));
384                }
385            });
386
387            // Phase 2: collect index-store snapshots and integrity counters.
388            store_handle.with_index(|store| {
389                let mut user_entries = 0u64;
390                let mut system_entries = 0u64;
391
392                for (key, value) in store.entries() {
393                    let Ok(decoded_key) = IndexKey::try_from_raw(&key) else {
394                        corrupted_entries = corrupted_entries.saturating_add(1);
395                        continue;
396                    };
397
398                    if decoded_key.uses_system_namespace() {
399                        system_entries = system_entries.saturating_add(1);
400                    } else {
401                        user_entries = user_entries.saturating_add(1);
402                    }
403
404                    if value.validate().is_err() {
405                        corrupted_entries = corrupted_entries.saturating_add(1);
406                    }
407                }
408
409                index.push(IndexStoreSnapshot::new(
410                    path.to_string(),
411                    store.len(),
412                    user_entries,
413                    system_entries,
414                    store.memory_bytes(),
415                ));
416            });
417        }
418    });
419
420    // Phase 3: enforce deterministic entity snapshot emission order.
421    // This remains stable even if outer store traversal internals change.
422    entity_storage
423        .sort_by(|left, right| (left.store(), left.path()).cmp(&(right.store(), right.path())));
424
425    Ok(StorageReport::new(
426        data,
427        index,
428        entity_storage,
429        corrupted_keys,
430        corrupted_entries,
431    ))
432}