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
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}