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