1use crate::{
2 db::{
3 Db, ensure_recovered,
4 identity::EntityName,
5 index::IndexKey,
6 store::{DataKey, StorageKey},
7 },
8 error::InternalError,
9 traits::CanisterKind,
10 value::Value,
11};
12use candid::CandidType;
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15
16#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
22pub struct StorageReport {
23 pub storage_data: Vec<DataStoreSnapshot>,
24 pub storage_index: Vec<IndexStoreSnapshot>,
25 pub entity_storage: Vec<EntitySnapshot>,
26 pub corrupted_keys: u64,
27 pub corrupted_entries: u64,
28}
29
30#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
36pub struct DataStoreSnapshot {
37 pub path: String,
38 pub entries: u64,
39 pub memory_bytes: u64,
40}
41
42#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
48pub struct IndexStoreSnapshot {
49 pub path: String,
50 pub entries: u64,
51 pub memory_bytes: u64,
52}
53
54#[derive(CandidType, Clone, Debug, Default, Deserialize, Serialize)]
60pub struct EntitySnapshot {
61 pub store: String,
63 pub path: String,
65 pub entries: u64,
67 pub memory_bytes: u64,
69 pub min_key: Option<Value>,
71 pub max_key: Option<Value>,
73}
74
75#[derive(Default)]
81struct EntityStats {
82 entries: u64,
83 memory_bytes: u64,
84 min_key: Option<StorageKey>,
85 max_key: Option<StorageKey>,
86}
87
88impl EntityStats {
89 fn update(&mut self, dk: &DataKey, value_len: u64) {
90 self.entries = self.entries.saturating_add(1);
91 self.memory_bytes = self
92 .memory_bytes
93 .saturating_add(DataKey::entry_size_bytes(value_len));
94
95 let k = dk.storage_key();
96
97 match &mut self.min_key {
98 Some(min) if k < *min => *min = k,
99 None => self.min_key = Some(k),
100 _ => {}
101 }
102
103 match &mut self.max_key {
104 Some(max) if k > *max => *max = k,
105 None => self.max_key = Some(k),
106 _ => {}
107 }
108 }
109}
110
111pub fn storage_report<C: CanisterKind>(
113 db: &Db<C>,
114 name_to_path: &[(&'static str, &'static str)],
115) -> Result<StorageReport, InternalError> {
116 ensure_recovered(db)?;
117 let name_map: BTreeMap<&'static str, &str> = name_to_path.iter().copied().collect();
119 let mut data = Vec::new();
120 let mut index = Vec::new();
121 let mut entity_storage: Vec<EntitySnapshot> = Vec::new();
122 let mut corrupted_keys = 0u64;
123 let mut corrupted_entries = 0u64;
124
125 db.with_data(|reg| {
126 reg.for_each(|path, store| {
127 data.push(DataStoreSnapshot {
128 path: path.to_string(),
129 entries: store.len(),
130 memory_bytes: store.memory_bytes(),
131 });
132
133 let mut by_entity: BTreeMap<EntityName, EntityStats> = BTreeMap::new();
135
136 for entry in store.iter() {
137 let Ok(dk) = DataKey::try_from_raw(entry.key()) else {
138 corrupted_keys = corrupted_keys.saturating_add(1);
139 continue;
140 };
141
142 let value_len = entry.value().len() as u64;
143
144 by_entity
145 .entry(*dk.entity_name())
146 .or_default()
147 .update(&dk, value_len);
148 }
149
150 for (entity_name, stats) in by_entity {
151 let path_name = name_map.get(entity_name.as_str()).copied().unwrap_or("");
152 entity_storage.push(EntitySnapshot {
153 store: path.to_string(),
154 path: path_name.to_string(),
155 entries: stats.entries,
156 memory_bytes: stats.memory_bytes,
157 min_key: stats.min_key.map(|key| key.as_value()),
158 max_key: stats.max_key.map(|key| key.as_value()),
159 });
160 }
161 });
162 });
163
164 db.with_index(|reg| {
165 reg.for_each(|path, store| {
166 index.push(IndexStoreSnapshot {
167 path: path.to_string(),
168 entries: store.len(),
169 memory_bytes: store.memory_bytes(),
170 });
171
172 for (key, value) in store.entries() {
173 if IndexKey::try_from_raw(&key).is_err() {
174 corrupted_entries = corrupted_entries.saturating_add(1);
175 continue;
176 }
177 if value.validate().is_err() {
178 corrupted_entries = corrupted_entries.saturating_add(1);
179 }
180 }
181 });
182 });
183
184 Ok(StorageReport {
185 storage_data: data,
186 storage_index: index,
187 entity_storage,
188 corrupted_keys,
189 corrupted_entries,
190 })
191}