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