Skip to main content

ic_memory/runtime/
allocations.rs

1use super::{MemoryManagerLayoutError, MemoryRuntime, RuntimeDiagnosticError, layout};
2use crate::{DiagnosticMemorySize, MEMORY_MANAGER_LEDGER_ID, WASM_PAGE_SIZE_BYTES};
3use ic_stable_structures::Memory;
4use serde::Serialize;
5
6///
7/// MemoryAllocation
8///
9/// Measured allocation of one usable ID, including zero-size IDs. Virtual
10/// extent is addressable capacity, never live payload occupancy. Bucket slack
11/// is assigned bucket capacity beyond virtual extent; it says nothing about
12/// unused bytes inside the virtual extent.
13///
14
15#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
16pub struct MemoryAllocation {
17    pub memory_manager_id: u8,
18    pub virtual_extent: DiagnosticMemorySize,
19    pub allocated_buckets: u16,
20}
21
22///
23/// MemoryAllocations
24///
25/// Owned, bounded, read-only physical allocation accounting for one runtime's
26/// backing memory. Exactly 255 entries are ordered by ID. Numeric sizes are
27/// measured from validated persisted metadata or exact arithmetic on those
28/// measurements; none are estimates. Physical extent is the supplied backing
29/// memory's extent, which is the IC stable extent only for that backing type.
30///
31/// Conservation: `physical_extent.bytes = manager_metadata_bytes +
32/// allocated_bucket_bytes + unmanaged_bytes`. Also `allocated_bucket_bytes =
33/// sum(memories.allocated_bytes) = virtual_extent.bytes + bucket_slack_bytes`.
34/// Known binding bytes include the reserved ledger slot; unknown binding bytes
35/// include allocations whose historical owners were deliberately not decoded.
36///
37
38#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
39pub struct MemoryAllocations {
40    /// Numeric accounting shared with the summary-only report.
41    pub summary: MemoryAllocationSummary,
42    pub memories: Vec<MemoryAllocation>,
43}
44
45///
46/// MemoryBindingSummary
47///
48/// Numeric bucket allocation and slack for one binding provenance partition.
49/// These values measure capacity, never payload occupancy.
50///
51
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
53pub struct MemoryBindingSummary {
54    pub allocated_buckets: u16,
55    pub virtual_extent: DiagnosticMemorySize,
56}
57
58///
59/// MemoryAllocationSummary
60///
61/// Bounded numeric allocation accounting without per-ID rows, stable keys,
62/// owners or pool metadata. Uses the same validated metadata and conservation
63/// equations as [`MemoryAllocations`]. Binding groups describe current runtime
64/// provenance; unknown includes omitted or retired keys without reading history.
65/// No payload occupancy is available.
66///
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
69pub struct MemoryAllocationSummary {
70    /// In-memory committed generation; unavailable before bootstrap.
71    pub current_generation: Option<u64>,
72    /// Actual persisted bucket size, never a requested/default assumption.
73    pub bucket_size_pages: u16,
74    pub allocated_buckets: u16,
75    pub physical_extent: DiagnosticMemorySize,
76    pub virtual_extent: DiagnosticMemorySize,
77    pub current_binding: MemoryBindingSummary,
78    pub ledger_binding: MemoryBindingSummary,
79    pub unknown_binding: MemoryBindingSummary,
80}
81
82impl MemoryAllocation {
83    /// Bucket capacity assigned to this ID under the measured bucket geometry.
84    #[must_use]
85    pub const fn allocated_bytes(&self, bucket_size_pages: u16) -> u64 {
86        self.allocated_buckets as u64 * bucket_size_pages as u64 * WASM_PAGE_SIZE_BYTES
87    }
88
89    /// Assigned capacity beyond this ID's virtual extent, not live payload occupancy.
90    /// Use the containing report's measured `bucket_size_pages`.
91    #[must_use]
92    pub const fn bucket_slack_bytes(&self, bucket_size_pages: u16) -> u64 {
93        self.allocated_bytes(bucket_size_pages) - self.virtual_extent.bytes()
94    }
95}
96
97impl MemoryBindingSummary {
98    /// Assigned capacity for this provenance group under the measured geometry.
99    #[must_use]
100    pub const fn allocated_bytes(self, bucket_size_pages: u16) -> u64 {
101        self.allocated_buckets as u64 * bucket_size_pages as u64 * WASM_PAGE_SIZE_BYTES
102    }
103
104    /// Assigned capacity beyond the group's virtual extents.
105    /// Use the containing report's measured `bucket_size_pages`.
106    #[must_use]
107    pub const fn bucket_slack_bytes(self, bucket_size_pages: u16) -> u64 {
108        self.allocated_bytes(bucket_size_pages) - self.virtual_extent.bytes()
109    }
110}
111
112impl MemoryAllocationSummary {
113    /// Supported, validated manager layout identity.
114    pub const MANAGER_LAYOUT_VERSION: u8 = 1;
115    /// Finite table capacity, excluding metadata and backing limits.
116    pub const BUCKET_CAPACITY: u32 = layout::BUCKET_CAPACITY as u32;
117    /// The complete metadata page, including header, table and padding.
118    pub const MANAGER_METADATA_BYTES: u64 = WASM_PAGE_SIZE_BYTES;
119    /// Fixed manager header bytes.
120    pub const MANAGER_HEADER_BYTES: u64 = layout::HEADER_BYTES as u64;
121    /// Fixed bucket ownership table bytes.
122    pub const MANAGER_BUCKET_TABLE_BYTES: u64 = layout::BUCKETS as u64;
123    /// Remaining metadata-page bytes.
124    pub const MANAGER_PADDING_BYTES: u64 = WASM_PAGE_SIZE_BYTES - layout::METADATA_BYTES as u64;
125    /// Exact metadata read budget, independent of ledger history and payload.
126    pub const METADATA_BYTES_READ: u64 = layout::METADATA_BYTES as u64;
127    /// Number of usable IDs, including empty IDs and governance.
128    pub const MEMORIES_MEASURED: u16 = crate::MEMORY_MANAGER_INVALID_ID as u16;
129
130    /// Bytes in each measured bucket.
131    #[must_use]
132    pub const fn bucket_size_bytes(&self) -> u64 {
133        self.bucket_size_pages as u64 * WASM_PAGE_SIZE_BYTES
134    }
135
136    /// Available bucket ownership entries, not backing-memory availability.
137    #[must_use]
138    pub const fn remaining_buckets(&self) -> u32 {
139        Self::BUCKET_CAPACITY - self.allocated_buckets as u32
140    }
141
142    /// Maximum assigned bucket capacity, excluding metadata.
143    #[must_use]
144    pub const fn maximum_bucket_bytes(&self) -> u64 {
145        Self::BUCKET_CAPACITY as u64 * self.bucket_size_bytes()
146    }
147
148    /// Assigned bucket capacity across all IDs.
149    #[must_use]
150    pub const fn allocated_bucket_bytes(&self) -> u64 {
151        self.allocated_buckets as u64 * self.bucket_size_bytes()
152    }
153
154    /// Assigned capacity beyond all virtual extents.
155    #[must_use]
156    pub const fn bucket_slack_bytes(&self) -> u64 {
157        self.allocated_bucket_bytes() - self.virtual_extent.bytes()
158    }
159
160    /// Assigned bytes belonging to current declarations or governance.
161    #[must_use]
162    pub const fn known_binding_bytes(&self) -> u64 {
163        (self.current_binding.allocated_buckets as u64
164            + self.ledger_binding.allocated_buckets as u64)
165            * self.bucket_size_bytes()
166    }
167
168    /// Assigned bytes with no current declaration binding.
169    #[must_use]
170    pub const fn unknown_binding_bytes(&self) -> u64 {
171        self.unknown_binding.allocated_bytes(self.bucket_size_pages)
172    }
173
174    /// Backing bytes after metadata and the assigned bucket region.
175    #[must_use]
176    pub const fn unmanaged_bytes(&self) -> u64 {
177        self.physical_extent.bytes() - Self::MANAGER_METADATA_BYTES - self.allocated_bucket_bytes()
178    }
179}
180
181impl<M: Memory> MemoryRuntime<M> {
182    /// Measure all IDs with a fixed metadata read and bounded current bindings.
183    ///
184    /// Reads at most 34,848 backing bytes. Never initializes stores, decodes the ledger, writes,
185    /// grows memory, or advances a generation. Available before bootstrap;
186    /// current provenance totals are then unavailable. Host labels and pool
187    /// eligibility are projected separately from borrowed runtime bindings.
188    pub fn memory_allocations(&self) -> Result<MemoryAllocations, RuntimeDiagnosticError> {
189        let (measured, summary) = self.measure_allocations()?;
190        let mut memories = Vec::with_capacity(layout::IDS);
191        for id in 0..255_u8 {
192            let index = usize::from(id);
193            let virtual_extent = DiagnosticMemorySize::from_wasm_pages(measured.pages[index]);
194            memories.push(MemoryAllocation {
195                memory_manager_id: id,
196                virtual_extent,
197                allocated_buckets: measured.buckets[index],
198            });
199        }
200        Ok(MemoryAllocations { summary, memories })
201    }
202
203    /// Measure numeric totals and binding partitions without constructing per-ID
204    /// rows or copying keys, owners or pool metadata. Reads at most 34,848 metadata
205    /// bytes; no ledger history, writes, growth, or generation changes occur.
206    pub fn memory_allocation_summary(
207        &self,
208    ) -> Result<MemoryAllocationSummary, RuntimeDiagnosticError> {
209        self.measure_allocations().map(|(_, summary)| summary)
210    }
211
212    fn measure_allocations(
213        &self,
214    ) -> Result<(layout::Layout, MemoryAllocationSummary), RuntimeDiagnosticError> {
215        let declarations = self.committed_allocations().ok();
216        let measured = layout::read(self.growth.backing.as_ref())?;
217        let live_buckets = self
218            .growth
219            .allocated_buckets
220            .try_borrow()
221            .map_err(|_| RuntimeDiagnosticError::ReentrantAccess)?;
222        if measured.bucket_pages != self.growth.bucket_size_pages
223            || measured.allocated_buckets != *live_buckets
224        {
225            return Err(super::RuntimeConstructionError::Layout(
226                MemoryManagerLayoutError::RuntimeMismatch,
227            )
228            .into());
229        }
230        let mut current = [false; layout::IDS];
231        if let Some(snapshot) = declarations {
232            for declaration in snapshot {
233                let id = declaration.slot().id();
234                current[usize::from(id)] = true;
235            }
236        }
237        let mut groups = [MemoryBindingSummary::default(); 3];
238        let mut total_pages = 0;
239        for id in 0..255_u8 {
240            let index = usize::from(id);
241            if self.memory_size_pages(id) != measured.pages[index] {
242                return Err(super::RuntimeConstructionError::Layout(
243                    MemoryManagerLayoutError::RuntimeMismatch,
244                )
245                .into());
246            }
247            let group = if id == MEMORY_MANAGER_LEDGER_ID {
248                1
249            } else if current[index] {
250                0
251            } else {
252                2
253            };
254            groups[group].allocated_buckets += measured.buckets[index];
255            groups[group].virtual_extent.wasm_pages += measured.pages[index];
256            total_pages += measured.pages[index];
257        }
258        let physical_extent = DiagnosticMemorySize::from_wasm_pages(measured.physical_pages);
259        let virtual_extent = DiagnosticMemorySize::from_wasm_pages(total_pages);
260        let summary = MemoryAllocationSummary {
261            current_generation: self.committed_generation().ok(),
262            bucket_size_pages: measured.bucket_pages,
263            allocated_buckets: measured.allocated_buckets,
264            physical_extent,
265            virtual_extent,
266            current_binding: groups[0],
267            ledger_binding: groups[1],
268            unknown_binding: groups[2],
269        };
270        Ok((measured, summary))
271    }
272}