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