Table of Contents

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's PinnedBytes / PinnedLiveBlocks metrics) 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:AlignmentWaste profiler 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