Memory Allocators
Pinned/unmanaged memory primitives every page cache, segment, and hash-map layers on for stable, GC-immune addresses.
Status: β Implemented Β· Visibility: Internal Β· Category: Foundation
π― What it solves
Typhon's hot paths hold raw pointers into engine-owned memory for microseconds at a time β page cache buffers, B+Tree node arenas, hash-map backing storage. A garbage collector that's free to move or compact objects mid-access would invalidate those pointers and corrupt unsafe code. Every such allocation also needs to show up somewhere when something leaks: a forgotten unmanaged block doesn't throw, it just silently grows the process. Memory Allocators give engine subsystems addresses that never move and a parent-owned accounting trail for every byte taken.
βοΈ How it works (in brief)
A MemoryAllocator hands out two flavors of block: unmanaged ones backed by NativeMemory.AlignedAlloc (no GC interaction at all, optional power-of-2 alignment) and managed ones backed by a regular managed byte array (GC-allocated on the normal movable heap β not the Pinned Object Heap β and pinned on demand via GCHandle when a raw pointer is needed). Every block is created with an explicit owning resource and is registered as that owner's child in the engine's resource tree, not the allocator's β so a leaked block shows up under the subsystem that requested it, not lost in a flat pool. Higher-level allocators (BlockAllocator for fixed-stride slots, ChainedBlockAllocator for linked chains, StructAllocator<T> for typed slots) all draw their backing storage from a MemoryAllocator and add their own allocation/free semantics on top.
π» Usage
Application code never calls the allocator directly β it's wired once at host startup and consumed internally by every engine subsystem that needs raw memory (page cache, B+Tree arenas, hash maps):
services.AddResourceRegistry();
services.AddMemoryAllocator(opts => opts.Name = "MyApp Memory Allocator");
// ... AddPagedMemoryMappedFile / AddDatabaseEngine etc. resolve IMemoryAllocator from DI
The only surface application code can touch directly is IMemoryResource β implement it on a custom resource type if you want it to report its size into the resource tree:
internal sealed class MyCache : IMemoryResource
{
public int EstimatedMemorySize => _bufferBytes; // excludes child resources
// ... rest of IResource members
}
| Option | Default | Effect |
|---|---|---|
MemoryAllocatorOptions.Name |
"Default Memory Allocator" |
Display name of the allocator's resource-tree node. |
β οΈ Guarantees & limits
- Stable addresses β unmanaged blocks never move for their lifetime; managed blocks only move while unpinned, and pinning is ref-counted so concurrent pinners are safe.
- Leak visibility, not leak prevention β every block is a child resource of its caller, so an undisposed block is visible in the resource tree (and via
MemoryAllocator'sPinnedBytes/PinnedLiveBlocksmetrics) rather than silently invisible; nothing reclaims it automatically. - Resource tree is organized by owner, not by allocator β a block's parent is the subsystem that requested it (e.g. a hash map or chained allocator), so diagnostics group memory by what's using it.
- Alignment is caller's choice, not free β unmanaged allocation accepts a power-of-2 alignment; requesting one larger than the natural size wastes the padding (tracked via the
Memory:AlignmentWasteprofiler event when alignment-waste tracing is on). - No automatic compaction or defragmentation β block allocators (
BlockAllocator,ChainedBlockAllocator) hand out fixed-stride slots from a freelist; freeing a slot makes it reusable but never shrinks the backing block. - Profiler-attributed β every alloc/free carries a 16-bit source tag so allocation volume can be attributed back to the originating subsystem when memory tracing is enabled; zero cost when it's off.
π§ͺ Tests
- MemoryAllocatorInstrumentationTests β
PinnedBytes/PinnedLiveBlockscounters, peak-never-regresses, source-tag attribution and default "unattributed" tag. - BlockAllocatorTests β fixed-stride slot alloc/free/freelist reuse via
BlockAllocator. - StructAllocatorTests β typed slots with
ICleanablereset-on-free viaStructAllocator<T>(same file asBlockAllocatorTests). - ChainedBlockAllocatorTests β linked-chain block growth via
ChainedBlockAllocator.
π Related
- Related feature: Resource Tree & Registry β how
IMemoryResourceplugs into the broader resource tree - Sibling: Page Allocation & Occupancy Tracking β the page cache draws its backing storage from this allocator.