Skip to content

Latest commit

 

History

History
616 lines (452 loc) · 17.8 KB

File metadata and controls

616 lines (452 loc) · 17.8 KB

UMR — Universal Memory Runtime

Modular, observable, high-performance memory management for C

License: MIT Language Version Platforms CI

Quick Start · Why UMR · Engines · Observability · Integration · Roadmap · Contributing


UMR is an open-source C memory runtime that sits between your application and the OS. It gives you a familiar malloc-style API plus six specialized allocator engines, a live-allocation tracker, leak detection, ASCII heap visualization, binary snapshots, four policy presets, and a CLI tool — all in a single dependency-free C library.

       Application
           │
           ▼
   UMR Public API          umr_malloc · arena · pool · slab · buddy · fragment
           │
           ▼
   Allocation Manager ─── Policy  (FAST · LOW_MEMORY · REALTIME · DEBUG)
           │
    ┌──────┼──────┬────────┬─────────┐
    ▼      ▼      ▼        ▼         ▼
 General  Arena  Pool   Slab/Buddy  Fragment
 Engine   Bump   Fixed  Object      Boundary-tag
           ptr   size   cache       coalescing
           │
           ▼
  Intelligence Layer     tracker · leaks · heatmap · snapshot · JSON
           │
           ▼
   OS Memory Provider    mmap (Linux/macOS) · VirtualAlloc (Windows)

Note: engines are opt-in via their own APIs. umr_malloc routes through the general heap. Specialized engines are never silently selected.


✨ What makes UMR different

Traditional malloc UMR
Black-box heap — no visibility Every allocation is trackable with ID, size, and source location
One strategy for everything Six purpose-built engines you choose explicitly
Leaks are hard to find Built-in leak detector with per-allocation reports
Fragmentation is invisible Fragment engine with quantified external/internal frag ratios
No runtime tuning Four policy presets: FAST, LOW_MEMORY, REALTIME, DEBUG
No tooling umr-cli, ASCII heatmap, JSON dump, binary snapshots

⚡ Quick Start

Requirements

Tool Minimum version
CMake 3.20+
C compiler GCC 12+, Clang 15+, MSVC 2022
Build backend Ninja (recommended) or Make

Build and test

# Clone
git clone https://github.com/universalmemoryruntime/umr.git
cd umr

# Configure + build (Release)
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

# Run the test suite
ctest --test-dir build --output-on-failure

Debug build with memory checks, guard pages, and source-location tracking:

cmake -S . -B build-debug \
  -DCMAKE_BUILD_TYPE=Debug \
  -DUMR_ENABLE_DEBUG=ON \
  -DUMR_USE_GUARD_PAGES=ON
cmake --build build-debug
ctest --test-dir build-debug --output-on-failure

Link with CMake FetchContent

include(FetchContent)
FetchContent_Declare(umr
  GIT_REPOSITORY https://github.com/universalmemoryruntime/umr.git
  GIT_TAG        v0.1.0
)
FetchContent_MakeAvailable(umr)

target_link_libraries(my_app PRIVATE UMR::umr)

Link with pkg-config (after install)

gcc my_app.c $(pkg-config --cflags --libs umr) -o my_app

🚀 Usage

Drop-in general allocator

#include "umr.h"

int main(void) {
    umr_init();

    // Standard allocation patterns — familiar API, observable internals
    char* buf  = (char*)umr_malloc(64);
    int*  nums = (int*)umr_calloc(16, sizeof(int));   // zero-filled
    buf = (char*)umr_realloc(buf, 256);               // grow in place when possible
    umr_free(nums);
    umr_free(buf);

    umr_shutdown();
}

Debug macros record __FILE__ and __LINE__ in every block header (enabled with -DUMR_ENABLE_DEBUG=ON):

void* p = UMR_MALLOC(256);   // recorded: myfile.c:12
UMR_FREE(p);

🔧 Allocator Engines

UMR ships six purpose-built engines. Pick the one that fits your workload.

Arena — bulk temporary allocation

Best for: game frame scratch, per-request server memory, parser nodes.

// One OS allocation on create. All allocs are a pointer bump — near-zero overhead.
umr_arena_t* arena = umr_arena_create(0);   // 0 = default 1 MiB chunk

void* node  = umr_arena_alloc(arena, sizeof(ASTNode));
void* simd  = umr_arena_alloc_aligned(arena, 256, 64);  // SIMD-aligned

umr_arena_reset(arena);    // rewind all — no OS round-trip, chunks are reused
umr_arena_destroy(arena);  // release OS memory

Nested arenas (child lives inside parent's memory, destroyed with parent):

umr_arena_t* child = umr_arena_create_child(parent, 0);

Pool — fixed-size object recycling

Best for: game entities, network connections, message objects.

umr_pool_t* pool = umr_pool_create(sizeof(Enemy), 256);  // 256 objects / chunk

Enemy* e = (Enemy*)umr_pool_alloc(pool);   // O(1) from freelist
// ... use e ...
umr_pool_free(pool, e);                    // O(1) back to freelist

printf("live: %llu\n", (unsigned long long)umr_pool_live_count(pool));
umr_pool_destroy(pool);

Slab — named object caches

Best for: multiple named types, kernel-style object caches, per-cache stats.

umr_slab_cache_t* cache = umr_slab_create("conn_state", sizeof(ConnState));

ConnState* c = (ConnState*)umr_slab_alloc(cache);
// ... use c ...
umr_slab_free(cache, c);

printf("live=%llu  slabs=%llu\n",
       (unsigned long long)umr_slab_live_count(cache),
       (unsigned long long)umr_slab_count(cache));

umr_slab_destroy(cache);

Buddy — power-of-two region management

Best for: GPU/DMA buffers, large variable-size blocks, sub-allocating fixed OS reservations.

// Mode 1: UMR manages backing memory
umr_buddy_t* b = umr_buddy_create(8 * 1024 * 1024);  // 8 MiB pool

void* tex = umr_buddy_alloc(b, 512 * 1024);  // rounded to next power-of-two
void* buf = umr_buddy_alloc(b, 4   * 1024);
umr_buddy_free(b, tex);   // buddy pairs merged eagerly
umr_buddy_destroy(b);

// Mode 2: caller supplies the memory (must be power-of-two size)
umr_buddy_t* b2 = umr_buddy_create_from(my_gpu_region, 4 * 1024 * 1024);
umr_buddy_destroy(b2);    // does NOT free your buffer

Fragment Heap — explicit split/merge with fragmentation metrics

Best for: fixed-size memory budgets (embedded, GPU VRAM, network buffers) where you need quantified fragmentation control.

// Three search strategies, selectable at creation
umr_fragment_heap_t* h = umr_fragment_heap_create(256 * 1024, UMR_FRAG_BEST_FIT);
//                                                              UMR_FRAG_FIRST_FIT
//                                                              UMR_FRAG_SEGREGATED

void* a = umr_fragment_alloc(h, 1024);
void* b = umr_fragment_alloc(h, 512);

umr_fragment_release(h, a);   // immediate coalescing with adjacent free blocks

// Resize in-place (shrink) or copy-and-grow (like realloc)
b = umr_fragment_resize(h, b, 2048);

// Quantified fragmentation metrics
umr_frag_metrics_t m;
umr_fragment_metrics_get(h, &m);
printf("external frag: %.1f%%  largest free: %zu bytes\n",
       m.external_frag_ratio * 100.0, m.largest_free_block);

umr_fragment_metrics_print(h, stdout);   // full table to terminal
umr_fragment_heap_destroy(h);

Picking the right engine

Workload Recommended engine
Bulk temporaries sharing one lifetime Arena
Hot single-type alloc/free cycle Pool
Multiple named types with per-cache stats Slab
Variable power-of-two sizes, GPU/DMA Buddy
Fixed budget, arbitrary sizes, frag visibility Fragment
Everything else umr_malloc

🔍 Observability

The intelligence layer is the feature that separates UMR from every other allocator.

Live stats — always available

umr_stats_t s;
umr_stats_get(&s);
printf("active: %zu bytes  peak: %zu bytes  frag: %.2f%%  live: %llu allocs\n",
       s.active_bytes, s.peak_active_bytes,
       s.fragmentation_ratio * 100.0,
       (unsigned long long)s.live_count);

umr_stats_print(stdout);    // formatted table
umr_viz_print_stats(stdout); // extended breakdown with per-engine summary

Allocation tracker

umr_tracker_enable();          // zero overhead when disabled (default)

// ... run workload ...

umr_viz_print_tracker(stdout); // tabular live allocation list
umr_leak_print(stdout);        // leak report

umr_leak_clear();              // declare current state as clean baseline
umr_tracker_disable();

Sampling mode for high-throughput paths:

umr_tracker_set_sample_rate(16);  // record 1 in 16 allocations

ASCII heap heatmap

umr_viz_print_heatmap(stdout, 64);  // 64 chars wide
=== UMR Heap Heatmap (25 spans, 1 cell = 1 span ~256 KiB) ===
  Legend: '.' empty  'l' low(1-25%)  'm' mid  'h' high  'F' full  'L' large

.l..l...l...l...l.lllllLL

Leak detection

// Programmatic check with full report
umr_leak_entry_t entries[64];
umr_leak_report_t report = { .entries = entries, .capacity = 64 };
size_t leaked = umr_leak_check(&report);
if (leaked) {
    for (size_t i = 0; i < report.count; ++i)
        printf("LEAK  %p  %zu bytes\n",
               report.entries[i].ptr, report.entries[i].size);
}

// Or one-liner to stderr
umr_leak_print(NULL);   // NULL → stderr

umr_shutdown() automatically reports any live allocations when the tracker is enabled.

JSON snapshot for tooling

umr_viz_dump_json(stdout);      // machine-readable stats + live allocations
umr_snapshot_save("heap.snap"); // binary snapshot for offline analysis
umr-cli json > heap-snapshot.json   # via CLI

🎛️ Policies

Apply a preset that tunes the runtime for your use case:

umr_policy_apply(UMR_POLICY_FAST_MODE);       // default — max throughput
umr_policy_apply(UMR_POLICY_LOW_MEMORY_MODE); // aggressively decommit idle spans
umr_policy_apply(UMR_POLICY_REALTIME_MODE);   // pre-warm, avoid syscalls on hot path
umr_policy_apply(UMR_POLICY_DEBUG_MODE);      // full tracker + canaries + quarantine

List all policies with descriptions:

umr_policy_list(stdout);
Available UMR policies:
  0  FAST_MODE        Maximum throughput. Tracker off. Default.
  1  LOW_MEMORY_MODE  Minimise OS footprint. Idle spans decommitted on free.
  2  REALTIME_MODE    Low-latency. Common size classes pre-warmed.
  3  DEBUG_MODE       Full observability. Tracker on. Leak report on shutdown.

🛠️ umr-cli

The umr-cli tool links against the library and exercises the in-process observability API directly:

umr-cli stats                    # extended heap stats
umr-cli leaks --enable-tracker   # leak report (exit code 2 if leaks found)
umr-cli heatmap 80               # ASCII heatmap at 80 columns
umr-cli tracker --enable-tracker # live allocation table
umr-cli json                     # JSON snapshot to stdout

Exit codes: 0 success · 1 usage error · 2 leaks detected.


⚙️ CMake & Integration

CMake options

Option Default Description
UMR_BUILD_SHARED OFF Build as shared library instead of static
UMR_BUILD_TESTS ON Build the unit + thread test suite
UMR_BUILD_EXAMPLES ON Build all example programs
UMR_BUILD_BENCHMARKS OFF Build microbenchmarks (vs libc)
UMR_ENABLE_DEBUG OFF Magic checks, source metadata, canaries
UMR_USE_GUARD_PAGES OFF Inaccessible guard page after large allocs
UMR_BUILD_CLI ON Build umr-cli tool
UMR_INSTALL ON Generate install rules + CMake package config

Full example with debug + guard pages

cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DUMR_ENABLE_DEBUG=ON \
  -DUMR_USE_GUARD_PAGES=ON \
  -DUMR_BUILD_BENCHMARKS=ON

cmake --build build --parallel
ctest --test-dir build --output-on-failure

AddressSanitizer / ThreadSanitizer

# ASan
cmake -S . -B build-asan \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DUMR_ENABLE_DEBUG=ON \
  -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build build-asan
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 ctest --test-dir build-asan

# TSan
cmake -S . -B build-tsan \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DCMAKE_C_FLAGS="-fsanitize=thread -fno-omit-frame-pointer" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
cmake --build build-tsan
ctest --test-dir build-tsan

Using WSL on Windows (bypasses AppLocker)

If your Windows environment blocks freshly compiled executables, build and run through WSL:

wsl bash run_simple_test.sh

📁 Repository Layout

umr/
├── core/               Runtime lifecycle, policy, TLS scratch heap
├── allocator/          Engine implementations
│   ├── general_allocator.c
│   ├── arena.c
│   ├── pool.c
│   ├── slab.c
│   └── buddy.c
├── fragment/           Fragment engine (split, merge, manager)
├── heap/               Global heap singleton + metadata
├── debug/              Canaries, quarantine, guard pages
├── profiler/           Tracker, leak detector, visualizer, snapshot
├── platform/           OS memory provider (linux / windows / macos)
├── include/
│   ├── umr.h           ← Public API (stable, ABI freeze post v1)
│   └── umr/internal/   ← Private headers (no ABI guarantee)
├── tests/              One test file per module + test_harness.h
├── benchmarks/         Microbenchmarks vs libc
├── examples/           Self-contained usage programs
├── tools/umr-cli/      In-process CLI
├── docs/               Architecture, vision, feature specs, roadmap
└── cmake/              CMake package config + pkg-config templates

🧪 Running the Simple Test

A single file covering all four areas — allocator, memory clearing, fragmentation, and visualization:

# On Linux / macOS
cmake --build build --target umr_simple_test
./build/umr_simple_test

# On Windows via WSL
wsl bash run_simple_test.sh

Expected output:

UMR Simple Test  (v0.1.0)
============================================================

=== 1. Allocator ===
  [PASS] malloc(0) returns NULL
  [PASS] calloc bytes are zero
  ...

=== 3. Fragmentation (fragment heap) ===
  -- strategy: BEST_FIT --
  fragment metrics after checkerboard:
    region=262144  live=1024  free=260800  ext_frag=0.002

=== 4. Visualization (terminal output) ===
  --- umr_viz_print_heatmap (64 cols) ---
  .l..l...l...l...l.lllllLL

============================================================
Results: 76 passed, 0 failed

🗺️ Roadmap

Version Status Contents
v0.1.0 Current General allocator · Arena · Pool · Slab · Buddy · Fragment engine · Tracker · Leak detector · Visualizer · Snapshot · Policies · CLI · CI
v0.2.0 Planned Performance tuning · Benchmark suite vs mimalloc/jemalloc · Thread-local caches
v0.3.0 Planned Relocatable handles (UMR_Handle) · Compaction experiments
v1.0.0 Planned Stable ABI promise · Doxygen + MkDocs site · Published benchmark report
v2.x Future Auto-fragmentation optimization · Cold memory compression · Remote heap debugger

What's not in v1 by design: garbage collection, automatic pointer relocation, compiler plugins, kernel-mode operation.


📖 Documentation

Doc Contents
docs/vision.md Why UMR exists and what it's trying to be
docs/plan.md Phased roadmap and delivery plan
docs/feature.md Feature-by-feature specification and API contracts
docs/architecture.md System design, module layout, threading model
CONTRIBUTING.md Build setup, coding standards, PR process

Build the full docs site locally:

pip install mkdocs-material
mkdocs serve

Generate API reference:

doxygen Doxyfile

🤝 Contributing

Contributions are welcome. Before opening a PR:

# 1. Configure a development build
cmake -S . -B build-dev \
  -DCMAKE_BUILD_TYPE=Debug \
  -DUMR_ENABLE_DEBUG=ON \
  -DUMR_BUILD_TESTS=ON \
  -DUMR_BUILD_BENCHMARKS=ON

# 2. Build and run all tests
cmake --build build-dev
ctest --test-dir build-dev --output-on-failure

# 3. Check for regressions
./build-dev/umr_bench_suite

See CONTRIBUTING.md for the full guide: coding standards, naming conventions, commit style, PR template, and design principles.

Good first issues

  • Adding a new example program in examples/
  • Improving Doxygen comments in include/umr.h
  • Writing a benchmark comparing two strategies in benchmarks/
  • Extending test_fragment.c with edge-case merge tests

CI

Every push and pull request runs the full matrix via GitHub Actions:

Job Platform Checks
Build + test Ubuntu, Windows, macOS Release + Debug
AddressSanitizer Ubuntu Buffer overflow, use-after-free
ThreadSanitizer Ubuntu Data races

📄 License

MIT © 2026 UniversalMemoryRuntime — see LICENSE for the full text.


Built with care for systems programmers, game developers, and anyone who believes memory should be observable, controllable, and fast.