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