Skip to main content

icydb_core/db/diagnostics/
mod.rs

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