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