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 use execution_trace::{ExecutionAccessPathVariant, ExecutionOptimization, ExecutionTrace};
25
26///
27/// StorageReport
28/// Live storage snapshot report
29///
30
31#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
32pub struct StorageReport {
33    pub storage_data: Vec<DataStoreSnapshot>,
34    pub storage_index: Vec<IndexStoreSnapshot>,
35    pub entity_storage: Vec<EntitySnapshot>,
36    pub corrupted_keys: u64,
37    pub corrupted_entries: u64,
38}
39
40///
41/// DataStoreSnapshot
42/// Store-level snapshot metrics.
43///
44
45#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
46pub struct DataStoreSnapshot {
47    pub path: String,
48    pub entries: u64,
49    pub memory_bytes: u64,
50}
51
52///
53/// IndexStoreSnapshot
54/// Index-store snapshot metrics
55///
56
57#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
58pub struct IndexStoreSnapshot {
59    pub path: String,
60    pub entries: u64,
61    pub user_entries: u64,
62    pub system_entries: u64,
63    pub memory_bytes: u64,
64}
65
66///
67/// EntitySnapshot
68/// Per-entity storage breakdown across stores
69///
70
71#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
72pub struct EntitySnapshot {
73    /// Store path (e.g., icydb_schema_tests::schema::TestDataStore)
74    pub store: String,
75
76    /// Entity path (e.g., icydb_schema_tests::canister::db::Index)
77    pub path: String,
78
79    /// Number of rows for this entity in the store
80    pub entries: u64,
81
82    /// Approximate bytes used (key + value)
83    pub memory_bytes: u64,
84
85    /// Minimum primary key for this entity (entity-local ordering)
86    pub min_key: Option<Value>,
87
88    /// Maximum primary key for this entity (entity-local ordering)
89    pub max_key: Option<Value>,
90}
91
92///
93/// EntityStats
94/// Internal struct for building per-entity stats before snapshotting.
95///
96
97#[derive(Default)]
98struct EntityStats {
99    entries: u64,
100    memory_bytes: u64,
101    min_key: Option<StorageKey>,
102    max_key: Option<StorageKey>,
103}
104
105impl EntityStats {
106    // Accumulate per-entity counters and keep min/max over entity-local storage keys.
107    fn update(&mut self, dk: &DataKey, value_len: u64) {
108        self.entries = self.entries.saturating_add(1);
109        self.memory_bytes = self
110            .memory_bytes
111            .saturating_add(DataKey::entry_size_bytes(value_len));
112
113        let k = dk.storage_key();
114
115        match &mut self.min_key {
116            Some(min) if k < *min => *min = k,
117            None => self.min_key = Some(k),
118            _ => {}
119        }
120
121        match &mut self.max_key {
122            Some(max) if k > *max => *max = k,
123            None => self.max_key = Some(k),
124            _ => {}
125        }
126    }
127}
128
129/// Build one deterministic storage snapshot with per-entity rollups.
130///
131/// This path is read-only and fail-closed on decode/validation errors by counting
132/// corrupted keys/entries instead of panicking.
133pub(crate) fn storage_report<C: CanisterKind>(
134    db: &Db<C>,
135    name_to_path: &[(&'static str, &'static str)],
136) -> Result<StorageReport, InternalError> {
137    db.ensure_recovered_state()?;
138    // Build name→path map once, reuse across stores.
139    let name_map: BTreeMap<&'static str, &str> = name_to_path.iter().copied().collect();
140    let mut data = Vec::new();
141    let mut index = Vec::new();
142    let mut entity_storage: Vec<EntitySnapshot> = Vec::new();
143    let mut corrupted_keys = 0u64;
144    let mut corrupted_entries = 0u64;
145
146    db.with_store_registry(|reg| {
147        // Keep diagnostics snapshots deterministic by traversing stores in path order.
148        let mut stores = reg.iter().collect::<Vec<_>>();
149        stores.sort_by_key(|(path, _)| *path);
150
151        for (path, store_handle) in stores {
152            // Phase 1: collect data-store snapshots and per-entity stats.
153            store_handle.with_data(|store| {
154                data.push(DataStoreSnapshot {
155                    path: path.to_string(),
156                    entries: store.len(),
157                    memory_bytes: store.memory_bytes(),
158                });
159
160                // Track per-entity counts, memory, and min/max Keys (not DataKeys)
161                let mut by_entity: BTreeMap<EntityName, EntityStats> = BTreeMap::new();
162
163                for entry in store.iter() {
164                    let Ok(dk) = DataKey::try_from_raw(entry.key()) else {
165                        corrupted_keys = corrupted_keys.saturating_add(1);
166                        continue;
167                    };
168
169                    let value_len = entry.value().len() as u64;
170
171                    by_entity
172                        .entry(*dk.entity_name())
173                        .or_default()
174                        .update(&dk, value_len);
175                }
176
177                for (entity_name, stats) in by_entity {
178                    let path_name = name_map
179                        .get(entity_name.as_str())
180                        .copied()
181                        .unwrap_or(entity_name.as_str());
182                    entity_storage.push(EntitySnapshot {
183                        store: path.to_string(),
184                        path: path_name.to_string(),
185                        entries: stats.entries,
186                        memory_bytes: stats.memory_bytes,
187                        min_key: stats.min_key.map(|key| key.as_value()),
188                        max_key: stats.max_key.map(|key| key.as_value()),
189                    });
190                }
191            });
192
193            // Phase 2: collect index-store snapshots and integrity counters.
194            store_handle.with_index(|store| {
195                let mut user_entries = 0u64;
196                let mut system_entries = 0u64;
197
198                for (key, value) in store.entries() {
199                    let Ok(decoded_key) = IndexKey::try_from_raw(&key) else {
200                        corrupted_entries = corrupted_entries.saturating_add(1);
201                        continue;
202                    };
203
204                    if decoded_key.uses_system_namespace() {
205                        system_entries = system_entries.saturating_add(1);
206                    } else {
207                        user_entries = user_entries.saturating_add(1);
208                    }
209
210                    if value.validate().is_err() {
211                        corrupted_entries = corrupted_entries.saturating_add(1);
212                    }
213                }
214
215                index.push(IndexStoreSnapshot {
216                    path: path.to_string(),
217                    entries: store.len(),
218                    user_entries,
219                    system_entries,
220                    memory_bytes: store.memory_bytes(),
221                });
222            });
223        }
224    });
225
226    // Phase 3: enforce deterministic entity snapshot emission order.
227    // This remains stable even if outer store traversal internals change.
228    entity_storage.sort_by(|left, right| {
229        (left.store.as_str(), left.path.as_str()).cmp(&(right.store.as_str(), right.path.as_str()))
230    });
231
232    Ok(StorageReport {
233        storage_data: data,
234        storage_index: index,
235        entity_storage,
236        corrupted_keys,
237        corrupted_entries,
238    })
239}