Skip to main content

ic_memory/
diagnostics.rs

1use crate::{
2    constants::WASM_PAGE_SIZE_BYTES,
3    ledger::{AllocationLedger, AllocationRecord},
4    physical::CommitSnapshotDiagnostic,
5    policy::PolicyIdentity,
6};
7use serde::Serialize;
8
9///
10/// DiagnosticExport
11///
12/// Read-only machine-readable allocation ledger export.
13///
14/// Reports support serialization only; applications own any diagnostic input
15/// protocol. This type is never used for durable ledger recovery.
16///
17
18#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
19pub struct DiagnosticExport {
20    /// Current committed generation.
21    pub current_generation: u64,
22    /// Allocation records.
23    pub records: Vec<DiagnosticRecord>,
24}
25
26///
27/// DiagnosticRuntimeBinding
28///
29/// Operator-facing view of the policy identity and declaration snapshot bound
30/// to one successful memory-runtime bootstrap.
31///
32
33#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
34pub struct DiagnosticRuntimeBinding {
35    /// Bounded semantic identity of the bootstrap policy.
36    pub policy_identity: PolicyIdentity,
37    /// Exact host namespace grants and physical exclusions bound at bootstrap.
38    pub allocation_pool: crate::MemoryAllocationPool,
39}
40
41impl DiagnosticRuntimeBinding {
42    /// Build one runtime binding diagnostic.
43    #[must_use]
44    pub const fn new(
45        policy_identity: PolicyIdentity,
46        allocation_pool: crate::MemoryAllocationPool,
47    ) -> Self {
48        Self {
49            policy_identity,
50            allocation_pool,
51        }
52    }
53}
54
55///
56/// MemoryRuntimeDoctorReport
57///
58/// Preflight and runtime diagnostic report for one concrete
59/// [`crate::MemoryRuntime`].
60///
61/// This report is intended for operator-facing diagnostics. Recoverable
62/// runtime problems, such as corrupt stable-cell bytes or commit recovery
63/// failure, are represented as fields instead of aborting report construction.
64/// Reports support serialization only; applications own diagnostic input protocols.
65///
66
67#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
68pub struct MemoryRuntimeDoctorReport {
69    /// Whether this runtime has completed bootstrap validation.
70    pub bootstrapped: bool,
71    /// Policy identity supplied for this diagnostic evaluation.
72    pub tested_policy_identity: PolicyIdentity,
73    /// Binding published by this runtime's successful bootstrap, when present.
74    pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
75    /// Whether the tested identity and declarations match the established
76    /// bootstrap binding.
77    pub bootstrap_binding: DiagnosticCheck,
78    /// Stable-cell ledger storage status.
79    pub stable_cell: DiagnosticStableCell,
80    /// Protected commit recovery status when a ledger record was readable.
81    pub commit_recovery: Option<CommitSnapshotDiagnostic>,
82    /// Recovered allocation ledger export when protected recovery succeeded.
83    pub ledger: Option<DiagnosticExport>,
84    /// Key-only requirements contributed by linked components.
85    pub requests: Vec<crate::MemoryRequest>,
86    /// Tested host namespace grants and physical exclusions.
87    pub allocation_pool: crate::MemoryAllocationPool,
88    /// Declaration validation result under the tested caller-supplied policy.
89    pub validation: DiagnosticCheck,
90}
91
92///
93/// DiagnosticCode
94///
95/// Stable machine-readable category for an operator diagnostic failure.
96///
97
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99pub enum DiagnosticCode {
100    /// Stable-cell storage could not be decoded.
101    #[serde(rename = "stable_cell")]
102    StableCell,
103    /// Persisted bytes use a recognized but unsupported durable format.
104    #[serde(rename = "unsupported_format")]
105    UnsupportedFormat,
106    /// Protected ledger recovery failed.
107    #[serde(rename = "ledger_recovery")]
108    LedgerRecovery,
109    /// Current declarations failed allocation validation.
110    #[serde(rename = "allocation_validation")]
111    AllocationValidation,
112    /// Tested bootstrap identity or declarations differ from runtime state.
113    #[serde(rename = "runtime_binding")]
114    RuntimeBinding,
115}
116
117///
118/// DiagnosticFailure
119///
120/// Machine-readable diagnostic code paired with an operator-facing message.
121///
122
123#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
124pub struct DiagnosticFailure {
125    /// Stable diagnostic category.
126    pub code: DiagnosticCode,
127    /// Human-readable failure detail.
128    pub message: String,
129}
130
131impl DiagnosticFailure {
132    /// Build a coded diagnostic failure.
133    #[must_use]
134    pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
135        Self {
136            code,
137            message: message.into(),
138        }
139    }
140}
141
142///
143/// DiagnosticStableCell
144///
145/// Read-only diagnostic view of the stable-cell ledger storage envelope.
146///
147
148#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
149pub struct DiagnosticStableCell {
150    /// Stable-cell status.
151    pub status: DiagnosticStableCellStatus,
152    /// Backing memory size for the ledger cell.
153    pub memory_size: DiagnosticMemorySize,
154}
155
156impl DiagnosticStableCell {
157    /// Build a stable-cell diagnostic.
158    #[must_use]
159    pub const fn new(
160        status: DiagnosticStableCellStatus,
161        memory_size: DiagnosticMemorySize,
162    ) -> Self {
163        Self {
164            status,
165            memory_size,
166        }
167    }
168}
169
170///
171/// DiagnosticStableCellStatus
172///
173/// Stable-cell ledger storage status.
174///
175
176#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
177pub enum DiagnosticStableCellStatus {
178    /// The ledger memory is empty and can be initialized.
179    Empty,
180    /// The stable-cell envelope and ledger record decoded successfully.
181    Readable,
182    /// The ledger memory is present but could not be decoded as the expected
183    /// stable-cell ledger record.
184    Corrupt {
185        /// Stable-cell envelope or ledger-record decode failure.
186        failure: DiagnosticFailure,
187    },
188}
189
190///
191/// DiagnosticCheck
192///
193/// Read-only diagnostic status for a preflight check.
194///
195
196#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
197pub enum DiagnosticCheck {
198    /// The check could not run because prerequisite state was unavailable.
199    NotRun {
200        /// Stable diagnostic category.
201        code: DiagnosticCode,
202        /// Reason the check could not run.
203        message: String,
204    },
205    /// The check completed successfully.
206    Passed,
207    /// The check ran and found a problem.
208    Failed {
209        /// Stable diagnostic category.
210        code: DiagnosticCode,
211        /// Validation failure.
212        message: String,
213    },
214}
215
216impl DiagnosticCheck {
217    /// Build a passed diagnostic check.
218    #[must_use]
219    pub const fn passed() -> Self {
220        Self::Passed
221    }
222
223    /// Build a failed diagnostic check.
224    #[must_use]
225    pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
226        Self::Failed {
227            code,
228            message: message.into(),
229        }
230    }
231
232    /// Build a skipped diagnostic check.
233    #[must_use]
234    pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
235        Self::NotRun {
236            code,
237            message: message.into(),
238        }
239    }
240}
241
242impl DiagnosticExport {
243    /// Build a read-only diagnostic export from an allocation ledger.
244    ///
245    /// Records start unmeasured. The exporter can fill their size fields from
246    /// its backing-memory observations; this DTO constructor neither recovers
247    /// nor measures memory. Physical recovery status belongs to the doctor report.
248    #[must_use]
249    #[cfg(any(test, feature = "test-support"))]
250    pub fn from_ledger(ledger: &AllocationLedger) -> Self {
251        Self::from_records(ledger.current_generation, ledger.records.iter().cloned())
252    }
253
254    // Normal exports consume their decoded records. Borrowed exports copy
255    // records directly into the same projection without an intermediate ledger.
256    pub(crate) fn from_owned_ledger(ledger: AllocationLedger) -> Self {
257        Self::from_records(ledger.current_generation, ledger.records.into_iter())
258    }
259
260    fn from_records(
261        current_generation: u64,
262        records: impl Iterator<Item = AllocationRecord>,
263    ) -> Self {
264        Self {
265            current_generation,
266            records: records
267                .map(|allocation| DiagnosticRecord {
268                    allocation,
269                    memory_size: None,
270                })
271                .collect(),
272        }
273    }
274}
275
276///
277/// DiagnosticRecord
278///
279/// Read-only diagnostic allocation record.
280#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
281pub struct DiagnosticRecord {
282    /// Allocation record.
283    pub allocation: AllocationRecord,
284    /// Live backing memory size, when the exporter measured one.
285    ///
286    /// This is allocation size reported by the backing memory, not logical user
287    /// payload size inside the stable structure.
288    #[serde(skip_serializing_if = "Option::is_none")]
289    pub memory_size: Option<DiagnosticMemorySize>,
290}
291
292///
293/// DiagnosticMemorySize
294///
295/// Live size reported by a backing stable memory.
296///
297
298#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
299pub struct DiagnosticMemorySize {
300    /// WebAssembly pages reported by the memory.
301    pub wasm_pages: u64,
302}
303
304impl DiagnosticMemorySize {
305    /// Build a size from a WebAssembly page count.
306    #[must_use]
307    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
308        Self { wasm_pages }
309    }
310
311    /// Bytes represented by the page count, saturating on an unrepresentable size.
312    #[must_use]
313    pub const fn bytes(self) -> u64 {
314        self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
322
323    #[test]
324    fn diagnostic_export_copies_ledger_records() {
325        let declaration = AllocationDeclaration::new(
326            "app.users.v1",
327            MemoryManagerSlot::new(100).expect("usable slot"),
328        )
329        .expect("declaration");
330        let ledger = AllocationLedger {
331            current_generation: 3,
332            records: vec![AllocationRecord::active(&declaration)],
333        };
334
335        let export = DiagnosticExport::from_ledger(&ledger);
336
337        assert_eq!(export.current_generation, 3);
338        assert_eq!(export.records.len(), 1);
339        assert_eq!(export.records[0].memory_size, None);
340
341        assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
342        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
343
344        assert_eq!(
345            wire,
346            serde_json::json!({
347                "current_generation": 3,
348                "records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100, "state": "Active"}}]
349            })
350        );
351    }
352
353    #[test]
354    fn diagnostic_outcome_states_serialize() {
355        let stable_cell = DiagnosticStableCell::new(
356            DiagnosticStableCellStatus::Corrupt {
357                failure: DiagnosticFailure::new(
358                    DiagnosticCode::StableCell,
359                    "bad stable-cell record",
360                ),
361            },
362            DiagnosticMemorySize::from_wasm_pages(1),
363        );
364        let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
365        let check = DiagnosticCheck::failed(
366            DiagnosticCode::AllocationValidation,
367            "duplicate declaration",
368        );
369
370        assert_eq!(
371            serde_json::to_value(DiagnosticCheck::passed()).unwrap(),
372            "Passed"
373        );
374        assert_eq!(
375            serde_json::to_value(check).unwrap(),
376            serde_json::json!({
377                "Failed": {"code": "allocation_validation", "message": "duplicate declaration"}
378            })
379        );
380        assert_eq!(
381            serde_json::to_value(stable_cell).unwrap(),
382            serde_json::json!({
383                "status": {"Corrupt": {"failure": {"code": "stable_cell", "message": "bad stable-cell record"}}},
384                "memory_size": {"wasm_pages": 1}
385            })
386        );
387        assert_eq!(
388            serde_json::to_value(allocation_pool).unwrap(),
389            serde_json::json!({
390                "authorities": [], "excluded_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
391            })
392        );
393    }
394
395    #[test]
396    fn diagnostic_codes_have_stable_wire_names() {
397        let cases = [
398            (DiagnosticCode::StableCell, "stable_cell"),
399            (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
400            (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
401            (
402                DiagnosticCode::AllocationValidation,
403                "allocation_validation",
404            ),
405            (DiagnosticCode::RuntimeBinding, "runtime_binding"),
406        ];
407
408        for (code, expected) in cases {
409            assert_eq!(
410                crate::test_cbor::to_value(code).expect("diagnostic code value"),
411                crate::test_cbor::Value::Text(expected.to_string())
412            );
413        }
414    }
415
416    #[test]
417    fn diagnostic_export_can_include_memory_sizes() {
418        let declaration = AllocationDeclaration::new(
419            "app.users.v1",
420            MemoryManagerSlot::new(100).expect("usable slot"),
421        )
422        .expect("declaration");
423        let ledger = AllocationLedger {
424            current_generation: 3,
425            records: vec![AllocationRecord::active(&declaration)],
426        };
427
428        let mut export = DiagnosticExport::from_ledger(&ledger);
429        export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
430
431        assert_eq!(
432            export.records[0].memory_size,
433            Some(DiagnosticMemorySize { wasm_pages: 2 })
434        );
435        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
436        assert_eq!(
437            wire["records"][0]["memory_size"],
438            serde_json::json!({"wasm_pages": 2})
439        );
440    }
441}