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 pub commit_recovery: Option<CommitStoreDiagnostic>,
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
42#[serde(deny_unknown_fields)]
43pub struct DefaultMemoryManagerDoctorReport {
44 pub bootstrapped: bool,
46 pub ledger_anchor: AllocationSlotDescriptor,
48 pub stable_cell: DiagnosticStableCell,
50 pub commit_recovery: Option<CommitStoreDiagnostic>,
52 pub ledger: Option<DiagnosticExport>,
54 pub registered_declarations: Vec<DiagnosticDeclaration>,
56 pub range_authority: DiagnosticRangeAuthority,
59 pub validation: DiagnosticCheck,
65}
66
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
74#[serde(deny_unknown_fields)]
75pub struct DiagnosticDeclaration {
76 pub authority: String,
78 pub declaration: AllocationDeclaration,
80}
81
82impl DiagnosticDeclaration {
83 #[must_use]
85 pub fn new(authority: impl Into<String>, declaration: AllocationDeclaration) -> Self {
86 Self {
87 authority: authority.into(),
88 declaration,
89 }
90 }
91}
92
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
100#[serde(deny_unknown_fields)]
101pub struct DiagnosticRangeAuthority {
102 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
104 pub effective_authority: Option<MemoryManagerRangeAuthority>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub error: Option<String>,
110}
111
112impl DiagnosticRangeAuthority {
113 #[must_use]
115 pub const fn new(
116 registered_records: Vec<MemoryManagerAuthorityRecord>,
117 effective_authority: Option<MemoryManagerRangeAuthority>,
118 error: Option<String>,
119 ) -> Self {
120 Self {
121 registered_records,
122 effective_authority,
123 error,
124 }
125 }
126}
127
128#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct DiagnosticStableCell {
137 pub status: DiagnosticStableCellStatus,
139 pub memory_size: DiagnosticMemorySize,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub error: Option<String>,
144}
145
146impl DiagnosticStableCell {
147 #[must_use]
149 pub const fn new(
150 status: DiagnosticStableCellStatus,
151 memory_size: DiagnosticMemorySize,
152 error: Option<String>,
153 ) -> Self {
154 Self {
155 status,
156 memory_size,
157 error,
158 }
159 }
160}
161
162#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
169pub enum DiagnosticStableCellStatus {
170 Empty,
172 Readable,
174 Corrupt,
177}
178
179#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
186#[serde(deny_unknown_fields)]
187pub struct DiagnosticCheck {
188 pub status: DiagnosticCheckStatus,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub message: Option<String>,
193}
194
195impl DiagnosticCheck {
196 #[must_use]
198 pub const fn passed() -> Self {
199 Self {
200 status: DiagnosticCheckStatus::Passed,
201 message: None,
202 }
203 }
204
205 #[must_use]
207 pub fn failed(message: impl Into<String>) -> Self {
208 Self {
209 status: DiagnosticCheckStatus::Failed,
210 message: Some(message.into()),
211 }
212 }
213
214 #[must_use]
216 pub fn not_run(message: impl Into<String>) -> Self {
217 Self {
218 status: DiagnosticCheckStatus::NotRun,
219 message: Some(message.into()),
220 }
221 }
222}
223
224#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
231pub enum DiagnosticCheckStatus {
232 NotRun,
234 Passed,
236 Failed,
238}
239
240impl DiagnosticExport {
241 #[must_use]
243 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
244 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
245 }
246
247 #[must_use]
249 pub fn from_ledger_with_commit_recovery(
250 ledger: &AllocationLedger,
251 ledger_anchor: AllocationSlotDescriptor,
252 commit_recovery: Option<CommitStoreDiagnostic>,
253 ) -> Self {
254 Self::from_ledger_with_commit_recovery_and_memory_sizes(
255 ledger,
256 ledger_anchor,
257 commit_recovery,
258 std::iter::empty(),
259 )
260 }
261
262 #[must_use]
264 pub fn from_ledger_with_memory_sizes(
265 ledger: &AllocationLedger,
266 ledger_anchor: AllocationSlotDescriptor,
267 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
268 ) -> Self {
269 Self::from_ledger_with_commit_recovery_and_memory_sizes(
270 ledger,
271 ledger_anchor,
272 None,
273 memory_sizes,
274 )
275 }
276
277 #[must_use]
279 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
280 ledger: &AllocationLedger,
281 ledger_anchor: AllocationSlotDescriptor,
282 commit_recovery: Option<CommitStoreDiagnostic>,
283 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
284 ) -> Self {
285 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
286 Self {
287 current_generation: ledger.current_generation,
288 ledger_anchor,
289 records: ledger
290 .allocation_history()
291 .records()
292 .iter()
293 .cloned()
294 .map(|allocation| {
295 let memory_size = memory_sizes.get(allocation.slot()).copied();
296 DiagnosticRecord {
297 allocation,
298 memory_size,
299 }
300 })
301 .collect(),
302 generations: ledger
303 .allocation_history()
304 .generations()
305 .iter()
306 .cloned()
307 .map(|generation| DiagnosticGeneration { generation })
308 .collect(),
309 commit_recovery,
310 }
311 }
312}
313
314#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
319#[serde(deny_unknown_fields)]
320pub struct DiagnosticRecord {
321 pub allocation: AllocationRecord,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub memory_size: Option<DiagnosticMemorySize>,
329}
330
331#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
338#[serde(deny_unknown_fields)]
339pub struct DiagnosticMemorySize {
340 pub wasm_pages: u64,
342 pub bytes: u64,
344}
345
346impl DiagnosticMemorySize {
347 #[must_use]
349 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
350 Self {
351 wasm_pages,
352 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
353 }
354 }
355}
356
357#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
362#[serde(deny_unknown_fields)]
363pub struct DiagnosticGeneration {
364 pub generation: GenerationRecord,
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use crate::{
372 declaration::AllocationDeclaration,
373 ledger::{AllocationHistory, AllocationRecord, AllocationState},
374 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
375 schema::SchemaMetadata,
376 };
377
378 #[test]
379 fn diagnostic_export_copies_ledger_records() {
380 let declaration = AllocationDeclaration::new(
381 "app.users.v1",
382 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
383 None,
384 SchemaMetadata::default(),
385 )
386 .expect("declaration");
387 let ledger = AllocationLedger {
388 current_generation: 3,
389 allocation_history: AllocationHistory::from_parts(
390 vec![
391 AllocationRecord::from_declaration(3, declaration, AllocationState::Active)
392 .expect("valid schema metadata"),
393 ],
394 vec![GenerationRecord {
395 generation: 3,
396 parent_generation: 2,
397 runtime_fingerprint: Some("wasm:abc123".to_string()),
398 declaration_count: 1,
399 committed_at: None,
400 }],
401 ),
402 };
403
404 let export = DiagnosticExport::from_ledger(
405 &ledger,
406 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
407 );
408
409 assert_eq!(export.current_generation, 3);
410 assert_eq!(export.records.len(), 1);
411 assert_eq!(export.records[0].memory_size, None);
412 assert_eq!(export.generations.len(), 1);
413 assert_eq!(
414 export.ledger_anchor,
415 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
416 );
417 assert_eq!(export.commit_recovery, None);
418 }
419
420 #[test]
421 fn diagnostic_export_rejects_unknown_top_level_fields() {
422 use crate::test_cbor::Value;
423
424 let export = DiagnosticExport {
425 current_generation: 0,
426 ledger_anchor: AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
427 records: Vec::new(),
428 generations: Vec::new(),
429 commit_recovery: None,
430 };
431 let Value::Map(mut map) = crate::test_cbor::to_value(export).expect("diagnostic value")
432 else {
433 panic!("diagnostic export encodes as a map");
434 };
435 crate::test_cbor::map_insert(
436 &mut map,
437 Value::Text("future_field".to_string()),
438 Value::Bool(true),
439 );
440 let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("diagnostic bytes");
441
442 let err = crate::test_cbor::from_slice::<DiagnosticExport>(&bytes)
443 .expect_err("unknown diagnostic field must fail closed");
444
445 assert!(err.to_string().contains("future_field"));
446 }
447
448 #[test]
449 fn diagnostic_export_can_include_commit_recovery_state() {
450 let ledger = AllocationLedger {
451 current_generation: 3,
452 allocation_history: AllocationHistory::default(),
453 };
454 let commit_recovery = CommitStoreDiagnostic {
455 slot0: CommitSlotDiagnostic {
456 present: true,
457 generation: Some(3),
458 valid: true,
459 },
460 slot1: CommitSlotDiagnostic {
461 present: false,
462 generation: None,
463 valid: false,
464 },
465 authoritative_generation: Some(3),
466 recovery_error: None,
467 };
468
469 let export = DiagnosticExport::from_ledger_with_commit_recovery(
470 &ledger,
471 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
472 Some(commit_recovery),
473 );
474
475 assert_eq!(export.commit_recovery, Some(commit_recovery));
476 }
477
478 #[test]
479 fn diagnostic_export_can_include_memory_sizes() {
480 let declaration = AllocationDeclaration::new(
481 "app.users.v1",
482 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
483 None,
484 SchemaMetadata::default(),
485 )
486 .expect("declaration");
487 let ledger = AllocationLedger {
488 current_generation: 3,
489 allocation_history: AllocationHistory::from_parts(
490 vec![
491 AllocationRecord::from_declaration(3, declaration, AllocationState::Active)
492 .expect("valid schema metadata"),
493 ],
494 Vec::new(),
495 ),
496 };
497
498 let export = DiagnosticExport::from_ledger_with_memory_sizes(
499 &ledger,
500 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
501 [(
502 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
503 DiagnosticMemorySize::from_wasm_pages(2),
504 )],
505 );
506
507 assert_eq!(
508 export.records[0].memory_size,
509 Some(DiagnosticMemorySize {
510 wasm_pages: 2,
511 bytes: 131_072,
512 })
513 );
514 }
515
516 #[test]
517 fn diagnostic_export_can_report_recovery_failure() {
518 let ledger = AllocationLedger {
519 current_generation: 0,
520 allocation_history: AllocationHistory::default(),
521 };
522 let commit_recovery = CommitStoreDiagnostic {
523 slot0: CommitSlotDiagnostic {
524 present: false,
525 generation: None,
526 valid: false,
527 },
528 slot1: CommitSlotDiagnostic {
529 present: false,
530 generation: None,
531 valid: false,
532 },
533 authoritative_generation: None,
534 recovery_error: Some(CommitRecoveryError::NoValidGeneration),
535 };
536
537 let export = DiagnosticExport::from_ledger_with_commit_recovery(
538 &ledger,
539 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
540 Some(commit_recovery),
541 );
542
543 assert_eq!(
544 export
545 .commit_recovery
546 .expect("commit recovery")
547 .recovery_error,
548 Some(CommitRecoveryError::NoValidGeneration)
549 );
550 }
551}