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, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
103pub enum DiagnosticCode {
104 #[serde(rename = "eager_init")]
106 EagerInit,
107 #[serde(rename = "declaration_registry")]
109 DeclarationRegistry,
110 #[serde(rename = "range_registry")]
112 RangeRegistry,
113 #[serde(rename = "range_authority")]
115 RangeAuthority,
116 #[serde(rename = "declaration_snapshot")]
118 DeclarationSnapshot,
119 #[serde(rename = "stable_cell")]
121 StableCell,
122 #[serde(rename = "unsupported_format")]
124 UnsupportedFormat,
125 #[serde(rename = "ledger_recovery")]
127 LedgerRecovery,
128 #[serde(rename = "genesis_ledger")]
130 GenesisLedger,
131 #[serde(rename = "allocation_validation")]
133 AllocationValidation,
134}
135
136#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
143#[serde(deny_unknown_fields)]
144pub struct DiagnosticFailure {
145 pub code: DiagnosticCode,
147 pub message: String,
149}
150
151impl DiagnosticFailure {
152 #[must_use]
154 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
155 Self {
156 code,
157 message: message.into(),
158 }
159 }
160}
161
162#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
169#[serde(deny_unknown_fields)]
170pub struct DiagnosticRangeAuthority {
171 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
173 pub effective_authority: Result<MemoryManagerRangeAuthority, DiagnosticFailure>,
175}
176
177impl DiagnosticRangeAuthority {
178 #[must_use]
180 pub const fn new(
181 registered_records: Vec<MemoryManagerAuthorityRecord>,
182 effective_authority: Result<MemoryManagerRangeAuthority, DiagnosticFailure>,
183 ) -> Self {
184 Self {
185 registered_records,
186 effective_authority,
187 }
188 }
189}
190
191#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
198#[serde(deny_unknown_fields)]
199pub struct DiagnosticStableCell {
200 pub status: DiagnosticStableCellStatus,
202 pub memory_size: DiagnosticMemorySize,
204}
205
206impl DiagnosticStableCell {
207 #[must_use]
209 pub const fn new(
210 status: DiagnosticStableCellStatus,
211 memory_size: DiagnosticMemorySize,
212 ) -> Self {
213 Self {
214 status,
215 memory_size,
216 }
217 }
218}
219
220#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub enum DiagnosticStableCellStatus {
229 Empty,
231 Readable,
233 Corrupt {
236 failure: DiagnosticFailure,
238 },
239}
240
241#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
248#[serde(deny_unknown_fields)]
249pub enum DiagnosticCheck {
250 NotRun {
252 code: DiagnosticCode,
254 message: String,
256 },
257 Passed,
259 Failed {
261 code: DiagnosticCode,
263 message: String,
265 },
266}
267
268impl DiagnosticCheck {
269 #[must_use]
271 pub const fn passed() -> Self {
272 Self::Passed
273 }
274
275 #[must_use]
277 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
278 Self::Failed {
279 code,
280 message: message.into(),
281 }
282 }
283
284 #[must_use]
286 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
287 Self::NotRun {
288 code,
289 message: message.into(),
290 }
291 }
292}
293
294impl DiagnosticExport {
295 #[must_use]
297 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
298 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
299 }
300
301 #[must_use]
303 pub fn from_ledger_with_commit_recovery(
304 ledger: &AllocationLedger,
305 ledger_anchor: AllocationSlotDescriptor,
306 commit_recovery: Option<CommitStoreDiagnostic>,
307 ) -> Self {
308 Self::from_ledger_with_commit_recovery_and_memory_sizes(
309 ledger,
310 ledger_anchor,
311 commit_recovery,
312 std::iter::empty(),
313 )
314 }
315
316 #[must_use]
318 pub fn from_ledger_with_memory_sizes(
319 ledger: &AllocationLedger,
320 ledger_anchor: AllocationSlotDescriptor,
321 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
322 ) -> Self {
323 Self::from_ledger_with_commit_recovery_and_memory_sizes(
324 ledger,
325 ledger_anchor,
326 None,
327 memory_sizes,
328 )
329 }
330
331 #[must_use]
333 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
334 ledger: &AllocationLedger,
335 ledger_anchor: AllocationSlotDescriptor,
336 commit_recovery: Option<CommitStoreDiagnostic>,
337 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
338 ) -> Self {
339 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
340 Self {
341 current_generation: ledger.current_generation,
342 ledger_anchor,
343 records: ledger
344 .allocation_history()
345 .records()
346 .iter()
347 .cloned()
348 .map(|allocation| {
349 let memory_size = memory_sizes.get(allocation.slot()).copied();
350 DiagnosticRecord {
351 allocation,
352 memory_size,
353 }
354 })
355 .collect(),
356 generations: ledger
357 .allocation_history()
358 .generations()
359 .iter()
360 .cloned()
361 .map(|generation| DiagnosticGeneration { generation })
362 .collect(),
363 commit_recovery,
364 }
365 }
366}
367
368#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
373#[serde(deny_unknown_fields)]
374pub struct DiagnosticRecord {
375 pub allocation: AllocationRecord,
377 #[serde(skip_serializing_if = "Option::is_none")]
382 pub memory_size: Option<DiagnosticMemorySize>,
383}
384
385#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
392#[serde(deny_unknown_fields)]
393pub struct DiagnosticMemorySize {
394 pub wasm_pages: u64,
396 pub bytes: u64,
398}
399
400impl DiagnosticMemorySize {
401 #[must_use]
403 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
404 Self {
405 wasm_pages,
406 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
407 }
408 }
409}
410
411#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
416#[serde(deny_unknown_fields)]
417pub struct DiagnosticGeneration {
418 pub generation: GenerationRecord,
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::{
426 declaration::AllocationDeclaration,
427 ledger::{AllocationHistory, AllocationRecord},
428 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
429 schema::SchemaMetadata,
430 };
431
432 #[test]
433 fn diagnostic_export_copies_ledger_records() {
434 let declaration = AllocationDeclaration::new(
435 "app.users.v1",
436 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
437 None,
438 SchemaMetadata::default(),
439 )
440 .expect("declaration");
441 let ledger = AllocationLedger {
442 current_generation: 3,
443 allocation_history: AllocationHistory::from_parts(
444 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
445 vec![GenerationRecord {
446 generation: 3,
447 parent_generation: 2,
448 runtime_fingerprint: Some("wasm:abc123".to_string()),
449 declaration_count: 1,
450 committed_at: None,
451 }],
452 ),
453 };
454
455 let export = DiagnosticExport::from_ledger(
456 &ledger,
457 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
458 );
459
460 assert_eq!(export.current_generation, 3);
461 assert_eq!(export.records.len(), 1);
462 assert_eq!(export.records[0].memory_size, None);
463 assert_eq!(export.generations.len(), 1);
464 assert_eq!(
465 export.ledger_anchor,
466 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
467 );
468 assert_eq!(export.commit_recovery, None);
469 }
470
471 #[test]
472 fn diagnostic_export_rejects_unknown_top_level_fields() {
473 use crate::test_cbor::Value;
474
475 let export = DiagnosticExport {
476 current_generation: 0,
477 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
478 records: Vec::new(),
479 generations: Vec::new(),
480 commit_recovery: None,
481 };
482 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
483 else {
484 panic!("diagnostic export encodes as a map");
485 };
486 crate::test_cbor::map_insert(
487 &mut map,
488 Value::Text("future_field".to_string()),
489 Value::Bool(true),
490 );
491 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
492
493 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
494 .expect_err("unknown diagnostic field must fail closed");
495
496 assert!(err.to_string().contains("future_field"));
497 }
498
499 #[test]
500 fn diagnostic_outcome_states_round_trip() {
501 let stable_cell = DiagnosticStableCell::new(
502 DiagnosticStableCellStatus::Corrupt {
503 failure: DiagnosticFailure::new(
504 DiagnosticCode::StableCell,
505 "bad stable-cell record",
506 ),
507 },
508 DiagnosticMemorySize::from_wasm_pages(1),
509 );
510 let range_authority = DiagnosticRangeAuthority::new(
511 Vec::new(),
512 Err(DiagnosticFailure::new(
513 DiagnosticCode::RangeAuthority,
514 "overlapping authority ranges",
515 )),
516 );
517 let check = DiagnosticCheck::failed(
518 DiagnosticCode::AllocationValidation,
519 "duplicate declaration",
520 );
521
522 for value in [DiagnosticCheck::passed(), check] {
523 let bytes = crate::test_cbor::to_vec(&value).expect("check bytes");
524 let decoded: DiagnosticCheck =
525 crate::test_cbor::from_slice(&bytes).expect("check round trip");
526 assert_eq!(decoded, value);
527 }
528
529 let bytes = crate::test_cbor::to_vec(&stable_cell).expect("stable-cell diagnostic bytes");
530 let decoded: DiagnosticStableCell =
531 crate::test_cbor::from_slice(&bytes).expect("stable-cell round trip");
532 assert_eq!(decoded, stable_cell);
533
534 let bytes = crate::test_cbor::to_vec(&range_authority).expect("range diagnostic bytes");
535 let decoded: DiagnosticRangeAuthority =
536 crate::test_cbor::from_slice(&bytes).expect("range round trip");
537 assert_eq!(decoded, range_authority);
538 }
539
540 #[test]
541 fn diagnostic_codes_have_stable_wire_names() {
542 let cases = [
543 (DiagnosticCode::EagerInit, "eager_init"),
544 (DiagnosticCode::DeclarationRegistry, "declaration_registry"),
545 (DiagnosticCode::RangeRegistry, "range_registry"),
546 (DiagnosticCode::RangeAuthority, "range_authority"),
547 (DiagnosticCode::DeclarationSnapshot, "declaration_snapshot"),
548 (DiagnosticCode::StableCell, "stable_cell"),
549 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
550 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
551 (DiagnosticCode::GenesisLedger, "genesis_ledger"),
552 (
553 DiagnosticCode::AllocationValidation,
554 "allocation_validation",
555 ),
556 ];
557
558 for (code, expected) in cases {
559 assert_eq!(
560 crate::test_cbor::to_value(code).expect("diagnostic code value"),
561 crate::test_cbor::Value::Text(expected.to_string())
562 );
563 }
564 }
565
566 #[test]
567 fn diagnostic_export_can_include_commit_recovery_state() {
568 let ledger = AllocationLedger {
569 current_generation: 3,
570 allocation_history: AllocationHistory::default(),
571 };
572 let commit_recovery = CommitStoreDiagnostic {
573 slot0: CommitSlotDiagnostic::Valid { generation: 3 },
574 slot1: CommitSlotDiagnostic::Empty,
575 recovery: Ok(3),
576 };
577
578 let export = DiagnosticExport::from_ledger_with_commit_recovery(
579 &ledger,
580 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
581 Some(commit_recovery),
582 );
583
584 assert_eq!(export.commit_recovery, Some(commit_recovery));
585 }
586
587 #[test]
588 fn diagnostic_export_can_include_memory_sizes() {
589 let declaration = AllocationDeclaration::new(
590 "app.users.v1",
591 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
592 None,
593 SchemaMetadata::default(),
594 )
595 .expect("declaration");
596 let ledger = AllocationLedger {
597 current_generation: 3,
598 allocation_history: AllocationHistory::from_parts(
599 vec![AllocationRecord::active(3, declaration).expect("valid schema metadata")],
600 Vec::new(),
601 ),
602 };
603
604 let export = DiagnosticExport::from_ledger_with_memory_sizes(
605 &ledger,
606 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
607 [(
608 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
609 DiagnosticMemorySize::from_wasm_pages(2),
610 )],
611 );
612
613 assert_eq!(
614 export.records[0].memory_size,
615 Some(DiagnosticMemorySize {
616 wasm_pages: 2,
617 bytes: 131_072,
618 })
619 );
620 }
621
622 #[test]
623 fn diagnostic_export_can_report_recovery_failure() {
624 let ledger = AllocationLedger {
625 current_generation: 0,
626 allocation_history: AllocationHistory::default(),
627 };
628 let commit_recovery = CommitStoreDiagnostic {
629 slot0: CommitSlotDiagnostic::Empty,
630 slot1: CommitSlotDiagnostic::Empty,
631 recovery: Err(CommitRecoveryError::NoValidGeneration),
632 };
633
634 let export = DiagnosticExport::from_ledger_with_commit_recovery(
635 &ledger,
636 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
637 Some(commit_recovery),
638 );
639
640 assert_eq!(
641 export.commit_recovery.expect("commit recovery").recovery,
642 Err(CommitRecoveryError::NoValidGeneration)
643 );
644 }
645}