1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
4 physical::CommitStoreDiagnostic,
5 slot::AllocationSlotDescriptor,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
15pub struct DiagnosticExport {
16 pub current_generation: u64,
18 pub ledger_anchor: AllocationSlotDescriptor,
20 pub records: Vec<DiagnosticRecord>,
22 pub generations: Vec<DiagnosticGeneration>,
24 pub commit_recovery: Option<CommitStoreDiagnostic>,
26}
27
28impl DiagnosticExport {
29 #[must_use]
31 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
32 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
33 }
34
35 #[must_use]
37 pub fn from_ledger_with_commit_recovery(
38 ledger: &AllocationLedger,
39 ledger_anchor: AllocationSlotDescriptor,
40 commit_recovery: Option<CommitStoreDiagnostic>,
41 ) -> Self {
42 Self::from_ledger_with_commit_recovery_and_memory_sizes(
43 ledger,
44 ledger_anchor,
45 commit_recovery,
46 std::iter::empty(),
47 )
48 }
49
50 #[must_use]
52 pub fn from_ledger_with_memory_sizes(
53 ledger: &AllocationLedger,
54 ledger_anchor: AllocationSlotDescriptor,
55 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
56 ) -> Self {
57 Self::from_ledger_with_commit_recovery_and_memory_sizes(
58 ledger,
59 ledger_anchor,
60 None,
61 memory_sizes,
62 )
63 }
64
65 #[must_use]
67 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
68 ledger: &AllocationLedger,
69 ledger_anchor: AllocationSlotDescriptor,
70 commit_recovery: Option<CommitStoreDiagnostic>,
71 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
72 ) -> Self {
73 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
74 Self {
75 current_generation: ledger.current_generation,
76 ledger_anchor,
77 records: ledger
78 .allocation_history()
79 .records()
80 .iter()
81 .cloned()
82 .map(|allocation| {
83 let memory_size = memory_sizes.get(allocation.slot()).copied();
84 DiagnosticRecord {
85 allocation,
86 memory_size,
87 }
88 })
89 .collect(),
90 generations: ledger
91 .allocation_history()
92 .generations()
93 .iter()
94 .cloned()
95 .map(|generation| DiagnosticGeneration { generation })
96 .collect(),
97 commit_recovery,
98 }
99 }
100}
101
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct DiagnosticRecord {
108 pub allocation: AllocationRecord,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub memory_size: Option<DiagnosticMemorySize>,
116}
117
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
125#[serde(deny_unknown_fields)]
126pub struct DiagnosticMemorySize {
127 pub wasm_pages: u64,
129 pub bytes: u64,
131}
132
133impl DiagnosticMemorySize {
134 #[must_use]
136 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
137 Self {
138 wasm_pages,
139 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
140 }
141 }
142}
143
144#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
149pub struct DiagnosticGeneration {
150 pub generation: GenerationRecord,
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::{
158 declaration::AllocationDeclaration,
159 ledger::{AllocationHistory, AllocationRecord, AllocationState},
160 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
161 schema::SchemaMetadata,
162 };
163
164 #[test]
165 fn diagnostic_export_copies_ledger_records() {
166 let declaration = AllocationDeclaration::new(
167 "app.users.v1",
168 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
169 None,
170 SchemaMetadata::default(),
171 )
172 .expect("declaration");
173 let ledger = AllocationLedger {
174 current_generation: 3,
175 allocation_history: AllocationHistory::from_parts(
176 vec![AllocationRecord::from_declaration(
177 3,
178 declaration,
179 AllocationState::Active,
180 )],
181 vec![GenerationRecord {
182 generation: 3,
183 parent_generation: 2,
184 runtime_fingerprint: Some("wasm:abc123".to_string()),
185 declaration_count: 1,
186 committed_at: None,
187 }],
188 ),
189 };
190
191 let export = DiagnosticExport::from_ledger(
192 &ledger,
193 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
194 );
195
196 assert_eq!(export.current_generation, 3);
197 assert_eq!(export.records.len(), 1);
198 assert_eq!(export.records[0].memory_size, None);
199 assert_eq!(export.generations.len(), 1);
200 assert_eq!(
201 export.ledger_anchor,
202 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
203 );
204 assert_eq!(export.commit_recovery, None);
205 }
206
207 #[test]
208 fn diagnostic_export_can_include_commit_recovery_state() {
209 let ledger = AllocationLedger {
210 current_generation: 3,
211 allocation_history: AllocationHistory::default(),
212 };
213 let commit_recovery = CommitStoreDiagnostic {
214 slot0: CommitSlotDiagnostic {
215 present: true,
216 generation: Some(3),
217 valid: true,
218 },
219 slot1: CommitSlotDiagnostic {
220 present: false,
221 generation: None,
222 valid: false,
223 },
224 authoritative_generation: Some(3),
225 recovery_error: None,
226 };
227
228 let export = DiagnosticExport::from_ledger_with_commit_recovery(
229 &ledger,
230 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
231 Some(commit_recovery),
232 );
233
234 assert_eq!(export.commit_recovery, Some(commit_recovery));
235 }
236
237 #[test]
238 fn diagnostic_export_can_include_memory_sizes() {
239 let declaration = AllocationDeclaration::new(
240 "app.users.v1",
241 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
242 None,
243 SchemaMetadata::default(),
244 )
245 .expect("declaration");
246 let ledger = AllocationLedger {
247 current_generation: 3,
248 allocation_history: AllocationHistory::from_parts(
249 vec![AllocationRecord::from_declaration(
250 3,
251 declaration,
252 AllocationState::Active,
253 )],
254 Vec::new(),
255 ),
256 };
257
258 let export = DiagnosticExport::from_ledger_with_memory_sizes(
259 &ledger,
260 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
261 [(
262 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
263 DiagnosticMemorySize::from_wasm_pages(2),
264 )],
265 );
266
267 assert_eq!(
268 export.records[0].memory_size,
269 Some(DiagnosticMemorySize {
270 wasm_pages: 2,
271 bytes: 131_072,
272 })
273 );
274 }
275
276 #[test]
277 fn diagnostic_export_can_report_recovery_failure() {
278 let ledger = AllocationLedger {
279 current_generation: 0,
280 allocation_history: AllocationHistory::default(),
281 };
282 let commit_recovery = CommitStoreDiagnostic {
283 slot0: CommitSlotDiagnostic {
284 present: false,
285 generation: None,
286 valid: false,
287 },
288 slot1: CommitSlotDiagnostic {
289 present: false,
290 generation: None,
291 valid: false,
292 },
293 authoritative_generation: None,
294 recovery_error: Some(CommitRecoveryError::NoValidGeneration),
295 };
296
297 let export = DiagnosticExport::from_ledger_with_commit_recovery(
298 &ledger,
299 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
300 Some(commit_recovery),
301 );
302
303 assert_eq!(
304 export
305 .commit_recovery
306 .expect("commit recovery")
307 .recovery_error,
308 Some(CommitRecoveryError::NoValidGeneration)
309 );
310 }
311}