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: Result<PolicyIdentity, DiagnosticFailure>,
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    /// Runtime bootstrap policy identity was invalid.
113    #[serde(rename = "policy_identity")]
114    PolicyIdentity,
115    /// Tested bootstrap identity or declarations differ from runtime state.
116    #[serde(rename = "runtime_binding")]
117    RuntimeBinding,
118}
119
120///
121/// DiagnosticFailure
122///
123/// Machine-readable diagnostic code paired with an operator-facing message.
124///
125
126#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
127pub struct DiagnosticFailure {
128    /// Stable diagnostic category.
129    pub code: DiagnosticCode,
130    /// Human-readable failure detail.
131    pub message: String,
132}
133
134impl DiagnosticFailure {
135    /// Build a coded diagnostic failure.
136    #[must_use]
137    pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
138        Self {
139            code,
140            message: message.into(),
141        }
142    }
143}
144
145///
146/// DiagnosticStableCell
147///
148/// Read-only diagnostic view of the stable-cell ledger storage envelope.
149///
150
151#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
152pub struct DiagnosticStableCell {
153    /// Stable-cell status.
154    pub status: DiagnosticStableCellStatus,
155    /// Backing memory size for the ledger cell.
156    pub memory_size: DiagnosticMemorySize,
157}
158
159impl DiagnosticStableCell {
160    /// Build a stable-cell diagnostic.
161    #[must_use]
162    pub const fn new(
163        status: DiagnosticStableCellStatus,
164        memory_size: DiagnosticMemorySize,
165    ) -> Self {
166        Self {
167            status,
168            memory_size,
169        }
170    }
171}
172
173///
174/// DiagnosticStableCellStatus
175///
176/// Stable-cell ledger storage status.
177///
178
179#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
180pub enum DiagnosticStableCellStatus {
181    /// The ledger memory is empty and can be initialized.
182    Empty,
183    /// The stable-cell envelope and ledger record decoded successfully.
184    Readable,
185    /// The ledger memory is present but could not be decoded as the expected
186    /// stable-cell ledger record.
187    Corrupt {
188        /// Stable-cell envelope or ledger-record decode failure.
189        failure: DiagnosticFailure,
190    },
191}
192
193///
194/// DiagnosticCheck
195///
196/// Read-only diagnostic status for a preflight check.
197///
198
199#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
200pub enum DiagnosticCheck {
201    /// The check could not run because prerequisite state was unavailable.
202    NotRun {
203        /// Stable diagnostic category.
204        code: DiagnosticCode,
205        /// Reason the check could not run.
206        message: String,
207    },
208    /// The check completed successfully.
209    Passed,
210    /// The check ran and found a problem.
211    Failed {
212        /// Stable diagnostic category.
213        code: DiagnosticCode,
214        /// Validation failure.
215        message: String,
216    },
217}
218
219impl DiagnosticCheck {
220    /// Build a passed diagnostic check.
221    #[must_use]
222    pub const fn passed() -> Self {
223        Self::Passed
224    }
225
226    /// Build a failed diagnostic check.
227    #[must_use]
228    pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
229        Self::Failed {
230            code,
231            message: message.into(),
232        }
233    }
234
235    /// Build a skipped diagnostic check.
236    #[must_use]
237    pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
238        Self::NotRun {
239            code,
240            message: message.into(),
241        }
242    }
243}
244
245impl DiagnosticExport {
246    /// Build a read-only diagnostic export from an allocation ledger.
247    ///
248    /// Records start unmeasured. The exporter can fill their size fields from
249    /// its backing-memory observations; this DTO constructor neither recovers
250    /// nor measures memory. Physical recovery status belongs to the doctor report.
251    #[must_use]
252    pub fn from_ledger(ledger: &AllocationLedger) -> Self {
253        Self::from_records(ledger.current_generation, ledger.records.iter().cloned())
254    }
255
256    // Normal exports consume their decoded records. Borrowed exports copy
257    // records directly into the same projection without an intermediate ledger.
258    pub(crate) fn from_owned_ledger(ledger: AllocationLedger) -> Self {
259        Self::from_records(ledger.current_generation, ledger.records.into_iter())
260    }
261
262    fn from_records(
263        current_generation: u64,
264        records: impl Iterator<Item = AllocationRecord>,
265    ) -> Self {
266        Self {
267            current_generation,
268            records: records
269                .map(|allocation| DiagnosticRecord {
270                    allocation,
271                    memory_size: None,
272                })
273                .collect(),
274        }
275    }
276}
277
278///
279/// DiagnosticRecord
280///
281/// Read-only diagnostic allocation record.
282#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
283pub struct DiagnosticRecord {
284    /// Allocation record.
285    pub allocation: AllocationRecord,
286    /// Live backing memory size, when the exporter measured one.
287    ///
288    /// This is allocation size reported by the backing memory, not logical user
289    /// payload size inside the stable structure.
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub memory_size: Option<DiagnosticMemorySize>,
292}
293
294///
295/// DiagnosticMemorySize
296///
297/// Live size reported by a backing stable memory.
298///
299
300#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
301pub struct DiagnosticMemorySize {
302    /// WebAssembly pages reported by the memory.
303    pub wasm_pages: u64,
304}
305
306impl DiagnosticMemorySize {
307    /// Build a size from a WebAssembly page count.
308    #[must_use]
309    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
310        Self { wasm_pages }
311    }
312
313    /// Bytes represented by the page count, saturating on an unrepresentable size.
314    #[must_use]
315    pub const fn bytes(self) -> u64 {
316        self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
324
325    #[test]
326    fn diagnostic_export_copies_ledger_records() {
327        let declaration = AllocationDeclaration::new(
328            "app.users.v1",
329            MemoryManagerSlot::new(100).expect("usable slot"),
330        )
331        .expect("declaration");
332        let ledger = AllocationLedger {
333            current_generation: 3,
334            records: vec![AllocationRecord::active(&declaration)],
335        };
336
337        let export = DiagnosticExport::from_ledger(&ledger);
338
339        assert_eq!(export.current_generation, 3);
340        assert_eq!(export.records.len(), 1);
341        assert_eq!(export.records[0].memory_size, None);
342
343        assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
344        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
345
346        assert_eq!(
347            wire,
348            serde_json::json!({
349                "current_generation": 3,
350                "records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100, "state": "Active"}}]
351            })
352        );
353    }
354
355    #[test]
356    fn diagnostic_outcome_states_serialize() {
357        let stable_cell = DiagnosticStableCell::new(
358            DiagnosticStableCellStatus::Corrupt {
359                failure: DiagnosticFailure::new(
360                    DiagnosticCode::StableCell,
361                    "bad stable-cell record",
362                ),
363            },
364            DiagnosticMemorySize::from_wasm_pages(1),
365        );
366        let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
367        let check = DiagnosticCheck::failed(
368            DiagnosticCode::AllocationValidation,
369            "duplicate declaration",
370        );
371
372        assert_eq!(
373            serde_json::to_value(DiagnosticCheck::passed()).unwrap(),
374            "Passed"
375        );
376        assert_eq!(
377            serde_json::to_value(check).unwrap(),
378            serde_json::json!({
379                "Failed": {"code": "allocation_validation", "message": "duplicate declaration"}
380            })
381        );
382        assert_eq!(
383            serde_json::to_value(stable_cell).unwrap(),
384            serde_json::json!({
385                "status": {"Corrupt": {"failure": {"code": "stable_cell", "message": "bad stable-cell record"}}},
386                "memory_size": {"wasm_pages": 1}
387            })
388        );
389        assert_eq!(
390            serde_json::to_value(allocation_pool).unwrap(),
391            serde_json::json!({
392                "authorities": [], "excluded_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
393            })
394        );
395    }
396
397    #[test]
398    fn diagnostic_codes_have_stable_wire_names() {
399        let cases = [
400            (DiagnosticCode::StableCell, "stable_cell"),
401            (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
402            (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
403            (
404                DiagnosticCode::AllocationValidation,
405                "allocation_validation",
406            ),
407            (DiagnosticCode::PolicyIdentity, "policy_identity"),
408            (DiagnosticCode::RuntimeBinding, "runtime_binding"),
409        ];
410
411        for (code, expected) in cases {
412            assert_eq!(
413                crate::test_cbor::to_value(code).expect("diagnostic code value"),
414                crate::test_cbor::Value::Text(expected.to_string())
415            );
416        }
417    }
418
419    #[test]
420    fn diagnostic_export_can_include_memory_sizes() {
421        let declaration = AllocationDeclaration::new(
422            "app.users.v1",
423            MemoryManagerSlot::new(100).expect("usable slot"),
424        )
425        .expect("declaration");
426        let ledger = AllocationLedger {
427            current_generation: 3,
428            records: vec![AllocationRecord::active(&declaration)],
429        };
430
431        let mut export = DiagnosticExport::from_ledger(&ledger);
432        export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
433
434        assert_eq!(
435            export.records[0].memory_size,
436            Some(DiagnosticMemorySize { wasm_pages: 2 })
437        );
438        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
439        assert_eq!(
440            wire["records"][0]["memory_size"],
441            serde_json::json!({"wasm_pages": 2})
442        );
443    }
444}