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 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 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 #[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 let recovery = stable_cell
54 .record
55 .map(|record| record.recover_with_diagnostic());
56 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.runtime_bootstrap_identity();
65 let established_bootstrap_binding = self.established_bootstrap_binding();
66 let bootstrap_binding = diagnostic_bootstrap_binding(
67 &tested_policy_identity,
68 declarations,
69 pool,
70 &self.lifecycle,
71 );
72 let validation = diagnostic_validation(
73 self,
74 declarations,
75 pool,
76 policy,
77 recovery.as_ref().map(|(recovered, _)| recovered),
78 );
79 let commit_recovery = recovery.as_ref().map(|(_, diagnostic)| *diagnostic);
80 let ledger = recovery.and_then(|(recovered, _)| {
81 recovered.ok().map(|recovered| {
82 let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
83 for (record, pages) in export.records.iter_mut().zip(&observed_pages) {
84 record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(*pages));
85 }
86 export
87 })
88 });
89
90 MemoryRuntimeDoctorReport {
91 bootstrapped: self.is_bootstrapped(),
92 tested_policy_identity,
93 established_bootstrap_binding,
94 bootstrap_binding,
95 stable_cell: stable_cell.diagnostic,
96 commit_recovery,
97 ledger,
98 requests: declarations.requests().to_vec(),
99 allocation_pool: pool.clone(),
100 validation,
101 }
102 }
103
104 fn recovered_diagnostic_export(&self, recovered: RecoveredLedger) -> DiagnosticExport {
105 let mut export = DiagnosticExport::from_owned_ledger(recovered.into_ledger());
106 for record in &mut export.records {
107 record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(
108 self.memory_size_pages(record.allocation.slot().id()),
109 ));
110 }
111 export
112 }
113
114 fn established_bootstrap_binding(&self) -> Option<DiagnosticRuntimeBinding> {
115 match &self.lifecycle {
116 RuntimeLifecycle::Unbootstrapped => None,
117 RuntimeLifecycle::Bootstrapped { binding, .. } => Some(DiagnosticRuntimeBinding::new(
118 binding.policy_identity,
119 binding.pool.clone(),
120 )),
121 }
122 }
123
124 fn stable_cell_diagnostic(&self) -> StableCellDiagnostic {
125 let memory = self
126 .memory_manager
127 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
128 let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
129 match decode_stable_cell_commit_store_from_memory(&memory) {
130 Ok(record) => StableCellDiagnostic {
131 diagnostic: DiagnosticStableCell::new(
132 if memory_size.wasm_pages == 0 {
133 DiagnosticStableCellStatus::Empty
134 } else {
135 DiagnosticStableCellStatus::Readable
136 },
137 memory_size,
138 ),
139 record: Some(record),
140 },
141 Err(err) => StableCellDiagnostic {
142 diagnostic: DiagnosticStableCell::new(
143 DiagnosticStableCellStatus::Corrupt {
144 failure: DiagnosticFailure::new(
145 DiagnosticCode::StableCell,
146 err.to_string(),
147 ),
148 },
149 memory_size,
150 ),
151 record: None,
152 },
153 }
154 }
155}
156
157struct StableCellDiagnostic {
158 diagnostic: DiagnosticStableCell,
159 record: Option<LedgerCommitStore>,
160}
161
162fn diagnostic_validation<M: Memory, P: RuntimeBootstrapPolicy>(
163 runtime: &MemoryRuntime<M>,
164 declarations: &SealedDeclarationSnapshot,
165 pool: &crate::MemoryAllocationPool,
166 custom_policy: &P,
167 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
168) -> DiagnosticCheck
169where
170 P::Error: Display,
171{
172 let recovered = match diagnostic_validation_ledger(recovered) {
173 Ok(recovered) => recovered,
174 Err(failure) => return DiagnosticCheck::not_run(failure.code, failure.message),
175 };
176 let occupied = recovered.ledger().occupied_slots();
177 if let Err(error) = runtime.validate_pool_custody(&occupied, pool) {
178 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, error.to_string());
179 }
180 let resolved = match declarations.resolve(&recovered, &occupied, Vec::new(), pool) {
181 Ok(resolved) => resolved,
182 Err(err) => {
183 return DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string());
184 }
185 };
186 match crate::validation::check_allocations(&recovered, &resolved, custom_policy) {
187 Ok(()) => DiagnosticCheck::passed(),
188 Err(err) => DiagnosticCheck::failed(DiagnosticCode::AllocationValidation, err.to_string()),
189 }
190}
191
192fn diagnostic_bootstrap_binding(
193 tested_policy_identity: &PolicyIdentity,
194 declarations: &SealedDeclarationSnapshot,
195 pool: &crate::MemoryAllocationPool,
196 lifecycle: &RuntimeLifecycle,
197) -> DiagnosticCheck {
198 let RuntimeLifecycle::Bootstrapped { binding, .. } = lifecycle else {
199 return DiagnosticCheck::not_run(
200 DiagnosticCode::RuntimeBinding,
201 "runtime has not completed bootstrap",
202 );
203 };
204 match binding.validate::<std::convert::Infallible>(declarations, pool, tested_policy_identity) {
205 Ok(()) => DiagnosticCheck::passed(),
206 Err(_) => DiagnosticCheck::failed(
207 DiagnosticCode::RuntimeBinding,
208 "tested policy/declaration/pool binding differs from established runtime binding",
209 ),
210 }
211}
212
213pub(super) fn diagnostic_validation_ledger(
214 recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
215) -> Result<Cow<'_, crate::RecoveredLedger>, DiagnosticFailure> {
216 if let Some(Ok(recovered)) = recovered {
217 return Ok(Cow::Borrowed(recovered));
218 }
219 if let Some(Err(err)) = recovered {
220 if let Some(genesis) = RecoveredLedger::genesis_for_absent_snapshot(err) {
221 return Ok(Cow::Owned(genesis));
222 }
223 let code = if matches!(
224 err,
225 LedgerCommitError::Recovery(crate::CommitRecoveryError::UnsupportedFormat { .. })
226 ) {
227 DiagnosticCode::UnsupportedFormat
228 } else {
229 DiagnosticCode::LedgerRecovery
230 };
231 return Err(DiagnosticFailure::new(
232 code,
233 format!("protected ledger recovery: {err}"),
234 ));
235 }
236 Err(DiagnosticFailure::new(
237 DiagnosticCode::StableCell,
238 "stable-cell ledger record is not readable",
239 ))
240}