1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 declaration::AllocationDeclaration,
4 ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
5 physical::CommitStoreDiagnostic,
6 slot::{AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerRangeAuthority},
7};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeMap;
10
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(deny_unknown_fields)]
17pub struct DiagnosticExport {
18 pub current_generation: u64,
20 pub ledger_anchor: AllocationSlotDescriptor,
22 pub records: Vec<DiagnosticRecord>,
24 pub generations: Vec<DiagnosticGeneration>,
26 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
28 pub commit_recovery: Option<CommitStoreDiagnostic>,
29}
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
43#[serde(deny_unknown_fields)]
44pub struct DefaultMemoryManagerDoctorReport {
45 pub bootstrapped: bool,
47 pub ledger_anchor: AllocationSlotDescriptor,
49 pub stable_cell: DiagnosticStableCell,
51 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
53 pub commit_recovery: Option<CommitStoreDiagnostic>,
54 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
56 pub ledger: Option<DiagnosticExport>,
57 pub registered_declarations: Vec<DiagnosticDeclaration>,
59 pub range_authority: DiagnosticRangeAuthority,
62 pub validation: DiagnosticCheck,
68}
69
70#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct DiagnosticDeclaration {
79 pub authority: String,
81 pub declaration: AllocationDeclaration,
83}
84
85impl DiagnosticDeclaration {
86 #[must_use]
88 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
89 Self {
90 authority: authority.into(),
91 declaration,
92 }
93 }
94}
95
96#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
103#[serde(deny_unknown_fields)]
104pub struct DiagnosticRangeAuthority {
105 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
107 pub effective_authority: Result<MemoryManagerRangeAuthority, String>,
109}
110
111impl DiagnosticRangeAuthority {
112 #[must_use]
114 pub const fn new(
115 registered_records: Vec<MemoryManagerAuthorityRecord>,
116 effective_authority: Result<MemoryManagerRangeAuthority, String>,
117 ) -> Self {
118 Self {
119 registered_records,
120 effective_authority,
121 }
122 }
123}
124
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
132#[serde(deny_unknown_fields)]
133pub struct DiagnosticStableCell {
134 pub status: DiagnosticStableCellStatus,
136 pub memory_size: DiagnosticMemorySize,
138}
139
140impl DiagnosticStableCell {
141 #[must_use]
143 pub const fn new(
144 status: DiagnosticStableCellStatus,
145 memory_size: DiagnosticMemorySize,
146 ) -> Self {
147 Self {
148 status,
149 memory_size,
150 }
151 }
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
161#[serde(deny_unknown_fields)]
162pub enum DiagnosticStableCellStatus {
163 Empty,
165 Readable,
167 Corrupt {
170 error: String,
172 },
173}
174
175#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182#[serde(deny_unknown_fields)]
183pub enum DiagnosticCheck {
184 NotRun {
186 message: String,
188 },
189 Passed,
191 Failed {
193 message: String,
195 },
196}
197
198impl DiagnosticCheck {
199 #[must_use]
201 pub const fn passed() -> Self {
202 Self::Passed
203 }
204
205 #[must_use]
207 pub fn failed(message: impl Into<String>) -> Self {
208 Self::Failed {
209 message: message.into(),
210 }
211 }
212
213 #[must_use]
215 pub fn not_run(message: impl Into<String>) -> Self {
216 Self::NotRun {
217 message: message.into(),
218 }
219 }
220}
221
222impl DiagnosticExport {
223 #[must_use]
225 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
226 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
227 }
228
229 #[must_use]
231 pub fn from_ledger_with_commit_recovery(
232 ledger: &AllocationLedger,
233 ledger_anchor: AllocationSlotDescriptor,
234 commit_recovery: Option<CommitStoreDiagnostic>,
235 ) -> Self {
236 Self::from_ledger_with_commit_recovery_and_memory_sizes(
237 ledger,
238 ledger_anchor,
239 commit_recovery,
240 std::iter::empty(),
241 )
242 }
243
244 #[must_use]
246 pub fn from_ledger_with_memory_sizes(
247 ledger: &AllocationLedger,
248 ledger_anchor: AllocationSlotDescriptor,
249 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
250 ) -> Self {
251 Self::from_ledger_with_commit_recovery_and_memory_sizes(
252 ledger,
253 ledger_anchor,
254 None,
255 memory_sizes,
256 )
257 }
258
259 #[must_use]
261 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
262 ledger: &AllocationLedger,
263 ledger_anchor: AllocationSlotDescriptor,
264 commit_recovery: Option<CommitStoreDiagnostic>,
265 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
266 ) -> Self {
267 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
268 Self {
269 current_generation: ledger.current_generation,
270 ledger_anchor,
271 records: ledger
272 .allocation_history()
273 .records()
274 .iter()
275 .cloned()
276 .map(|allocation| {
277 let memory_size = memory_sizes.get(allocation.slot()).copied();
278 DiagnosticRecord {
279 allocation,
280 memory_size,
281 }
282 })
283 .collect(),
284 generations: ledger
285 .allocation_history()
286 .generations()
287 .iter()
288 .cloned()
289 .map(|generation| DiagnosticGeneration { generation })
290 .collect(),
291 commit_recovery,
292 }
293 }
294}
295
296#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
301#[serde(deny_unknown_fields)]
302pub struct DiagnosticRecord {
303 pub allocation: AllocationRecord,
305 #[serde(skip_serializing_if = "Option::is_none")]
310 pub memory_size: Option<DiagnosticMemorySize>,
311}
312
313#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
320#[serde(deny_unknown_fields)]
321pub struct DiagnosticMemorySize {
322 pub wasm_pages: u64,
324 pub bytes: u64,
326}
327
328impl DiagnosticMemorySize {
329 #[must_use]
331 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
332 Self {
333 wasm_pages,
334 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
335 }
336 }
337}
338
339#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
344#[serde(deny_unknown_fields)]
345pub struct DiagnosticGeneration {
346 pub generation: GenerationRecord,
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::{
354 declaration::AllocationDeclaration,
355 ledger::{AllocationHistory, AllocationRecord},
356 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
357 schema::SchemaMetadata,
358 };
359
360 #[test]
361 fn diagnostic_export_copies_ledger_records() {
362 let declaration = AllocationDeclaration::new(
363 "app.users.v1",
364 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
365 None,
366 SchemaMetadata::default(),
367 )
368 .expect("declaration");
369 let ledger = AllocationLedger {
370 current_generation: 3,
371 allocation_history: AllocationHistory::from_parts(
372 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
373 vec![GenerationRecord {
374 generation: 3,
375 parent_generation: 2,
376 runtime_fingerprint: Some("wasm:abc123".to_string()),
377 declaration_count: 1,
378 committed_at: None,
379 }],
380 ),
381 };
382
383 let export = DiagnosticExport::from_ledger(
384 &ledger,
385 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
386 );
387
388 assert_eq!(export.current_generation, 3);
389 assert_eq!(export.records.len(), 1);
390 assert_eq!(export.records[0].memory_size, None);
391 assert_eq!(export.generations.len(), 1);
392 assert_eq!(
393 export.ledger_anchor,
394 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
395 );
396 assert_eq!(export.commit_recovery, None);
397 }
398
399 #[test]
400 fn diagnostic_export_rejects_unknown_top_level_fields() {
401 use crate::test_cbor::Value;
402
403 let export = DiagnosticExport {
404 current_generation: 0,
405 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
406 records: Vec::new(),
407 generations: Vec::new(),
408 commit_recovery: None,
409 };
410 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
411 else {
412 panic!("diagnostic export encodes as a map");
413 };
414 crate::test_cbor::map_insert(
415 &mut map,
416 Value::Text("future_field".to_string()),
417 Value::Bool(true),
418 );
419 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
420
421 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
422 .expect_err("unknown diagnostic field must fail closed");
423
424 assert!(err.to_string().contains("future_field"));
425 }
426
427 #[test]
428 fn diagnostic_outcome_states_round_trip() {
429 let stable_cell = DiagnosticStableCell::new(
430 DiagnosticStableCellStatus::Corrupt {
431 error: "bad stable-cell record".to_string(),
432 },
433 DiagnosticMemorySize::from_wasm_pages(1),
434 );
435 let range_authority = DiagnosticRangeAuthority::new(
436 Vec::new(),
437 Err("overlapping authority ranges".to_string()),
438 );
439 let check = DiagnosticCheck::failed("duplicate declaration");
440
441 for value in [DiagnosticCheck::passed(), check] {
442 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
443 let decoded: DiagnosticCheck =
444 crate::test_cbor::from_slice(&bytes).expect("check round trip");
445 assert_eq!(decoded, value);
446 }
447
448 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
449 let decoded: DiagnosticStableCell =
450 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
451 assert_eq!(decoded, stable_cell);
452
453 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
454 let decoded: DiagnosticRangeAuthority =
455 crate::test_cbor::from_slice(&bytes).expect("range round trip");
456 assert_eq!(decoded, range_authority);
457 }
458
459 #[test]
460 fn diagnostic_export_can_include_commit_recovery_state() {
461 let ledger = AllocationLedger {
462 current_generation: 3,
463 allocation_history: AllocationHistory::default(),
464 };
465 let commit_recovery = CommitStoreDiagnostic {
466 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
467 slot1: CommitSlotDiagnostic::Empty,
468 recovery: Ok(3),
469 };
470
471 let export = DiagnosticExport::from_ledger_with_commit_recovery(
472 &ledger,
473 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
474 Some(commit_recovery),
475 );
476
477 assert_eq!(export.commit_recovery, Some(commit_recovery));
478 }
479
480 #[test]
481 fn diagnostic_export_can_include_memory_sizes() {
482 let declaration = AllocationDeclaration::new(
483 "app.users.v1",
484 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
485 None,
486 SchemaMetadata::default(),
487 )
488 .expect("declaration");
489 let ledger = AllocationLedger {
490 current_generation: 3,
491 allocation_history: AllocationHistory::from_parts(
492 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
493 Vec::new(),
494 ),
495 };
496
497 let export = DiagnosticExport::from_ledger_with_memory_sizes(
498 &ledger,
499 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
500 [(
501 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
502 DiagnosticMemorySize::from_wasm_pages(2),
503 )],
504 );
505
506 assert_eq!(
507 export.records[0].memory_size,
508 Some(DiagnosticMemorySize {
509 wasm_pages: 2,
510 bytes: 131_072,
511 })
512 );
513 }
514
515 #[test]
516 fn diagnostic_export_can_report_recovery_failure() {
517 let ledger = AllocationLedger {
518 current_generation: 0,
519 allocation_history: AllocationHistory::default(),
520 };
521 let commit_recovery = CommitStoreDiagnostic {
522 slot0: CommitSlotDiagnostic::Empty,
523 slot1: CommitSlotDiagnostic::Empty,
524 recovery: Err(CommitRecoveryError::NoValidGeneration),
525 };
526
527 let export = DiagnosticExport::from_ledger_with_commit_recovery(
528 &ledger,
529 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
530 Some(commit_recovery),
531 );
532
533 assert_eq!(
534 export.commit_recovery.expect("commit recovery").recovery,
535 Err(CommitRecoveryError::NoValidGeneration)
536 );
537 }
538}