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    fn from_records(
44        current_generation: u64,
45        records: impl Iterator<Item = AllocationRecord>,
46    ) -> Self {
47        Self {
48            current_generation,
49            records: records
50                .map(|allocation| DiagnosticRecord {
51                    allocation,
52                    memory_size: None,
53                })
54                .collect(),
55        }
56    }
57}
58
59///
60/// DiagnosticRecord
61///
62/// Read-only diagnostic allocation record.
63#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
64pub struct DiagnosticRecord {
65    /// Allocation record.
66    pub allocation: AllocationRecord,
67    /// Live backing memory size, when the exporter measured one.
68    ///
69    /// This is allocation size reported by the backing memory, not logical user
70    /// payload size inside the stable structure.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub memory_size: Option<DiagnosticMemorySize>,
73}
74
75///
76/// DiagnosticMemorySize
77///
78/// Live size reported by a backing stable memory.
79///
80
81#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
82pub struct DiagnosticMemorySize {
83    /// WebAssembly pages reported by the memory.
84    pub wasm_pages: u64,
85}
86
87impl DiagnosticMemorySize {
88    /// Build a size from a WebAssembly page count.
89    #[must_use]
90    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
91        Self { wasm_pages }
92    }
93
94    /// Bytes represented by the page count, saturating on an unrepresentable size.
95    #[must_use]
96    pub const fn bytes(self) -> u64 {
97        self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
105
106    #[test]
107    fn diagnostic_export_copies_ledger_records() {
108        let declaration = AllocationDeclaration::new(
109            "app.users.v1",
110            MemoryManagerSlot::new(100).expect("usable slot"),
111        )
112        .expect("declaration");
113        let ledger = AllocationLedger {
114            current_generation: 3,
115            records: vec![AllocationRecord::from_declaration(&declaration)],
116        };
117
118        let export = DiagnosticExport::from_ledger(&ledger);
119
120        assert_eq!(export.current_generation, 3);
121        assert_eq!(export.records.len(), 1);
122        assert_eq!(export.records[0].memory_size, None);
123
124        assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
125        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
126
127        assert_eq!(
128            wire,
129            serde_json::json!({
130                "current_generation": 3,
131                "records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100}}]
132            })
133        );
134    }
135
136    #[test]
137    fn diagnostic_export_can_include_memory_sizes() {
138        let declaration = AllocationDeclaration::new(
139            "app.users.v1",
140            MemoryManagerSlot::new(100).expect("usable slot"),
141        )
142        .expect("declaration");
143        let ledger = AllocationLedger {
144            current_generation: 3,
145            records: vec![AllocationRecord::from_declaration(&declaration)],
146        };
147
148        let mut export = DiagnosticExport::from_ledger(&ledger);
149        export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
150
151        assert_eq!(
152            export.records[0].memory_size,
153            Some(DiagnosticMemorySize { wasm_pages: 2 })
154        );
155        let wire = serde_json::to_value(&export).expect("diagnostic JSON");
156        assert_eq!(
157            wire["records"][0]["memory_size"],
158            serde_json::json!({"wasm_pages": 2})
159        );
160    }
161}