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