Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, policy::NoopPolicy};
2use crate::{
3    AllocationHistory, AllocationLedger, AllocationSlotDescriptor, DiagnosticCheck, DiagnosticCode,
4    DiagnosticDeclaration, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
5    DiagnosticRangeAuthority, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
6    LedgerPayloadEnvelopeError, MemoryRuntimeDoctorReport, StableCellLedgerRecord,
7    physical::CommitStoreDiagnostic, registry::SealedDeclarationSnapshot,
8    slot::MEMORY_MANAGER_LEDGER_ID, stable_cell::decode_stable_cell_ledger_record_from_memory,
9};
10use ic_stable_structures::Memory;
11
12impl<M: Memory> MemoryRuntime<M> {
13    /// Export this runtime's recovered ledger and live virtual-memory sizes.
14    pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
15        if !self.is_bootstrapped() {
16            return Err(RuntimeDiagnosticError::NotBootstrapped);
17        }
18        let record = self.ledger_record_from_memory()?;
19        let recovered = record.store().recover()?;
20        let ledger = recovered.ledger();
21        Ok(
22            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
23                ledger,
24                ledger_anchor_descriptor(),
25                Some(record.store().physical().diagnostic()),
26                self.memory_sizes(ledger)?,
27            ),
28        )
29    }
30
31    /// Diagnose protected commit recovery from this runtime's ledger memory.
32    ///
33    /// This operation is available before bootstrap when the stable-cell
34    /// envelope is readable or the ledger memory is empty.
35    pub fn commit_recovery_diagnostic(
36        &self,
37    ) -> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
38        let record = self.ledger_record_from_memory()?;
39        Ok(record.store().physical().diagnostic())
40    }
41
42    /// Build preflight and lifecycle diagnostics for this runtime.
43    #[must_use]
44    pub fn doctor_report(
45        &self,
46        declarations: &SealedDeclarationSnapshot,
47    ) -> MemoryRuntimeDoctorReport {
48        let stable_cell = self.stable_cell_diagnostic();
49        let commit_recovery = stable_cell
50            .record
51            .as_ref()
52            .map(|record| record.store().physical().diagnostic());
53        let recovered = stable_cell
54            .record
55            .as_ref()
56            .map(|record| record.store().recover());
57        let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
58        let ledger = recovered_for_export.map(|recovered| {
59            DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
60                recovered.ledger(),
61                ledger_anchor_descriptor(),
62                commit_recovery,
63                self.memory_sizes_lossy(recovered.ledger()),
64            )
65        });
66        let diagnostic_declarations = declarations
67            .registered_declarations()
68            .iter()
69            .map(|registration| {
70                DiagnosticDeclaration::new(
71                    registration.authority(),
72                    registration.declaration().clone(),
73                )
74            })
75            .collect();
76        let registered_records = declarations
77            .registered_ranges()
78            .iter()
79            .map(|registration| registration.record().clone())
80            .collect();
81        let range_authority = DiagnosticRangeAuthority::new(
82            registered_records,
83            Ok(declarations.range_authority().clone()),
84        );
85        let validation = diagnostic_validation(
86            declarations,
87            stable_cell.record.as_ref(),
88            recovered.as_ref(),
89        );
90
91        MemoryRuntimeDoctorReport {
92            bootstrapped: self.is_bootstrapped(),
93            ledger_anchor: ledger_anchor_descriptor(),
94            stable_cell: stable_cell.diagnostic,
95            commit_recovery,
96            ledger,
97            registered_declarations: diagnostic_declarations,
98            range_authority,
99            validation,
100        }
101    }
102
103    fn memory_sizes(
104        &self,
105        ledger: &AllocationLedger,
106    ) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
107        ledger
108            .allocation_history()
109            .records()
110            .iter()
111            .map(|record| {
112                let id = record.slot().memory_manager_id()?;
113                Ok((
114                    record.slot().clone(),
115                    DiagnosticMemorySize::from_wasm_pages(self.memory(id).size()),
116                ))
117            })
118            .collect()
119    }
120
121    fn memory_sizes_lossy(
122        &self,
123        ledger: &AllocationLedger,
124    ) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)> {
125        self.memory_sizes(ledger).unwrap_or_default()
126    }
127
128    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
129        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
130        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
131        if memory.size() == 0 {
132            return StableCellDiagnostic {
133                diagnostic: DiagnosticStableCell::new(
134                    DiagnosticStableCellStatus::Empty,
135                    memory_size,
136                ),
137                record: Some(StableCellLedgerRecord::default()),
138            };
139        }
140
141        match decode_stable_cell_ledger_record_from_memory(&memory) {
142            Ok(record) => StableCellDiagnostic {
143                diagnostic: DiagnosticStableCell::new(
144                    DiagnosticStableCellStatus::Readable,
145                    memory_size,
146                ),
147                record: Some(record),
148            },
149            Err(err) => StableCellDiagnostic {
150                diagnostic: DiagnosticStableCell::new(
151                    DiagnosticStableCellStatus::Corrupt {
152                        failure: DiagnosticFailure::new(
153                            DiagnosticCode::StableCell,
154                            err.to_string(),
155                        ),
156                    },
157                    memory_size,
158                ),
159                record: None,
160            },
161        }
162    }
163}
164
165struct StableCellDiagnostic {
166    diagnostic: DiagnosticStableCell,
167    record: Option<StableCellLedgerRecord>,
168}
169
170const fn ledger_anchor_descriptor() -> AllocationSlotDescriptor {
171    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
172}
173
174fn diagnostic_validation(
175    declarations: &SealedDeclarationSnapshot,
176    stable_cell_record: Option<&StableCellLedgerRecord>,
177    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
178) -> DiagnosticCheck {
179    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
180        Ok(recovered) => recovered,
181        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
182    };
183    let policy = super::policy::RuntimeMemoryManagerPolicy {
184        declarations,
185        custom_policy: &NoopPolicy,
186    };
187    match crate::validate_allocations(
188        &recovered,
189        declarations.allocation_snapshot().clone(),
190        &policy,
191    ) {
192        Ok(_) => DiagnosticCheck::passed(),
193        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
194    }
195}
196
197pub(super) fn diagnostic_validation_ledger(
198    stable_cell_record: Option<&StableCellLedgerRecord>,
199    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
200) -> Result<crate::RecoveredLedger, DiagnosticFailure> {
201    if let Some(Ok(recovered)) = recovered {
202        return Ok(recovered.clone());
203    }
204    if let Some(Err(err)) = recovered {
205        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
206            return diagnostic_genesis_recovered_ledger();
207        }
208        let code = if matches!(
209            err,
210            LedgerCommitError::PayloadEnvelope(
211                LedgerPayloadEnvelopeError::UnsupportedFormat { .. }
212            )
213        ) {
214            DiagnosticCode::UnsupportedFormat
215        } else {
216            DiagnosticCode::LedgerRecovery
217        };
218        return Err(DiagnosticFailure::new(
219            code,
220            format!("protected ledger recovery: {err}"),
221        ));
222    }
223    if stable_cell_record.is_some() {
224        return diagnostic_genesis_recovered_ledger();
225    }
226    Err(DiagnosticFailure::new(
227        DiagnosticCode::StableCell,
228        "stable-cell ledger record is not readable",
229    ))
230}
231
232fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, DiagnosticFailure> {
233    AllocationLedger::new(0, AllocationHistory::default())
234        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
235        .map_err(|err| {
236            DiagnosticFailure::new(
237                DiagnosticCode::GenesisLedger,
238                format!("genesis ledger: {err}"),
239            )
240        })
241}