Skip to main content

icydb_core/db/diagnostics/
mod.rs

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