Memory Management
Memory Management
Overview
This document provides the technical details of memory management in ic-dbms, also known as Layer 0 (the Memory Layer). Understanding this layer is useful for:
- Performance optimization
- Debugging memory issues
- Contributing to ic-dbms
- Understanding storage costs
How Internet Computer Memory Works
On the Internet Computer, canisters have access to stable memory that persists across upgrades. Key characteristics:
- Page-based: Memory is divided into 64 KiB (65,536 bytes) pages
- Growable: Canisters start small and can allocate additional pages
- Persistent: Survives canister upgrades
- Limited: Subject to subnet memory limits
ic-dbms uses stable memory directly (not the heap) to ensure data persistence.
Memory Model
┌─────────────────────────────────────────────┐
│ Page 0: Schema Registry (65 KiB) │
│ - Table fingerprints → Page ledger pages │
├─────────────────────────────────────────────┤
│ Page 1: ACL Table (65 KiB) │
│ - List of allowed principals │
├─────────────────────────────────────────────┤
│ Page 2: Table "users" Page Ledger │
├─────────────────────────────────────────────┤
│ Page 3: Table "users" Free Segments Ledger │
├─────────────────────────────────────────────┤
│ Page 4: Table "posts" Page Ledger │
├─────────────────────────────────────────────┤
│ Page 5: Table "posts" Free Segments Ledger │
├─────────────────────────────────────────────┤
│ Page 6: Table "users" Records - Page 1 │
├─────────────────────────────────────────────┤
│ Page 7: Table "users" Records - Page 2 │
├─────────────────────────────────────────────┤
│ Page 8: Table "posts" Records - Page 1 │
├─────────────────────────────────────────────┤
│ ... │
└─────────────────────────────────────────────┘
Layout characteristics:
- Reserved pages (0-1) are allocated at initialization
- Each table gets its own Page Ledger and Free Segments Ledger
- Record pages are allocated on demand
- Pages can be interleaved between tables
Memory Provider
The MemoryProvider trait abstracts memory access:
pub trait MemoryProvider {
/// Size of a memory page in bytes (64 KiB for IC)
const PAGE_SIZE: u64;
/// Current memory size in bytes
fn size(&self) -> u64;
/// Number of allocated pages
fn pages(&self) -> u64;
/// Grow memory by new_pages
/// Returns previous size on success
fn grow(&mut self, new_pages: u64) -> MemoryResult<u64>;
/// Read bytes from memory at offset
fn read(&self, offset: u64, buf: &mut [u8]) -> MemoryResult<()>;
/// Write bytes to memory at offset
fn write(&mut self, offset: u64, buf: &[u8]) -> MemoryResult<()>;
}
Implementations:
| Implementation | Use Case |
|---|---|
IcMemoryProvider |
Production (uses ic_cdk::stable::*) |
HeapMemoryProvider |
Testing (uses Vec<u8>) |
// Production: Uses IC stable memory APIs
pub struct IcMemoryProvider;
#[cfg(target_family = "wasm")]
impl MemoryProvider for IcMemoryProvider {
const PAGE_SIZE: u64 = ic_cdk::stable::WASM_PAGE_SIZE_IN_BYTES;
fn grow(&mut self, new_pages: u64) -> MemoryResult<u64> {
ic_cdk::stable::stable_grow(new_pages)
.map_err(MemoryError::StableMemoryError)
}
fn read(&self, offset: u64, buf: &mut [u8]) -> MemoryResult<()> {
ic_cdk::stable::stable_read(offset, buf);
Ok(())
}
fn write(&mut self, offset: u64, buf: &[u8]) -> MemoryResult<()> {
ic_cdk::stable::stable_write(offset, buf);
Ok(())
}
}
// Testing: Uses heap memory
pub struct HeapMemoryProvider {
memory: Vec<u8>,
}
Memory Manager
The MemoryManager builds on MemoryProvider to handle page allocation:
pub struct MemoryManager<P: MemoryProvider> {
provider: P,
}
// Global instance (thread-local for IC)
thread_local! {
#[cfg(target_family = "wasm")]
pub static MEMORY_MANAGER: RefCell<MemoryManager<IcMemoryProvider>> =
RefCell::new(MemoryManager::init(IcMemoryProvider::default()));
#[cfg(not(target_family = "wasm"))]
pub static MEMORY_MANAGER: RefCell<MemoryManager<HeapMemoryProvider>> =
RefCell::new(MemoryManager::init(HeapMemoryProvider::default()));
}
impl<P: MemoryProvider> MemoryManager<P> {
/// Initialize and allocate reserved pages
fn init(provider: P) -> Self;
/// Page size in bytes
pub const fn page_size(&self) -> u64;
/// ACL page number (always 1)
pub const fn acl_page(&self) -> Page;
/// Schema registry page (always 0)
pub const fn schema_page(&self) -> Page;
/// Allocate a new page, returns page number
pub fn allocate_page(&mut self) -> MemoryResult<Page>;
/// Read data at page + offset
pub fn read_at<D: Encode>(&self, page: Page, offset: PageOffset) -> MemoryResult<D>;
/// Write data at page + offset
pub fn write_at<E: Encode>(&mut self, page: Page, offset: PageOffset, data: &E) -> MemoryResult<()>;
}
Encode Trait
All data stored in memory implements the Encode trait:
pub trait Encode {
/// Size characteristic: Fixed or Dynamic
const SIZE: DataSize;
/// Memory alignment in bytes
/// - For Fixed: must equal size
/// - For Dynamic: minimum 8, default 32
const ALIGNMENT: PageOffset;
/// Encode to bytes
fn encode(&'_ self) -> Cow<'_, [u8]>;
/// Decode from bytes
fn decode(data: Cow<[u8]>) -> MemoryResult<Self>
where
Self: Sized;
/// Size of encoded data
fn size(&self) -> MSize;
}
pub enum DataSize {
/// Fixed size in bytes (e.g., integers)
Fixed(MSize),
/// Variable size (e.g., strings, blobs)
Dynamic,
}
Examples:
| Type | SIZE | ALIGNMENT |
|---|---|---|
Uint32 |
Fixed(4) |
4 |
Int64 |
Fixed(8) |
8 |
Text |
Dynamic |
32 (default) |
Blob |
Dynamic |
32 (default) |
| User-defined record | Dynamic |
Configurable (default 32) |
Schema Registry
The Schema Registry maps tables to their storage pages:
/// Information about a table's storage pages
pub struct TableRegistryPage {
pub pages_list_page: Page, // Page Ledger location
pub free_segments_page: Page, // Free Segments Ledger location
}
/// Maps table fingerprints to storage locations
pub struct SchemaRegistry {
tables: HashMap<TableFingerprint, TableRegistryPage>,
}
Table Fingerprint:
- Unique identifier derived from table schema
- Used to detect schema changes on upgrade
- Enables multiple tables in one canister
ACL Storage
The Access Control List is stored in Page 1:
pub struct AccessControlList {
allowed: Vec<Principal>,
}
impl AccessControlList {
/// Load from stable memory
pub fn load() -> MemoryResult<Self>;
/// Check if principal is allowed
pub fn is_allowed(&self, principal: &Principal) -> bool;
/// Add principal (writes to memory)
pub fn add_principal(&mut self, principal: Principal) -> MemoryResult<()>;
/// Remove principal (writes to memory)
pub fn remove_principal(&mut self, principal: &Principal) -> MemoryResult<()>;
}
Table Registry
Each table has a TableRegistry managing its records:
pub struct TableRegistry<E: Encode> {
_marker: PhantomData<E>,
free_segments_ledger: FreeSegmentsLedger,
page_ledger: PageLedger,
}
Page Ledger
Tracks which pages contain records for this table:
pub struct PageLedger {
ledger_page: Page, // Where this ledger is stored
pages: PageTable, // List of data pages with free space info
}
impl PageLedger {
/// Load from memory
pub fn load(page: Page) -> MemoryResult<Self>;
/// Get page for writing a record
/// Returns existing page with space or allocates new
pub fn get_page_for_record<R: Encode>(&mut self, record: &R) -> MemoryResult<Page>;
/// Commit allocation (update free space tracking)
pub fn commit<R: Encode>(&mut self, page: Page, record: &R) -> MemoryResult<()>;
}
Free Segments Ledger
Tracks free space from deleted/moved records:
pub struct FreeSegmentsLedger {
free_segments_page: Page,
tables: PagesTable, // Pages containing FreeSegmentsTables
}
pub struct FreeSegment {
pub page: Page,
pub offset: PageOffset,
pub size: MSize,
}
impl FreeSegmentsLedger {
/// Insert a free segment (when record is deleted)
pub fn insert_free_segment<E: Encode>(
&mut self,
page: Page,
offset: PageOffset,
record: &E,
) -> MemoryResult<()>;
/// Find reusable space for a record
pub fn find_reusable_segment<E: Encode>(
&self,
record: &E,
) -> MemoryResult<Option<FreeSegmentTicket>>;
/// Commit reused space
pub fn commit_reused_space<E: Encode>(
&mut self,
record: &E,
segment: FreeSegmentTicket,
) -> MemoryResult<()>;
}
Space reuse logic:
- When a record is deleted, its space is added to free segments
- When inserting, check for suitable free segment first
- If found, reuse the space; remaining space becomes new free segment
- Adjacent free segments are merged to reduce fragmentation
Record Storage
Record Encoding
Records are wrapped in RawRecord with a length header:
┌─────────────────────────────────────────┐
│ 2 bytes: Data length (little-endian) │
├─────────────────────────────────────────┤
│ N bytes: Encoded data │
├─────────────────────────────────────────┤
│ Padding to alignment boundary │
└─────────────────────────────────────────┘
Dynamic size example (alignment=32, data=24 bytes):
Bytes 0-1: Data length (24)
Bytes 2-25: Data (24 bytes)
Bytes 26-31: Padding (6 bytes)
Total: 32 bytes (aligned)
Fixed size example (size=14 bytes):
Bytes 0-1: Data length (14)
Bytes 2-15: Data (14 bytes)
Total: 16 bytes (no padding for fixed)
Record Alignment
Alignment ensures efficient memory access:
impl<E: Encode> Alignment for E {
fn alignment() -> usize {
match E::SIZE {
DataSize::Fixed(size) => size as usize,
DataSize::Dynamic => E::ALIGNMENT as usize,
}
}
}
fn align_up<E: Encode>(size: usize) -> usize {
let align = E::alignment();
(size + align - 1) / align * align
}
Configuring alignment:
#[derive(Table, ...)]
#[table = "large_records"]
#[alignment = 64] // Custom alignment for this table
pub struct LargeRecord {
// ...
}
Table Reader
Reading records from a table:
impl<E: Encode> TableRegistry<E> {
pub fn read_all(&self) -> MemoryResult<Vec<E>> {
let mut records = Vec::new();
for page in self.page_ledger.pages() {
let mut offset = 0;
while offset < PAGE_SIZE {
// Read length header
let len = read_u16_le(page, offset);
if len == 0 {
// Skip empty slot
offset += E::alignment();
continue;
}
// Read and decode record
let data = read_bytes(page, offset + 2, len);
let record = E::decode(data)?;
records.push(record);
// Move to next aligned position
offset += align_up::<E>(len + 2);
}
}
Ok(records)
}
}
Read process:
- Read 2 bytes at offset for data length
- If length is 0, skip to next aligned position
- Read
lengthbytes of data - Decode data into record
- Move to next aligned position
- Repeat until end of page
Index Registry
Note: Index Registry is reserved for future use (RFU).
Planned features:
- Secondary indexes for faster queries
- B-tree or hash-based indexes
- Automatic index updates on insert/update/delete