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
113impl<M: Memory> MemoryRuntime<M> {
114    /// Measure all IDs with a fixed metadata read and bounded current bindings.
115    ///
116    /// Reads at most 34,848 backing bytes. Never initializes stores, decodes the ledger, writes,
117    /// grows memory, or advances a generation. Available before bootstrap;
118    /// current declaration/range bindings are then unavailable.
119    pub fn memory_allocations(&self) -> Result<MemoryAllocations, RuntimeDiagnosticError> {
120        let declarations = match &self.lifecycle {
121            RuntimeLifecycle::Unbootstrapped => None,
122            RuntimeLifecycle::Bootstrapped { binding, .. } => Some(&binding.declarations),
123        };
124        // Bound collection before reading metadata or copying any declarations.
125        if declarations.is_some_and(|snapshot| {
126            snapshot.registered_declarations().len() >= layout::IDS
127                || snapshot.range_authority().authorities().len() > layout::IDS
128        }) {
129            return Err(RuntimeDiagnosticError::AllocationBound);
130        }
131        let measured = layout::read(self.backing.as_ref())?;
132        if measured.bucket_pages != self.bucket_size_pages {
133            return Err(super::RuntimeConstructionError::Layout(
134                MemoryManagerLayoutError::RuntimeMismatch,
135            )
136            .into());
137        }
138        let bucket_size_bytes = u64::from(measured.bucket_pages) * WASM_PAGE_SIZE_BYTES;
139        let mut memories = Vec::with_capacity(layout::IDS);
140        let mut total_pages = 0;
141        let mut known_binding_bytes = 0;
142        for id in 0..255_u8 {
143            let index = usize::from(id);
144            if self.memory(id).size() != measured.pages[index] {
145                return Err(super::RuntimeConstructionError::Layout(
146                    MemoryManagerLayoutError::RuntimeMismatch,
147                )
148                .into());
149            }
150            let (binding, range_claim) = current_binding(id, declarations);
151            let virtual_extent = DiagnosticMemorySize::from_wasm_pages(measured.pages[index]);
152            let allocated_bytes = u64::from(measured.buckets[index]) * bucket_size_bytes;
153            if !matches!(binding, AllocationBinding::Unknown) {
154                known_binding_bytes += allocated_bytes;
155            }
156            total_pages += measured.pages[index];
157            memories.push(MemoryAllocation {
158                memory_manager_id: id,
159                binding,
160                range_claim,
161                virtual_extent,
162                allocated_buckets: measured.buckets[index],
163                allocated_bytes,
164                bucket_slack_bytes: allocated_bytes - virtual_extent.bytes,
165                payload_bytes: None,
166            });
167        }
168        let allocated_bucket_bytes = u64::from(measured.allocated_buckets) * bucket_size_bytes;
169        let physical_extent = DiagnosticMemorySize::from_wasm_pages(measured.physical_pages);
170        let virtual_extent = DiagnosticMemorySize::from_wasm_pages(total_pages);
171        Ok(MemoryAllocations {
172            current_generation: self
173                .committed_allocations()
174                .ok()
175                .map(crate::CommittedAllocations::generation),
176            manager_layout_version: 1,
177            bucket_size_pages: measured.bucket_pages,
178            bucket_size_bytes,
179            bucket_capacity: 32_768,
180            allocated_buckets: measured.allocated_buckets,
181            remaining_buckets: 32_768 - u32::from(measured.allocated_buckets),
182            maximum_bucket_bytes: 32_768 * bucket_size_bytes,
183            physical_extent,
184            virtual_extent,
185            manager_metadata_bytes: WASM_PAGE_SIZE_BYTES,
186            manager_header_bytes: layout::HEADER_BYTES as u64,
187            manager_bucket_table_bytes: layout::BUCKETS as u64,
188            manager_padding_bytes: WASM_PAGE_SIZE_BYTES - layout::METADATA_BYTES as u64,
189            allocated_bucket_bytes,
190            bucket_slack_bytes: allocated_bucket_bytes - virtual_extent.bytes,
191            known_binding_bytes,
192            unknown_binding_bytes: allocated_bucket_bytes - known_binding_bytes,
193            unmanaged_bytes: physical_extent.bytes - WASM_PAGE_SIZE_BYTES - allocated_bucket_bytes,
194            metadata_bytes_read: layout::METADATA_BYTES as u64,
195            memories,
196        })
197    }
198}
199
200fn current_binding(
201    id: u8,
202    declarations: Option<&crate::SealedDeclarationSnapshot>,
203) -> (AllocationBinding, Option<AllocationRangeClaim>) {
204    let mut binding = AllocationBinding::Unknown;
205    let mut range_claim = None;
206    if let Some(snapshot) = declarations {
207        if let Some(registration) = snapshot
208            .registered_declarations()
209            .iter()
210            .find(|registration| registration.declaration().slot().memory_manager_id() == Ok(id))
211        {
212            binding = AllocationBinding::Current {
213                stable_key: registration.declaration().stable_key().as_str().to_string(),
214                owner: registration.authority().to_string(),
215            };
216        }
217        if let Some(claim) = snapshot
218            .range_authority()
219            .authorities()
220            .iter()
221            .find(|claim| claim.range().contains(id))
222        {
223            range_claim = Some(AllocationRangeClaim {
224                authority: claim.authority().to_string(),
225                mode: claim.mode(),
226            });
227        }
228    }
229    if id == MEMORY_MANAGER_LEDGER_ID {
230        binding = AllocationBinding::Ledger {
231            stable_key: IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
232            owner: IC_MEMORY_AUTHORITY_OWNER.to_string(),
233        };
234    }
235    (binding, range_claim)
236}