Skip to main content

ic_memory/runtime/
allocations.rs

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