Skip to main content

ic_memory/
diagnostics.rs

1use crate::{
2    constants::WASM_PAGE_SIZE_BYTES,
3    ledger::{AllocationLedger, AllocationRecord, GenerationRecord},
4    physical::CommitStoreDiagnostic,
5    slot::AllocationSlotDescriptor,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10///
11/// DiagnosticExport
12///
13/// Read-only machine-readable allocation ledger export.
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
15pub struct DiagnosticExport {
16    /// Current committed generation.
17    pub current_generation: u64,
18    /// Ledger anchor descriptor.
19    pub ledger_anchor: AllocationSlotDescriptor,
20    /// Allocation records.
21    pub records: Vec<DiagnosticRecord>,
22    /// Generation records.
23    pub generations: Vec<DiagnosticGeneration>,
24    /// Optional protected commit recovery diagnostic.
25    pub commit_recovery: Option<CommitStoreDiagnostic>,
26}
27
28impl DiagnosticExport {
29    /// Build a read-only diagnostic export from an allocation ledger.
30    #[must_use]
31    pub fn from_ledger(ledger: &AllocationLedger, ledger_anchor: AllocationSlotDescriptor) -> Self {
32        Self::from_ledger_with_commit_recovery(ledger, ledger_anchor, None)
33    }
34
35    /// Build a read-only diagnostic export with protected commit recovery state.
36    #[must_use]
37    pub fn from_ledger_with_commit_recovery(
38        ledger: &AllocationLedger,
39        ledger_anchor: AllocationSlotDescriptor,
40        commit_recovery: Option<CommitStoreDiagnostic>,
41    ) -> Self {
42        Self::from_ledger_with_commit_recovery_and_memory_sizes(
43            ledger,
44            ledger_anchor,
45            commit_recovery,
46            std::iter::empty(),
47        )
48    }
49
50    /// Build a read-only diagnostic export with live memory sizes.
51    #[must_use]
52    pub fn from_ledger_with_memory_sizes(
53        ledger: &AllocationLedger,
54        ledger_anchor: AllocationSlotDescriptor,
55        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
56    ) -> Self {
57        Self::from_ledger_with_commit_recovery_and_memory_sizes(
58            ledger,
59            ledger_anchor,
60            None,
61            memory_sizes,
62        )
63    }
64
65    /// Build a read-only diagnostic export with protected recovery state and live memory sizes.
66    #[must_use]
67    pub fn from_ledger_with_commit_recovery_and_memory_sizes(
68        ledger: &AllocationLedger,
69        ledger_anchor: AllocationSlotDescriptor,
70        commit_recovery: Option<CommitStoreDiagnostic>,
71        memory_sizes: impl IntoIterator<Item = (AllocationSlotDescriptor, DiagnosticMemorySize)>,
72    ) -> Self {
73        let memory_sizes: BTreeMap<_, _> = memory_sizes.into_iter().collect();
74        Self {
75            current_generation: ledger.current_generation,
76            ledger_anchor,
77            records: ledger
78                .allocation_history()
79                .records()
80                .iter()
81                .cloned()
82                .map(|allocation| {
83                    let memory_size = memory_sizes.get(allocation.slot()).copied();
84                    DiagnosticRecord {
85                        allocation,
86                        memory_size,
87                    }
88                })
89                .collect(),
90            generations: ledger
91                .allocation_history()
92                .generations()
93                .iter()
94                .cloned()
95                .map(|generation| DiagnosticGeneration { generation })
96                .collect(),
97            commit_recovery,
98        }
99    }
100}
101
102///
103/// DiagnosticRecord
104///
105/// Read-only diagnostic allocation record.
106#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
107pub struct DiagnosticRecord {
108    /// Allocation record.
109    pub allocation: AllocationRecord,
110    /// Live backing memory size, when the exporter measured one.
111    ///
112    /// This is allocation size reported by the backing memory, not logical user
113    /// payload size inside the stable structure.
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub memory_size: Option<DiagnosticMemorySize>,
116}
117
118///
119/// DiagnosticMemorySize
120///
121/// Live size reported by a backing stable memory.
122///
123
124#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
125#[serde(deny_unknown_fields)]
126pub struct DiagnosticMemorySize {
127    /// WebAssembly pages reported by the memory.
128    pub wasm_pages: u64,
129    /// Bytes represented by the page count.
130    pub bytes: u64,
131}
132
133impl DiagnosticMemorySize {
134    /// Build a size from a WebAssembly page count.
135    #[must_use]
136    pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
137        Self {
138            wasm_pages,
139            bytes: wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES),
140        }
141    }
142}
143
144///
145/// DiagnosticGeneration
146///
147/// Read-only diagnostic generation record.
148#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
149pub struct DiagnosticGeneration {
150    /// Generation record.
151    pub generation: GenerationRecord,
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::{
158        declaration::AllocationDeclaration,
159        ledger::{AllocationHistory, AllocationRecord, AllocationState},
160        physical::{CommitRecoveryError, CommitSlotDiagnostic, CommitStoreDiagnostic},
161        schema::SchemaMetadata,
162    };
163
164    #[test]
165    fn diagnostic_export_copies_ledger_records() {
166        let declaration = AllocationDeclaration::new(
167            "app.users.v1",
168            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
169            None,
170            SchemaMetadata::default(),
171        )
172        .expect("declaration");
173        let ledger = AllocationLedger {
174            current_generation: 3,
175            allocation_history: AllocationHistory::from_parts(
176                vec![AllocationRecord::from_declaration(
177                    3,
178                    declaration,
179                    AllocationState::Active,
180                )],
181                vec![GenerationRecord {
182                    generation: 3,
183                    parent_generation: 2,
184                    runtime_fingerprint: Some("wasm:abc123".to_string()),
185                    declaration_count: 1,
186                    committed_at: None,
187                }],
188            ),
189        };
190
191        let export = DiagnosticExport::from_ledger(
192            &ledger,
193            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
194        );
195
196        assert_eq!(export.current_generation, 3);
197        assert_eq!(export.records.len(), 1);
198        assert_eq!(export.records[0].memory_size, None);
199        assert_eq!(export.generations.len(), 1);
200        assert_eq!(
201            export.ledger_anchor,
202            AllocationSlotDescriptor::memory_manager(0).expect("usable slot")
203        );
204        assert_eq!(export.commit_recovery, None);
205    }
206
207    #[test]
208    fn diagnostic_export_can_include_commit_recovery_state() {
209        let ledger = AllocationLedger {
210            current_generation: 3,
211            allocation_history: AllocationHistory::default(),
212        };
213        let commit_recovery = CommitStoreDiagnostic {
214            slot0: CommitSlotDiagnostic {
215                present: true,
216                generation: Some(3),
217                valid: true,
218            },
219            slot1: CommitSlotDiagnostic {
220                present: false,
221                generation: None,
222                valid: false,
223            },
224            authoritative_generation: Some(3),
225            recovery_error: None,
226        };
227
228        let export = DiagnosticExport::from_ledger_with_commit_recovery(
229            &ledger,
230            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
231            Some(commit_recovery),
232        );
233
234        assert_eq!(export.commit_recovery, Some(commit_recovery));
235    }
236
237    #[test]
238    fn diagnostic_export_can_include_memory_sizes() {
239        let declaration = AllocationDeclaration::new(
240            "app.users.v1",
241            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
242            None,
243            SchemaMetadata::default(),
244        )
245        .expect("declaration");
246        let ledger = AllocationLedger {
247            current_generation: 3,
248            allocation_history: AllocationHistory::from_parts(
249                vec![AllocationRecord::from_declaration(
250                    3,
251                    declaration,
252                    AllocationState::Active,
253                )],
254                Vec::new(),
255            ),
256        };
257
258        let export = DiagnosticExport::from_ledger_with_memory_sizes(
259            &ledger,
260            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
261            [(
262                AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
263                DiagnosticMemorySize::from_wasm_pages(2),
264            )],
265        );
266
267        assert_eq!(
268            export.records[0].memory_size,
269            Some(DiagnosticMemorySize {
270                wasm_pages: 2,
271                bytes: 131_072,
272            })
273        );
274    }
275
276    #[test]
277    fn diagnostic_export_can_report_recovery_failure() {
278        let ledger = AllocationLedger {
279            current_generation: 0,
280            allocation_history: AllocationHistory::default(),
281        };
282        let commit_recovery = CommitStoreDiagnostic {
283            slot0: CommitSlotDiagnostic {
284                present: false,
285                generation: None,
286                valid: false,
287            },
288            slot1: CommitSlotDiagnostic {
289                present: false,
290                generation: None,
291                valid: false,
292            },
293            authoritative_generation: None,
294            recovery_error: Some(CommitRecoveryError::NoValidGeneration),
295        };
296
297        let export = DiagnosticExport::from_ledger_with_commit_recovery(
298            &ledger,
299            AllocationSlotDescriptor::memory_manager(0).expect("usable slot"),
300            Some(commit_recovery),
301        );
302
303        assert_eq!(
304            export
305                .commit_recovery
306                .expect("commit recovery")
307                .recovery_error,
308            Some(CommitRecoveryError::NoValidGeneration)
309        );
310    }
311}