Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku RealtimeAlloc: Pool Sizing, Contention, and Callback Boundaries

Use Haiku RealtimeAlloc pools with explicit capacity and ownership, while accounting for locks, fallback behavior, and the absence of hard real-time guarantees.

Haiku’s RealtimeAlloc API provides memory pools through rtm_create_pool(), rtm_alloc(), rtm_free(), and related functions. Its name suggests an allocation path for real-time media work, but the public API and current implementation do not promise a hard real-time, wait-free, or allocation-latency bound. The implementation uses mutexes to protect pools and a global pool list. A pool can reduce repeated general-heap allocation in a controlled workload; it cannot by itself make an audio callback safe to block.

That distinction should shape the design. Reserve capacity and exercise the expected workload before starting latency-sensitive work. Keep pool operations out of a strict callback when lock contention would violate the callback budget, or prove the exact calling pattern and acceptable bound for the target system. If the processing path requires deterministic behavior, preallocate buffers before the stream begins and pass ownership through a bounded queue.

Create and own a pool before the critical path

rtm_create_pool() creates a pool for a requested total size and returns it through an output pointer. Check both the status and returned pointer before using the pool. Make pool ownership explicit in the component that controls the media lifecycle, and delete the pool only after every allocation from it has been returned or otherwise retired.

rtm_pool* pool = NULL;
status_t status = rtm_create_pool(&pool, 256 * 1024, "decode scratch");
if (status != B_OK || pool == NULL)
    return status != B_OK ? status : B_NO_MEMORY;

void* block = rtm_alloc(pool, requestedBytes);
if (block == NULL) {
    rtm_delete_pool(pool);
    return B_NO_MEMORY;
}

// Use the block only while its owning pool remains alive.
status = rtm_free(block);
if (status != B_OK)
    return status;

status = rtm_delete_pool(pool);

This is a lifecycle sketch, not a complete resource wrapper. If allocation fails after the pool is created, destroy the pool through a single cleanup path. In production code, store the pool and every outstanding block in an owner that can prove shutdown ordering. Never delete a pool while a worker may still free or use one of its blocks.

The implementation uses area-backed storage and aligns chunks to 256-byte boundaries. Treat the requested total as a capacity budget, not as a promise that every byte can be returned as a user allocation: bookkeeping and alignment consume space. Measure rtm_available() during representative workloads and keep headroom for fragmentation and peak simultaneous allocations.

Understand allocation and fallback behavior

The pool is a free-chunk allocator. The implementation searches free chunks, splits an adequately large chunk, and coalesces adjacent chunks on free. The exact cost depends on the current free list and pool contention. rtm_alloc() takes a pool mutex; rtm_free() must locate and lock the owning pool through shared state. This makes the API unsuitable to label “nonblocking” merely because it uses a preallocated area.

rtm_realloc() may need to allocate a new block, copy data, and free the old one. Do not call it from a path where allocation or copying is unbounded relative to the deadline. The query helpers rtm_size_for() and rtm_phys_size_for() serve different size questions; do not confuse requested usable size with physical chunk size including allocator overhead.

If the intended pool runs out, rtm_alloc() returns NULL; callers must handle that failure without corrupting the current media buffer. Avoid silently falling back to malloc() inside a callback. A fallback can appear to work during testing and later introduce unbounded latency exactly when the system is under memory pressure. Define a drop, reuse, or upstream backpressure policy before deployment.

rtm_default_pool() offers an API-provided default pool, but it does not remove the need to check allocation failure, coordinate ownership, or measure contention. If the application requires an isolated capacity budget, create and manage a dedicated pool rather than assuming that the default pool has the desired size or isolation.

Pool capacity is a concurrency budget

Size the pool from the maximum number of simultaneous buffers and their worst-case sizes, not the average. Include scratch buffers, delayed consumers, format conversion, and any queue that can retain old data. If each worker can retain n buffers of maximum size s, then the minimum payload estimate is the active-worker product plus explicit temporary headroom; allocator overhead and fragmentation must be measured separately.

Choose a failure policy for exhaustion. A decoder might reuse a preallocated buffer or stop accepting a new job. A recording path may need to report a dropped frame and preserve the file’s structural integrity. A UI preview may skip an update. Do not let each component invent a different fallback that changes memory pressure unpredictably.

Bound the number of active pool consumers and expose metrics for available bytes, allocation failures, high-water usage, and outstanding allocations. These metrics are more actionable than a single “out of memory” log because they show whether the pool was undersized, leaked, fragmented, or unexpectedly shared.

Match allocation and free lifetimes

rtm_free() receives the allocated pointer rather than a pool argument, so the allocator needs to identify which pool contains the block. Keep pointers unmodified and return each allocation exactly once. Do not pass a pointer from malloc(), another pool, or an interior address unless the API contract explicitly permits it. Set the caller’s pointer to NULL after successful release in application code to reduce accidental double-free paths.

For reallocations, update the owner only when the operation succeeds and the resulting pointer is valid. If a new allocation fails, preserve the old block and its content. Test growth, shrinkage, zero-sized requests, and pool deletion around the exact semantics of the target SDK; do not infer behavior from a different allocator.

Keep media callbacks bounded

An allocator protected by mutexes can block behind another thread. Therefore, the safe pattern for a tight callback is usually to allocate ahead of time, use fixed-size slots, and communicate ownership changes through a bounded queue. Use RealtimeAlloc for setup, noncritical conversion, or a verified path where its locking behavior is acceptable. Do not claim deadline safety from the API name.

If pool calls are unavoidable on a callback path, profile lock contention under concurrent load and define an explicit worst-case policy. A benchmark with a single thread and warm heap does not establish a timing bound. Include CPU contention, cache pressure, pool exhaustion, system memory pressure, and teardown in the test plan.

Failure-oriented verification

Test pool creation failure, zero or tiny sizes, maximum expected simultaneous allocations, exhaustion, repeated alloc/free cycles, fragmentation patterns, realloc failure, concurrent access, and shutdown with outstanding blocks. Verify rtm_available() trends back toward the baseline after each batch and use debug builds to expose ownership mistakes. Ensure every error path frees only blocks that were successfully allocated.

Measure callback duration separately from allocator throughput. Track latency percentiles and worst observed waits under load, but describe them as measurements rather than a guarantee. If deterministic timing is a requirement, use preallocation and a design that avoids locks in the critical path.

Repeat these measurements on the exact architecture and Haiku revision that will ship. Pool alignment, scheduler contention, and memory pressure can differ across machines, and debug heap settings may change whether blocks are cached at all. Keep a test that deliberately enables the debug allocation path so capacity dashboards do not assume every build uses the same reuse behavior.

Acceptance criteria

Accept a RealtimeAlloc integration when pool capacity is derived from worst-case concurrency, allocation failure has a domain-specific response, every pointer has one owner and one release, and critical callbacks do not assume mutex-protected operations are wait-free. Verify exhaustion and shutdown under concurrency on the target Haiku build.

RealtimeAlloc can organize pool-backed memory for media workloads. It does not create a hard real-time guarantee or replace explicit buffer ownership and timing analysis.

Related:

Sources:

Comments