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