Skip to main content

ic_memory/runtime/
diagnostics.rs

1use super::{MemoryRuntime, RuntimeDiagnosticError, RuntimeLifecycle};
2use crate::{
3    DiagnosticCheck, DiagnosticCode, DiagnosticExport, DiagnosticFailure, DiagnosticMemorySize,
4    DiagnosticRuntimeBinding, DiagnosticStableCell, DiagnosticStableCellStatus, LedgerCommitError,
5    LedgerCommitStore, MemoryRuntimeDoctorReport, PolicyIdentity, RecoveredLedger,
6    RuntimeBootstrapPolicy, physical::CommitSnapshotDiagnostic,
7    requests::SealedDeclarationSnapshot, slot::MEMORY_MANAGER_LEDGER_ID,
8    stable_cell::decode_stable_cell_commit_store_from_memory,
9};
10use ic_stable_structures::{Memory, memory_manager::MemoryId};
11use std::{borrow::Cow, fmt::Display};
12
13impl<M: Memory> MemoryRuntime<M> {
14    /// Export this runtime's recovered ledger and live virtual-memory sizes.
15    pub fn diagnostic_export(&self) -> Result<DiagnosticExport, RuntimeDiagnosticError> {
16        if !self.is_bootstrapped() {
17            return Err(RuntimeDiagnosticError::NotBootstrapped);
18        }
19        let recovered = self.ledger_record_from_memory()?.recover()?;
20        Ok(self.recovered_diagnostic_export(recovered))
21    }
22
23    /// Diagnose protected commit recovery from this runtime's ledger memory.
24    ///
25    /// This operation is available before bootstrap when the stable-cell
26    /// envelope is readable or the ledger memory is empty.
27    pub fn commit_recovery_diagnostic(
28        &self,
29    ) -> Result<CommitSnapshotDiagnostic, RuntimeDiagnosticError> {
30        let record = self.ledger_record_from_memory()?;
31        Ok(record.diagnostic())
32    }
33
34    /// Build preflight and lifecycle diagnostics for this runtime.
35    ///
36    /// Validation checks the supplied declarations and allocation policy only.
37    /// It does not execute `prepare_bootstrap`, predict its completed set, or
38    /// certify consumer admission. Diagnostics never replay preparation.
39    #[must_use]
40    pub fn doctor_report<P>(
41        &self,
42        declarations: &SealedDeclarationSnapshot,
43        pool: &crate::MemoryAllocationPool,
44        policy: &P,
45    ) -> MemoryRuntimeDoctorReport
46    where
47        P: RuntimeBootstrapPolicy,
48        P::Error: Display,
49    {
50        let stable_cell = self.stable_cell_diagnostic();
51        // Recovery owns its ledger and diagnostic evidence. Release the decoded
52        // physical snapshot before observing memory or invoking custom policy.
53        let recovery = stable_cell
54            .record
55            .map(|record| record.recover_with_diagnostic());
56        // Keep the original observation boundary before policy callbacks, without
57        // copying the recovered keys/records merely to retain those observations.
58        let mut observed_pages = [0; crate::constants::MAX_ALLOCATIONS];
59        if let Some((Ok(recovered), _)) = &recovery {
60            for (record, pages) in recovered.ledger().records().iter().zip(&mut observed_pages) {
61                *pages = self.memory_size_pages(record.slot().id());
62            }
63        }
64        let tested_policy_identity = policy
65            .runtime_bootstrap_identity()
66            .map_err(|err| DiagnosticFailure::new(DiagnosticCode::PolicyIdentity, err.to_string()));
67        let established_bootstrap_binding = self.established_bootstrap_binding();
68        let bootstrap_binding = diagnostic_bootstrap_binding(
69            &tested_policy_identity,
70            declarations,
71            pool,
72            &self.lifecycle,
73        );
74        let validation = match &tested_policy_identity {
75            Ok(_) => diagnostic_validation(
76                self,
77                declarations,
78                pool,
79                policy,
80                recovery.as_ref().map(|(recovered, _)| recovered),
81            ),
82            Err(failure) => DiagnosticCheck::not_run(failure.code, failure.message.clone()),
83        };
84        let commit_recovery = recovery.as_ref().map(|(_, diagnostic)| *diagnostic);
85        let ledger = recovery.and_then(|(recovered, _)| {
86            recovered.ok().map(|recovered| {
87                let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
88                for (record, pages) in export.records.iter_mut().zip(&observed_pages) {
89                    record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(*pages));
90                }
91                export
92            })
93        });
94
95        MemoryRuntimeDoctorReport {
96            bootstrapped: self.is_bootstrapped(),
97            tested_policy_identity,
98            established_bootstrap_binding,
99            bootstrap_binding,
100            stable_cell: stable_cell.diagnostic,
101            commit_recovery,
102            ledger,
103            requests: declarations.requests().to_vec(),
104            allocation_pool: pool.clone(),
105            validation,
106        }
107    }
108
109    fn recovered_diagnostic_export(&self, recovered: RecoveredLedger) -> DiagnosticExport {
110        let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
111        for record in &mut export.records {
112            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
113                self.memory_size_pages(record.allocation.slot().id()),
114            ));
115        }
116        export
117    }
118
119    fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
120        match &self.lifecycle {
121            RuntimeLifecycle::Unbootstrapped => None,
122            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
123                binding.policy_identity.clone(),
124                binding.pool.clone(),
125            )),
126        }
127    }
128
129    fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
130        let memory = self
131            .memory_manager
132            .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
133        let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
134        match decode_stable_cell_commit_store_from_memory(&memory) {
135            Ok(record) => StableCellDiagnostic {
136                diagnostic: DiagnosticStableCell::new(
137                    if memory_size.wasm_pages == 0 {
138                        DiagnosticStableCellStatus::Empty
139                    } else {
140                        DiagnosticStableCellStatus::Readable
141                    },
142                    memory_size,
143                ),
144                record: Some(record),
145            },
146            Err(err) => StableCellDiagnostic {
147                diagnostic: DiagnosticStableCell::new(
148                    DiagnosticStableCellStatus::Corrupt {
149                        failure: DiagnosticFailure::new(
150                            DiagnosticCode::StableCell,
151                            err.to_string(),
152                        ),
153                    },
154                    memory_size,
155                ),
156                record: None,
157            },
158        }
159    }
160}
161
162struct StableCellDiagnostic {
163    diagnostic: DiagnosticStableCell,
164    record: Option<LedgerCommitStore>,
165}
166
167fn diagnostic_validation<M: Memory, P: RuntimeBootstrapPolicy>(
168    runtime: &MemoryRuntime<M>,
169    declarations: &SealedDeclarationSnapshot,
170    pool: &crate::MemoryAllocationPool,
171    custom_policy: &P,
172    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
173) -> DiagnosticCheck
174where
175    P::Error: Display,
176{
177    let recovered = match diagnostic_validation_ledger(recovered) {
178        Ok(recovered) => recovered,
179        Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
180    };
181    let occupied = recovered.ledger().occupied_slots();
182    if let Err(error) = runtime.validate_pool_custody(&occupied, pool) {
183        return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, error.to_string());
184    }
185    let resolved = match declarations.resolve(&recovered, &occupied, Vec::new(), pool) {
186        Ok(resolved) => resolved,
187        Err(err) => {
188            return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
189        }
190    };
191    match crate::validation::check_allocations(&recovered, &resolved, custom_policy) {
192        Ok(()) => DiagnosticCheck::passed(),
193        Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
194    }
195}
196
197fn diagnostic_bootstrap_binding(
198    tested_policy_identity: &Result<PolicyIdentity, DiagnosticFailure>,
199    declarations: &SealedDeclarationSnapshot,
200    pool: &crate::MemoryAllocationPool,
201    lifecycle: &RuntimeLifecycle,
202) -> DiagnosticCheck {
203    let tested_policy_identity = match tested_policy_identity {
204        Ok(identity) => identity,
205        Err(failure) => {
206            return DiagnosticCheck::not_run(failure.code, failure.message.clone());
207        }
208    };
209    let RuntimeLifecycle::Bootstrapped { binding, .. } = lifecycle else {
210        return DiagnosticCheck::not_run(
211            DiagnosticCode::RuntimeBinding,
212            "runtime has not completed bootstrap",
213        );
214    };
215    match binding.validate::<std::convert::Infallible>(declarations, pool, tested_policy_identity) {
216        Ok(()) => DiagnosticCheck::passed(),
217        Err(_) => DiagnosticCheck::failed(
218            DiagnosticCode::RuntimeBinding,
219            "tested policy/declaration/pool binding differs from established runtime binding",
220        ),
221    }
222}
223
224pub(super) fn diagnostic_validation_ledger(
225    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
226) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
227    if let Some(Ok(recovered)) = recovered {
228        return Ok(Cow::Borrowed(recovered));
229    }
230    if let Some(Err(err)) = recovered {
231        if let Some(genesis) = RecoveredLedger::genesis_for_absent_snapshot(err) {
232            return Ok(Cow::Owned(genesis));
233        }
234        let code = if matches!(
235            err,
236            LedgerCommitError::Recovery(crate::CommitRecoveryError::UnsupportedFormat { .. })
237        ) {
238            DiagnosticCode::UnsupportedFormat
239        } else {
240            DiagnosticCode::LedgerRecovery
241        };
242        return Err(DiagnosticFailure::new(
243            code,
244            format!("protected ledger recovery: {err}"),
245        ));
246    }
247    Err(DiagnosticFailure::new(
248        DiagnosticCode::StableCell,
249        "stable-cell ledger record is not readable",
250    ))
251}