Skip to main content

ic_memory/
diagnostics.rs

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///
12/// DiagnosticExport
13///
14/// Read-only machine-readable allocation ledger export.
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16pub struct DiagnosticExport {
17    /// Current committed generation.
18    pub current_generation: u64,
19    /// Ledger anchor descriptor.
20    pub ledger_anchor: AllocationSlotDescriptor,
21    /// Allocation records.
22    pub records: Vec<DiagnosticRecord>,
23    /// Generation records.
24    pub generations: Vec<DiagnosticGeneration>,
25    /// Optional protected commit recovery diagnostic.
26    pub commit_recovery: Option<CommitStoreDiagnostic>,
27}
28
29///
30/// DefaultMemoryManagerDoctorReport
31///
32/// Preflight and runtime diagnostic report for the default `MemoryManager`
33/// integration.
34///
35/// This report is intended for operator-facing diagnostics. Recoverable
36/// runtime problems, such as corrupt stable-cell bytes or commit recovery
37/// failure, are represented as fields instead of aborting report construction.
38///
39
40#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
41#[serde(deny_unknown_fields)]
42pub struct DefaultMemoryManagerDoctorReport {
43    /// Whether the default runtime has completed bootstrap validation.
44    pub bootstrapped: bool,
45    /// Ledger anchor descriptor used by the default runtime.
46    pub ledger_anchor: AllocationSlotDescriptor,
47    /// Stable-cell ledger storage status.
48    pub stable_cell: DiagnosticStableCell,
49    /// Protected commit recovery status when a ledger record was readable.
50    pub commit_recovery: Option<CommitStoreDiagnostic>,
51    /// Recovered allocation ledger export when protected recovery succeeded.
52    pub ledger: Option<DiagnosticExport>,
53    /// Static declarations registered by linked crates.
54    pub registered_declarations: Vec<DiagnosticDeclaration>,
55    /// Static range authority registered by linked crates and the effective
56    /// authority table used by the default runtime.
57    pub range_authority: DiagnosticRangeAuthority,
58    /// Current generic default-runtime declaration validation preflight result.
59    ///
60    /// Caller-supplied policies passed to
61    /// [`crate::bootstrap_default_memory_manager_with_policy`] are not
62    /// represented in this check.
63    pub validation: DiagnosticCheck,
64}
65
66///
67/// DiagnosticDeclaration
68///
69/// Read-only diagnostic view of one static allocation declaration.
70///
71
72#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73#[serde(deny_unknown_fields)]
74pub struct DiagnosticDeclaration {
75    /// Crate or integration authority that registered the declaration.
76    pub declaring_crate: String,
77    /// Allocation declaration registered by that authority.
78    pub declaration: AllocationDeclaration,
79}
80
81impl DiagnosticDeclaration {
82    /// Build a diagnostic declaration record.
83    #[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///
93/// DiagnosticRangeAuthority
94///
95/// Read-only diagnostic view of registered and effective range authority.
96///
97
98#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
99#[serde(deny_unknown_fields)]
100pub struct DiagnosticRangeAuthority {
101    /// Range records registered directly by linked crates.
102    pub registered_records: Vec<MemoryManagerAuthorityRecord>,
103    /// Effective range authority table, including runtime-owned internal
104    /// records, when the table validated successfully.
105    pub effective_authority: Option<MemoryManagerRangeAuthority>,
106    /// Validation error when the effective authority table could not be built.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub error: Option<String>,
109}
110
111impl DiagnosticRangeAuthority {
112    /// Build a range-authority diagnostic.
113    #[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///
128/// DiagnosticStableCell
129///
130/// Read-only diagnostic view of the stable-cell ledger storage envelope.
131///
132
133#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
134#[serde(deny_unknown_fields)]
135pub struct DiagnosticStableCell {
136    /// Stable-cell status.
137    pub status: DiagnosticStableCellStatus,
138    /// Backing memory size for the ledger cell.
139    pub memory_size: DiagnosticMemorySize,
140    /// Decode error when the stable cell was not readable.
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub error: Option<String>,
143}
144
145impl DiagnosticStableCell {
146    /// Build a stable-cell diagnostic.
147    #[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///
162/// DiagnosticStableCellStatus
163///
164/// Stable-cell ledger storage status.
165///
166
167#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
168pub enum DiagnosticStableCellStatus {
169    /// The ledger memory is empty and can be initialized.
170    Empty,
171    /// The stable-cell envelope and ledger record decoded successfully.
172    Readable,
173    /// The ledger memory is present but could not be decoded as the expected
174    /// stable-cell ledger record.
175    Corrupt,
176}
177
178///
179/// DiagnosticCheck
180///
181/// Read-only diagnostic status for a preflight check.
182///
183
184#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
185#[serde(deny_unknown_fields)]
186pub struct DiagnosticCheck {
187    /// Check status.
188    pub status: DiagnosticCheckStatus,
189    /// Failure or skip reason.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub message: Option<String>,
192}
193
194impl DiagnosticCheck {
195    /// Build a passed diagnostic check.
196    #[must_use]
197    pub const fn passed() -> Self {
198        Self {
199            status: DiagnosticCheckStatus::Passed,
200            message: None,
201        }
202    }
203
204    /// Build a failed diagnostic check.
205    #[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    /// Build a skipped diagnostic check.
214    #[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///
224/// DiagnosticCheckStatus
225///
226/// Status for one diagnostic preflight check.
227///
228
229#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
230pub enum DiagnosticCheckStatus {
231    /// The check could not run because prerequisite state was unavailable.
232    NotRun,
233    /// The check completed successfully.
234    Passed,
235    /// The check ran and found a problem.
236    Failed,
237}
238
239impl DiagnosticExport {
240    /// Build a read-only diagnostic export from an allocation ledger.
241    #[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    /// Build a read-only diagnostic export with protected commit recovery state.
247    #[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    /// Build a read-only diagnostic export with live memory sizes.
262    #[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    /// Build a read-only diagnostic export with protected recovery state and live memory sizes.
277    #[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///
314/// DiagnosticRecord
315///
316/// Read-only diagnostic allocation record.
317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318pub struct DiagnosticRecord {
319    /// Allocation record.
320    pub allocation: AllocationRecord,
321    /// Live backing memory size, when the exporter measured one.
322    ///
323    /// This is allocation size reported by the backing memory, not logical user
324    /// payload size inside the stable structure.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub memory_size: Option<DiagnosticMemorySize>,
327}
328
329///
330/// DiagnosticMemorySize
331///
332/// Live size reported by a backing stable memory.
333///
334
335#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
336#[serde(deny_unknown_fields)]
337pub struct DiagnosticMemorySize {
338    /// WebAssembly pages reported by the memory.
339    pub wasm_pages: u64,
340    /// Bytes represented by the page count.
341    pub bytes: u64,
342}
343
344impl DiagnosticMemorySize {
345    /// Build a size from a WebAssembly page count.
346    #[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///
356/// DiagnosticGeneration
357///
358/// Read-only diagnostic generation record.
359#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
360pub struct DiagnosticGeneration {
361    /// Generation record.
362    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}