Skip to main content

ic_memory/runtime/
allocations.rs

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