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