Skip to main content

ic_memory/
diagnostics.rs

1use crate::{
2    constants::WASM_PAGE_SIZE_BYTES,
3    ledger::{AllocationLedger, AllocationRecord},
4};
5use serde::Serialize;
6
7///
8/// DiagnosticExport
9///
10/// Read-only machine-readable allocation ledger export.
11///
12/// Reports support serialization only; applications own any diagnostic input
13/// protocol. This type is never used for durable ledger recovery.
14///
15
16#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
17pub struct DiagnosticExport {
18    /// Current committed generation.
19    pub current_generation: u64,
20    /// Allocation records.
21    pub records: Vec<DiagnosticRecord>,
22}
23
24impl DiagnosticExport {
25    /// Build a read-only diagnostic export from an allocation ledger.
26    ///
27    /// Records start unmeasured. The exporter can fill their size fields from
28    /// its backing-memory observations; this DTO constructor neither recovers
29    /// nor measures memory. Physical recovery status is available separately
30    /// from the runtime.
31    #[must_use]
32    #[cfg(any(test, feature = "test-support"))]
33    pub fn from_ledger(ledger: &AllocationLedger) -> Self {
34        Self::from_records(ledger.current_generation, ledger.records.iter().cloned())
35    }
36
37    // Normal exports consume their decoded records. Borrowed exports copy
38    // records directly into the same projection without an intermediate ledger.
39    pub(crate) fn from_owned_ledger(ledger: AllocationLedger) -> Self {
40        Self::from_records(ledger.current_generation, ledger.records.into_iter())
41    }
42
43    // Callers own when sizes are observed: live exports measure here, while
44    // fixture reports supply readings captured before policy callbacks.
45    pub(crate) fn with_memory_sizes(
46        mut self,
47        mut pages: impl FnMut(&AllocationRecord) -> u64,
48    ) -> Self {
49        for record in &mut self.records {
50            record.memory_size = Some(DiagnosticMemorySize::from_wasm_pages(pages(
51                &record.allocation,
52            )));
53        }
54        self
55    }
56
57    fn from_records(
58        current_generation: u64,
59        records: impl Iterator<Item = AllocationRecord>,
60    ) -> Self {
61        Self {
62            current_generation,
63            records: records
64                .map(|allocation| DiagnosticRecord {
65                    allocation,
66                    memory_size: None,
67                })
68                .collect(),
69        }
70    }
71}
72
73///
74/// DiagnosticRecord
75///
76/// Read-only diagnostic allocation record.
77#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
78pub struct DiagnosticRecord {
79    /// Allocation record.
80    pub allocation: AllocationRecord,
81    /// Live backing memory size, when the exporter measured one.
82    ///
83    /// This is allocation size reported by the backing memory, not logical user
84    /// payload size inside the stable structure.
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub memory_size: Option<DiagnosticMemorySize>,
87}
88
89///
90/// DiagnosticMemorySize
91///
92/// Live size reported by a backing stable memory.
93///
94
95#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
96pub struct DiagnosticMemorySize {
97    /// WebAssembly pages reported by the memory.
98    pub wasm_pages: u64,
99}
100
101impl DiagnosticMemorySize {
102    /// Build a size from a WebAssembly page count.
103    #[must_use]
104    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
105        Self { wasm_pages }
106    }
107
108    /// Bytes represented by the page count, saturating on an unrepresentable size.
109    #[must_use]
110    pub const fn bytes(self) -> u64 {
111        self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
119
120    #[test]
121    fn diagnostic_export_copies_ledger_records() {
122        let declaration = AllocationDeclaration::new(
123            "app.users.v1",
124            MemoryManagerSlot::new(100).expect("usable slot"),
125        )
126        .expect("declaration");
127        let ledger = AllocationLedger {
128            current_generation: 3,
129            records: vec![AllocationRecord::from_declaration(&declaration)],
130        };
131
132        let export = DiagnosticExport::from_ledger(&ledger);
133
134        assert_eq!(export.current_generation, 3);
135        assert_eq!(export.records.len(), 1);
136        assert_eq!(export.records[0].memory_size, None);
137
138        assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
139        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
140
141        assert_eq!(
142            wire,
143            serde_json::json!({
144                "current_generation": 3,
145                "records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100}}]
146            })
147        );
148    }
149
150    #[test]
151    fn diagnostic_export_can_include_memory_sizes() {
152        let declaration = AllocationDeclaration::new(
153            "app.users.v1",
154            MemoryManagerSlot::new(100).expect("usable slot"),
155        )
156        .expect("declaration");
157        let ledger = AllocationLedger {
158            current_generation: 3,
159            records: vec![AllocationRecord::from_declaration(&declaration)],
160        };
161
162        let mut export = DiagnosticExport::from_ledger(&ledger);
163        export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
164
165        assert_eq!(
166            export.records[0].memory_size,
167            Some(DiagnosticMemorySize { wasm_pages: 2 })
168        );
169        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
170        assert_eq!(
171            wire["records"][0]["memory_size"],
172            serde_json::json!({"wasm_pages": 2})
173        );
174    }
175}