Skip to main content

icydb_core/db/diagnostics/
memory_allocations.rs

1//! Module: diagnostics::memory_allocations
2//! Responsibility: project bounded runtime allocation facts into Wire DTOs.
3//! Does not own: manager validation, allocation policy, or payload accounting.
4//! Boundary: ic-memory's owned report -> canister-wide storage diagnostics.
5
6#[cfg(test)]
7mod tests;
8
9use crate::{db::memory_runtime, error::InternalError};
10
11use ic_memory::MemoryAllocationSummary;
12use serde::Deserialize;
13
14/// Physical allocation accounting for the bound host memory runtime.
15///
16/// The ledger is already included in totals. Virtual extent is addressable
17/// memory, not live payload. Unknown bindings still own physical allocations.
18#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
19pub struct MemoryAllocations {
20    /// Committed runtime generation, absent before allocation bootstrap.
21    pub current_generation: Option<u64>,
22    /// Validated manager layout version.
23    pub manager_layout_version: u8,
24    /// Actual persisted bucket size in Wasm pages.
25    pub bucket_size_pages: u16,
26    /// Actual persisted bucket size in bytes.
27    pub bucket_size_bytes: u64,
28    /// Total bucket-table capacity shared by all slots.
29    pub bucket_capacity: u32,
30    /// Number of assigned buckets.
31    pub allocated_buckets: u16,
32    /// Remaining bucket-table entries.
33    pub remaining_buckets: u32,
34    /// Table capacity in bytes, excluding metadata and backing limits.
35    pub maximum_bucket_bytes: u64,
36    /// Entire backing extent, including manager metadata and residuals.
37    pub physical_extent: MemoryExtent,
38    /// Sum of addressable virtual extents.
39    pub virtual_extent: MemoryExtent,
40    /// Complete manager metadata page, including padding.
41    pub manager_metadata_bytes: u64,
42    /// Manager header bytes.
43    pub manager_header_bytes: u64,
44    /// Manager bucket-table bytes.
45    pub manager_bucket_table_bytes: u64,
46    /// Padding inside the manager metadata page.
47    pub manager_padding_bytes: u64,
48    /// Sum of bytes assigned to all slots, including the ledger.
49    pub allocated_bucket_bytes: u64,
50    /// Assigned bucket bytes beyond virtual extents.
51    pub bucket_slack_bytes: u64,
52    /// Assigned bytes with a current binding, including the ledger.
53    pub known_binding_bytes: u64,
54    /// Assigned bytes without a current binding; not free space.
55    pub unknown_binding_bytes: u64,
56    /// Backing bytes outside the manager's assigned region.
57    pub unmanaged_bytes: u64,
58    /// Metadata bytes read by the bounded collector.
59    pub metadata_bytes_read: u64,
60    /// All 255 usable slots in ID order, including zero-size slots.
61    pub memories: Vec<MemoryAllocation>,
62}
63
64impl MemoryAllocations {
65    // Project physical facts and current host labels from the same runtime.
66    // Unbootstrapped bindings stay unknown; retained ledger bytes grant no labels.
67    fn from_runtime<M: ic_memory::Memory>(
68        runtime: &ic_memory::MemoryRuntime<M>,
69    ) -> Result<Self, InternalError> {
70        let report = runtime
71            .memory_allocations()
72            .map_err(|_| InternalError::store_internal())?;
73        let (bindings, pool) = if runtime.is_bootstrapped() {
74            (
75                runtime
76                    .committed_allocations()
77                    .map_err(|_| InternalError::store_internal())?,
78                Some(
79                    runtime
80                        .allocation_pool()
81                        .map_err(|_| InternalError::store_internal())?,
82                ),
83            )
84        } else {
85            (&[][..], None)
86        };
87        Ok(Self {
88            current_generation: report.summary.current_generation,
89            manager_layout_version: MemoryAllocationSummary::MANAGER_LAYOUT_VERSION,
90            bucket_size_pages: report.summary.bucket_size_pages,
91            bucket_size_bytes: report.summary.bucket_size_bytes(),
92            bucket_capacity: MemoryAllocationSummary::BUCKET_CAPACITY,
93            allocated_buckets: report.summary.allocated_buckets,
94            remaining_buckets: report.summary.remaining_buckets(),
95            maximum_bucket_bytes: report.summary.maximum_bucket_bytes(),
96            physical_extent: report.summary.physical_extent.into(),
97            virtual_extent: report.summary.virtual_extent.into(),
98            manager_metadata_bytes: MemoryAllocationSummary::MANAGER_METADATA_BYTES,
99            manager_header_bytes: MemoryAllocationSummary::MANAGER_HEADER_BYTES,
100            manager_bucket_table_bytes: MemoryAllocationSummary::MANAGER_BUCKET_TABLE_BYTES,
101            manager_padding_bytes: MemoryAllocationSummary::MANAGER_PADDING_BYTES,
102            allocated_bucket_bytes: report.summary.allocated_bucket_bytes(),
103            bucket_slack_bytes: report.summary.bucket_slack_bytes(),
104            known_binding_bytes: report.summary.known_binding_bytes(),
105            unknown_binding_bytes: report.summary.unknown_binding_bytes(),
106            unmanaged_bytes: report.summary.unmanaged_bytes(),
107            metadata_bytes_read: MemoryAllocationSummary::METADATA_BYTES_READ,
108            memories: report
109                .memories
110                .into_iter()
111                .map(|memory| {
112                    MemoryAllocation::from_runtime(
113                        memory,
114                        report.summary.bucket_size_pages,
115                        bindings,
116                        pool,
117                    )
118                })
119                .collect::<Result<_, _>>()?,
120        })
121    }
122}
123
124/// Measured extent in 64-KiB Wasm pages and bytes.
125#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
126pub struct MemoryExtent {
127    /// Extent in Wasm pages.
128    pub wasm_pages: u64,
129    /// Extent in bytes.
130    pub bytes: u64,
131}
132
133impl From<ic_memory::DiagnosticMemorySize> for MemoryExtent {
134    fn from(extent: ic_memory::DiagnosticMemorySize) -> Self {
135        Self {
136            wasm_pages: extent.wasm_pages,
137            bytes: extent.bytes(),
138        }
139    }
140}
141
142/// Current binding of a physical slot; this grants no opening authority.
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
144pub enum MemoryAllocationBinding {
145    /// A declaration bound by successful runtime bootstrap.
146    Current {
147        /// Durable allocation key.
148        stable_key: String,
149        /// Declaring authority.
150        owner: String,
151    },
152    /// Substrate-reserved ledger slot, not evidence of a validated ledger payload.
153    Ledger {
154        /// Ledger allocation key.
155        stable_key: String,
156        /// Ledger authority.
157        owner: String,
158    },
159    /// No current binding is available; historical ownership is not decoded.
160    Unknown,
161}
162
163/// Physical accounting for one usable manager slot.
164#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
165pub struct MemoryAllocation {
166    /// Memory-manager slot ID.
167    pub memory_manager_id: u8,
168    /// Current allocation binding, including explicit unknown ownership.
169    pub binding: MemoryAllocationBinding,
170    /// Whether this slot belongs to the current host pool; absent before bootstrap.
171    pub pool_eligible: Option<bool>,
172    /// Addressable virtual extent, not payload occupancy.
173    pub virtual_extent: MemoryExtent,
174    /// Assigned bucket count.
175    pub allocated_buckets: u16,
176    /// Assigned bucket bytes.
177    pub allocated_bytes: u64,
178    /// Assigned capacity beyond the virtual extent.
179    pub bucket_slack_bytes: u64,
180    /// Unavailable: manager metadata cannot measure live payload occupancy.
181    pub payload_bytes: Option<u64>,
182}
183
184impl MemoryAllocation {
185    // Per-slot byte counts require the containing report's measured geometry.
186    // Keep that context explicit instead of assuming a default bucket size.
187    fn from_runtime(
188        memory: ic_memory::MemoryAllocation,
189        bucket_size_pages: u16,
190        bindings: &[ic_memory::AllocationDeclaration],
191        pool: Option<&ic_memory::MemoryAllocationPool>,
192    ) -> Result<Self, InternalError> {
193        let allocated_bytes = memory.allocated_bytes(bucket_size_pages);
194        let bucket_slack_bytes = memory.bucket_slack_bytes(bucket_size_pages);
195        let binding = if memory.memory_manager_id == ic_memory::MEMORY_MANAGER_LEDGER_ID {
196            MemoryAllocationBinding::Ledger {
197                stable_key: ic_memory::IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
198                owner: ic_memory::IC_MEMORY_AUTHORITY_OWNER.to_string(),
199            }
200        } else if let Some(declaration) = bindings
201            .iter()
202            .find(|binding| binding.slot().id() == memory.memory_manager_id)
203        {
204            let owner = pool
205                .and_then(|pool| pool.authority_for_key(declaration.stable_key()))
206                .ok_or_else(InternalError::store_invariant)?;
207            MemoryAllocationBinding::Current {
208                stable_key: declaration.stable_key().to_string(),
209                owner: owner.to_string(),
210            }
211        } else {
212            MemoryAllocationBinding::Unknown
213        };
214        Ok(Self {
215            memory_manager_id: memory.memory_manager_id,
216            binding,
217            pool_eligible: pool.map(|pool| pool.contains(memory.memory_manager_id)),
218            virtual_extent: memory.virtual_extent.into(),
219            allocated_buckets: memory.allocated_buckets,
220            allocated_bytes,
221            bucket_slack_bytes,
222            // The manager measures addressable capacity, never live payload.
223            payload_bytes: None,
224        })
225    }
226}
227
228// Inspect the existing binding only. Heap-only/native callers can have no
229// runtime; a corrupt or inaccessible runtime must not look like absence.
230pub(super) fn collect_memory_allocations() -> Result<Option<MemoryAllocations>, InternalError> {
231    let Some(runtime) = memory_runtime() else {
232        return Ok(None);
233    };
234    MemoryAllocations::from_runtime(runtime.as_ref()).map(Some)
235}