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