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