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)]
16pub struct DiagnosticExport {
17 pub current_generation: u64,
19 pub ledger_anchor: AllocationSlotDescriptor,
21 pub records: Vec<DiagnosticRecord>,
23 pub generations: Vec<DiagnosticGeneration>,
25 pub commit_recovery: Option<CommitStoreDiagnostic>,
27}
28
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(deny_unknown_fields)]
42pub struct DefaultMemoryManagerDoctorReport {
43 pub bootstrapped: bool,
45 pub ledger_anchor: AllocationSlotDescriptor,
47 pub stable_cell: DiagnosticStableCell,
49 pub commit_recovery: Option<CommitStoreDiagnostic>,
51 pub ledger: Option<DiagnosticExport>,
53 pub registered_declarations: Vec<DiagnosticDeclaration>,
55 pub range_authority: DiagnosticRangeAuthority,
58 pub validation: DiagnosticCheck,
64}
65
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73#[serde(deny_unknown_fields)]
74pub struct DiagnosticDeclaration {
75 pub declaring_crate: String,
77 pub declaration: AllocationDeclaration,
79}
80
81impl DiagnosticDeclaration {
82 #[must_use]
84 pub fn new(declaring_crate: impl Into<String>, declaration: AllocationDeclaration) -> Self {
85 Self {
86 declaring_crate: declaring_crate.into(),
87 declaration,
88 }
89 }
90}
91
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
99#[serde(deny_unknown_fields)]
100pub struct DiagnosticRangeAuthority {
101 pub registered_records: Vec<MemoryManagerAuthorityRecord>,
103 pub effective_authority: Option<MemoryManagerRangeAuthority>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub error: Option<String>,
109}
110
111impl DiagnosticRangeAuthority {
112 #[must_use]
114 pub const fn new(
115 registered_records: Vec<MemoryManagerAuthorityRecord>,
116 effective_authority: Option<MemoryManagerRangeAuthority>,
117 error: Option<String>,
118 ) -> Self {
119 Self {
120 registered_records,
121 effective_authority,
122 error,
123 }
124 }
125}
126
127#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
134#[serde(deny_unknown_fields)]
135pub struct DiagnosticStableCell {
136 pub status: DiagnosticStableCellStatus,
138 pub memory_size: DiagnosticMemorySize,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub error: Option<String>,
143}
144
145impl DiagnosticStableCell {
146 #[must_use]
148 pub const fn new(
149 status: DiagnosticStableCellStatus,
150 memory_size: DiagnosticMemorySize,
151 error: Option<String>,
152 ) -> Self {
153 Self {
154 status,
155 memory_size,
156 error,
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
168pub enum DiagnosticStableCellStatus {
169 Empty,
171 Readable,
173 Corrupt,
176}
177
178#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
185#[serde(deny_unknown_fields)]
186pub struct DiagnosticCheck {
187 pub status: DiagnosticCheckStatus,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub message: Option<String>,
192}
193
194impl DiagnosticCheck {
195 #[must_use]
197 pub const fn passed() -> Self {
198 Self {
199 status: DiagnosticCheckStatus::Passed,
200 message: None,
201 }
202 }
203
204 #[must_use]
206 pub fn failed(message: impl Into<String>) -> Self {
207 Self {
208 status: DiagnosticCheckStatus::Failed,
209 message: Some(message.into()),
210 }
211 }
212
213 #[must_use]
215 pub fn not_run(message: impl Into<String>) -> Self {
216 Self {
217 status: DiagnosticCheckStatus::NotRun,
218 message: Some(message.into()),
219 }
220 }
221}
222
223#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
230pub enum DiagnosticCheckStatus {
231 NotRun,
233 Passed,
235 Failed,
237}
238
239impl DiagnosticExport {
240 #[must_use]
242 pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
243 Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
244 }
245
246 #[must_use]
248 pub fn from_ledger_with_commit_recovery(
249 ledger: &AllocationLedger,
250 ledger_anchor: AllocationSlotDescriptor,
251 commit_recovery: Option<CommitStoreDiagnostic>,
252 ) -> Self {
253 Self::from_ledger_with_commit_recovery_and_memory_sizes(
254 ledger,
255 ledger_anchor,
256 commit_recovery,
257 std::iter::empty(),
258 )
259 }
260
261 #[must_use]
263 pub fn from_ledger_with_memory_sizes(
264 ledger: &AllocationLedger,
265 ledger_anchor: AllocationSlotDescriptor,
266 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
267 ) -> Self {
268 Self::from_ledger_with_commit_recovery_and_memory_sizes(
269 ledger,
270 ledger_anchor,
271 None,
272 memory_sizes,
273 )
274 }
275
276 #[must_use]
278 pub fn from_ledger_with_commit_recovery_and_memory_sizes(
279 ledger: &AllocationLedger,
280 ledger_anchor: AllocationSlotDescriptor,
281 commit_recovery: Option<CommitStoreDiagnostic>,
282 memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
283 ) -> Self {
284 let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
285 Self {
286 current_generation: ledger.current_generation,
287 ledger_anchor,
288 records: ledger
289 .allocation_history()
290 .records()
291 .iter()
292 .cloned()
293 .map(|allocation| {
294 let memory_size = memory_sizes.get(allocation.slot()).copied();
295 DiagnosticRecord {
296 allocation,
297 memory_size,
298 }
299 })
300 .collect(),
301 generations: ledger
302 .allocation_history()
303 .generations()
304 .iter()
305 .cloned()
306 .map(|generation| DiagnosticGeneration { generation })
307 .collect(),
308 commit_recovery,
309 }
310 }
311}
312
313#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318pub struct DiagnosticRecord {
319 pub allocation: AllocationRecord,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub memory_size: Option<DiagnosticMemorySize>,
327}
328
329#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
336#[serde(deny_unknown_fields)]
337pub struct DiagnosticMemorySize {
338 pub wasm_pages: u64,
340 pub bytes: u64,
342}
343
344impl DiagnosticMemorySize {
345 #[must_use]
347 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
348 Self {
349 wasm_pages,
350 bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
351 }
352 }
353}
354
355#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
360pub struct DiagnosticGeneration {
361 pub generation: GenerationRecord,
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::{
369 declaration::AllocationDeclaration,
370 ledger::{AllocationHistory, AllocationRecord, AllocationState},
371 physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
372 schema::SchemaMetadata,
373 };
374
375 #[test]
376 fn diagnostic_export_copies_ledger_records() {
377 let declaration = AllocationDeclaration::new(
378 "app.users.v1",
379 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
380 None,
381 SchemaMetadata::default(),
382 )
383 .expect("declaration");
384 let ledger = AllocationLedger {
385 current_generation: 3,
386 allocation_history: AllocationHistory::from_parts(
387 vec![AllocationRecord::from_declaration(
388 3,
389 declaration,
390 AllocationState::Active,
391 )],
392 vec![GenerationRecord {
393 generation: 3,
394 parent_generation: 2,
395 runtime_fingerprint: Some("wasm:abc123".to_string()),
396 declaration_count: 1,
397 committed_at: None,
398 }],
399 ),
400 };
401
402 let export = DiagnosticExport::from_ledger(
403 &ledger,
404 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
405 );
406
407 assert_eq!(export.current_generation, 3);
408 assert_eq!(export.records.len(), 1);
409 assert_eq!(export.records[0].memory_size, None);
410 assert_eq!(export.generations.len(), 1);
411 assert_eq!(
412 export.ledger_anchor,
413 AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
414 );
415 assert_eq!(export.commit_recovery, None);
416 }
417
418 #[test]
419 fn diagnostic_export_can_include_commit_recovery_state() {
420 let ledger = AllocationLedger {
421 current_generation: 3,
422 allocation_history: AllocationHistory::default(),
423 };
424 let commit_recovery = CommitStoreDiagnostic {
425 slot0: CommitSlotDiagnostic {
426 present: true,
427 generation: Some(3),
428 valid: true,
429 },
430 slot1: CommitSlotDiagnostic {
431 present: false,
432 generation: None,
433 valid: false,
434 },
435 authoritative_generation: Some(3),
436 recovery_error: None,
437 };
438
439 let export = DiagnosticExport::from_ledger_with_commit_recovery(
440 &ledger,
441 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
442 Some(commit_recovery),
443 );
444
445 assert_eq!(export.commit_recovery, Some(commit_recovery));
446 }
447
448 #[test]
449 fn diagnostic_export_can_include_memory_sizes() {
450 let declaration = AllocationDeclaration::new(
451 "app.users.v1",
452 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
453 None,
454 SchemaMetadata::default(),
455 )
456 .expect("declaration");
457 let ledger = AllocationLedger {
458 current_generation: 3,
459 allocation_history: AllocationHistory::from_parts(
460 vec![AllocationRecord::from_declaration(
461 3,
462 declaration,
463 AllocationState::Active,
464 )],
465 Vec::new(),
466 ),
467 };
468
469 let export = DiagnosticExport::from_ledger_with_memory_sizes(
470 &ledger,
471 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
472 [(
473 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
474 DiagnosticMemorySize::from_wasm_pages(2),
475 )],
476 );
477
478 assert_eq!(
479 export.records[0].memory_size,
480 Some(DiagnosticMemorySize {
481 wasm_pages: 2,
482 bytes: 131_072,
483 })
484 );
485 }
486
487 #[test]
488 fn diagnostic_export_can_report_recovery_failure() {
489 let ledger = AllocationLedger {
490 current_generation: 0,
491 allocation_history: AllocationHistory::default(),
492 };
493 let commit_recovery = CommitStoreDiagnostic {
494 slot0: CommitSlotDiagnostic {
495 present: false,
496 generation: None,
497 valid: false,
498 },
499 slot1: CommitSlotDiagnostic {
500 present: false,
501 generation: None,
502 valid: false,
503 },
504 authoritative_generation: None,
505 recovery_error: Some(CommitRecoveryError::NoValidGeneration),
506 };
507
508 let export = DiagnosticExport::from_ledger_with_commit_recovery(
509 &ledger,
510 AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
511 Some(commit_recovery),
512 );
513
514 assert_eq!(
515 export
516 .commit_recovery
517 .expect("commit recovery")
518 .recovery_error,
519 Some(CommitRecoveryError::NoValidGeneration)
520 );
521 }
522}