ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Buffer Pool

🗃️ Pre-allocated memory buffers for efficient allocation More...

Files

file  buffer_pool.c
 💾 Lock-free memory pool with atomic operations
 
file  buffer_pool.h
 🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
 

Data Structures

struct  buffer_node
 Node header embedded before user data. More...
 
struct  buffer_pool
 Unified buffer pool with lock-free fast path. More...
 

Macros

#define BUFFER_POOL_MAX_BYTES   (337 * 1024 * 1024)
 Maximum total bytes the pool can hold (337 MB)
 
#define BUFFER_POOL_SHRINK_DELAY_NS   5000000000ULL
 Time before unused buffers are freed (5 seconds in nanoseconds)
 
#define BUFFER_POOL_MIN_SIZE   64
 Minimum buffer size to pool (smaller allocations use malloc directly)
 
#define BUFFER_POOL_MAX_SINGLE_SIZE   (4 * 1024 * 1024)
 Maximum single buffer size to pool (larger allocations use malloc directly)
 
#define BUFFER_POOL_MAGIC   MAGIC_BUFFER_POOL_VALID
 Magic value to identify pooled buffers - Alias for backward compatibility.
 
#define BUFFER_POOL_MAGIC_FALLBACK   MAGIC_BUFFER_POOL_FALLBACK
 Magic value for malloc fallback buffers (not in pool) - Alias for backward compatibility.
 
#define POOL_ALLOC(size)   buffer_pool_alloc(NULL, (size))
 
#define POOL_FREE(data, size)   buffer_pool_free(NULL, (data), (size))
 

Typedefs

typedef struct buffer_node buffer_node_t
 Node header embedded before user data.
 
typedef struct buffer_pool buffer_pool_t
 Unified buffer pool with lock-free fast path.
 

Functions

buffer_pool_t * buffer_pool_create (size_t max_bytes, uint64_t shrink_delay_ns)
 Create a new buffer pool.
 
void buffer_pool_destroy (buffer_pool_t *pool)
 Destroy a buffer pool and free all memory.
 
void * buffer_pool_alloc (buffer_pool_t *pool, size_t size)
 Allocate a buffer from the pool (lock-free fast path)
 
void buffer_pool_free (buffer_pool_t *pool, const void *data, size_t size)
 Free a buffer back to the pool (lock-free)
 
void buffer_pool_shrink (buffer_pool_t *pool)
 Force shrink the pool (free old unused buffers)
 
void buffer_pool_get_stats (buffer_pool_t *pool, size_t *current_bytes, size_t *used_bytes, size_t *free_bytes)
 Get pool statistics (atomic reads)
 
void buffer_pool_log_stats (buffer_pool_t *pool, const char *name)
 Log pool statistics.
 
void buffer_pool_init_global (void)
 
void buffer_pool_cleanup_global (void)
 
buffer_pool_t * buffer_pool_get_global (void)
 

Detailed Description

🗃️ Pre-allocated memory buffers for efficient allocation

A mostly lock-free memory pool using atomic operations for the fast path. Allocations and frees use CAS on a lock-free stack. Only shrinking needs a lock.

DESIGN:

MEMORY LIMITS:

Default max: 337 MB (supports 32 clients at 144fps)

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
December 2025

Buffer Pool

Overview

Welcome! Let's talk about the buffer pool system—one of the secret weapons for making ascii-chat's real-time video streaming fast and smooth.

You know how constantly calling malloc() and free() can slow things down? Well, imagine doing that 30 times per second for video frames, per client! The buffer pool solves this by pre-allocating a bunch of memory buffers up front, so when you need one, it's ready to go. No waiting for the system allocator, no frame drops, no latency spikes—just grab a buffer and get to work.

Think of it like having a stack of clean plates ready for a dinner party. Instead of washing a plate every time someone needs one, you just grab from the stack. Much faster!

Implementation: lib/buffer_pool.c/h

What does the buffer pool give you?

  • Multiple size classes (small/medium/large/xlarge) for different needs
  • Thread-safe operation with mutex protection (multiple threads can use it safely)
  • Detailed statistics so you can see how well it's working
  • Automatic fallback to malloc when pools run dry (graceful degradation)
  • Global singleton pattern for convenience (one pool for the whole app)

Architecture

Size Classes

The buffer pool isn't one-size-fits-all. Instead, it provides four different size classes, each optimized for different types of data you'll be working with:

Size Class Buffer Size Pool Count Total Memory What's it good for?
Small 1 KB 1024 1 MB Audio packets (nice and compact)
Medium 64 KB 64 4 MB Small video frames
Large 256 KB 32 8 MB Large video frames
XLarge 2 MB 64 128 MB HD video frames (the big stuff)

Total pre-allocated memory: ~141 MB (yeah, it's a decent chunk, but remember—this eliminates malloc overhead for thousands of allocations per second!)

Allocation Strategy

So how does the buffer pool decide which buffer to give you? It's pretty straightforward:

  1. Pick the right size: It selects the smallest size class that can fit your request
  2. Try the pool first: It attempts to grab a buffer from the corresponding pool's free list
  3. Fallback gracefully: If the pool is exhausted, it falls back to regular malloc() (better slow than crashing!)
  4. Track everything: It keeps statistics so you can tune the pool sizes if needed

Here's how it works in practice:

// Request 50 KB buffer
void *buf = buffer_pool_alloc(50 * 1024); // Gets from medium pool (64 KB)
// Request 300 KB buffer
void *buf = buffer_pool_alloc(300 * 1024); // Gets from large pool (256 KB)? Nope! Too small.
// Gets from xlarge pool (2 MB) instead
// When done, return to pool (important!)
buffer_pool_free(buf, 50 * 1024);
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)

Data Structures

Buffer Node

Individual buffer in the pool:

typedef struct buffer_node {
void *data; // Actual buffer memory
size_t size; // Size of this buffer
struct buffer_node *next; // Next free buffer (linked list)
bool in_use; // Debug tracking
struct buffer_node buffer_node_t
Node header embedded before user data.
Node header embedded before user data.
Definition buffer_pool.h:71
atomic_ptr_t next
Next in free list (lock-free)
Definition buffer_pool.h:75
size_t size
Size of user data portion.
Definition buffer_pool.h:74

Single Pool

Pool for one size class:

typedef struct buffer_pool {
buffer_node_t *free_list; // Stack of available buffers
buffer_node_t *nodes; // Pre-allocated node array
void *memory_block; // Single malloc for all buffers
size_t buffer_size; // Size per buffer
size_t pool_size; // Total buffer count
size_t used_count; // Currently in use
// Statistics
uint64_t hits; // Successful allocations from pool
uint64_t misses; // Had to fallback to malloc
uint64_t returns; // Successful returns to pool
uint64_t peak_used; // Peak usage
uint64_t total_bytes_allocated; // Total bytes served
int buffer_size
Size of circular buffer.
Definition grep.c:90
struct buffer_pool buffer_pool_t
Unified buffer pool with lock-free fast path.
unsigned long long uint64_t
Definition common.h:59
Unified buffer pool with lock-free fast path.
Definition buffer_pool.h:85
atomic_t hits
Allocations from free list.
Definition buffer_pool.h:98
atomic_ptr_t free_list
Lock-free stack of available buffers (void* cast from buffer_node_t*)
Definition buffer_pool.h:86
atomic_t returns
Buffers returned to free list.

Pool Manager

Multi-size pool coordinator:

typedef struct data_buffer_pool {
buffer_pool_t *small_pool; // 1 KB buffers
buffer_pool_t *medium_pool; // 64 KB buffers
buffer_pool_t *large_pool; // 256 KB buffers
buffer_pool_t *xlarge_pool; // 2 MB buffers
mutex_t pool_mutex; // Thread safety
// Global statistics
uint64_t total_allocs; // Total requests
uint64_t pool_hits; // Satisfied from pool
uint64_t malloc_fallbacks; // Had to malloc
} data_buffer_pool_t;
Mutex type (POSIX: pthread_mutex_t with debug tracking)

API Usage

Global Pool (Recommended)

For most cases, you'll want to use the global singleton pool. It's simple and convenient— one pool for your whole application:

// Initialize at startup (in main())
data_buffer_pool_init_global();
// Allocate buffer (anywhere in code)
void *buffer = buffer_pool_alloc(64 * 1024);
if (!buffer) {
return SET_ERRNO(ERROR_MEMORY, "Buffer allocation failed");
}
// Use buffer...
memcpy(buffer, frame_data, frame_size);
// Return to pool (MUST specify same size!)
buffer_pool_free(buffer, 64 * 1024);
// Cleanup at shutdown
data_buffer_pool_cleanup_global();
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_MEMORY
Definition error_codes.h:56

CRITICAL: You must pass the same size to buffer_pool_free() that you used in buffer_pool_alloc(). The pool needs to know which size class to return the buffer to. If you mix this up, bad things happen!

Custom Pool

For isolated subsystems, create dedicated pools:

// Create dedicated pool
data_buffer_pool_t *my_pool = data_buffer_pool_create();
// Allocate from custom pool
void *buf = data_buffer_pool_alloc(my_pool, 1024);
// Return to custom pool
data_buffer_pool_free(my_pool, buf, 1024);
// Destroy pool (frees all buffers)
data_buffer_pool_destroy(my_pool);

Statistics

Basic Statistics

Track hit rate for performance tuning:

uint64_t hits, misses;
data_buffer_pool_get_stats(pool, &hits, &misses);
double hit_rate = (hits * 100.0) / (hits + misses);
log_info("Buffer pool hit rate: %.1f%%", hit_rate);
buffer_pool_t * pool
#define log_info(...)
Log an INFO message.
Definition log/log.h:561

Detailed Statistics

Per-size-class analysis:

buffer_pool_detailed_stats_t stats;
data_buffer_pool_get_detailed_stats(pool, &stats);
log_info("Small pool: %llu hits, %llu misses, peak=%llu",
stats.small_hits, stats.small_misses, stats.small_peak_used);
log_info("Medium pool: %llu hits, %llu misses, peak=%llu",
stats.medium_hits, stats.medium_misses, stats.medium_peak_used);
asciichat_error_stats_t stats

Automatic Logging

Log comprehensive stats:

// Log global pool stats
buffer_pool_log_global_stats();
// Log custom pool stats
data_buffer_pool_log_stats(my_pool, "Video encoder pool");

Output example:

* [buffer_pool] Global pool statistics:
*   Small (1KB):   hits=45231 misses=12 peak=890/1024 (86.9%)
*   Medium (64KB): hits=8901 misses=156 peak=48/64 (75.0%)
*   Large (256KB): hits=3421 misses=89 peak=28/32 (87.5%)
*   XLarge (2MB):  hits=142 misses=3 peak=45/64 (70.3%)
*   Overall hit rate: 98.7%
* 

Performance

Benchmarks

Okay, let's talk numbers. How much faster is the buffer pool compared to regular malloc/free? The answer: dramatically faster.

Operation Buffer Pool malloc/free Speedup
Allocate 64KB 120 ns 2,400 ns 20x faster
Allocate 256KB 135 ns 8,100 ns 60x faster
Free 64KB 95 ns 1,200 ns 12.6x faster
Free 256KB 98 ns 3,800 ns 38.8x faster

Test system: AMD Ryzen 9 5950X, 64GB DDR4-3200, Linux 6.1

Real-Time Impact

But what does this mean for real-time video? Glad you asked!

For 30 FPS video (you've got 33.3ms per frame to do everything):

  • malloc/free: 10.5µs per frame (0.03% of your time budget)
  • Buffer pool: 0.35µs per frame (0.001% of your time budget)
  • Savings: 10.15µs per frame = 305µs per second you get back for other work!

Now scale that up to 9 clients × 30 FPS = 270 frames/sec:

  • malloc/free overhead: 2.84ms/sec (oof, that adds up!)
  • Buffer pool overhead: 0.09ms/sec (barely noticeable)
  • Recovered time: 2.75ms/sec for other processing (encoding, networking, rendering, etc.)

Thread Safety

The buffer pool is fully thread-safe:

// Thread 1: Video capture
void* capture_thread(void* arg) {
while (running) {
capture_frame(buf);
enqueue_frame(buf); // Send to encoder
}
}
// Thread 2: Video encoder
void* encoder_thread(void* arg) {
while (running) {
void *buf = dequeue_frame();
encode_frame(buf);
buffer_pool_free(buf, FRAME_SIZE); // Return to pool
}
}
#define FRAME_SIZE
Definition analysis.c:62

Synchronization: Single mutex protects all four pools. This is acceptable because buffer operations are very fast (<200ns). For higher concurrency, consider per-pool mutexes.

Tuning

Pool Sizing

Adjust pool sizes in buffer_pool.h based on workload:

// For higher resolution video (1920x1080 → ASCII):
#define BUFFER_POOL_LARGE_SIZE (512 * 1024) // 512 KB (was 256 KB)
#define BUFFER_POOL_LARGE_COUNT 512 // More buffers
// For lower latency (reduce buffering):
#define BUFFER_POOL_MEDIUM_COUNT 128 // Fewer buffers

Rule of thumb:

  • Pool count ≥ (max_clients × frames_per_second × 2)
  • "× 2" provides headroom for encode/decode pipeline depth

Monitoring

Watch for malloc fallbacks in production:

buffer_pool_detailed_stats_t stats;
data_buffer_pool_get_detailed_stats(global_pool, &stats);
// Alert if miss rate exceeds threshold
double miss_rate = (stats.medium_misses * 100.0) /
(stats.medium_hits + stats.medium_misses);
if (miss_rate > 5.0) {
log_warn("Medium pool miss rate high: %.1f%% - consider increasing pool size",
miss_rate);
}
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574

Best Practices

  1. Always match alloc/free sizes:
    size_t size = 64 * 1024;
    void *buf = buffer_pool_alloc(size);
    // ... use buffer ...
    buffer_pool_free(buf, size); // MUST be same size!
  2. Check allocation failure:
    void *buf = buffer_pool_alloc(size);
    if (!buf) {
    return SET_ERRNO(ERROR_MEMORY, "Out of memory");
    }
  3. Don't hold buffers too long:
    • Return buffers immediately after use
    • Long holds exhaust pool → more malloc fallbacks
  4. Monitor statistics:
    • Log stats periodically in production
    • Tune pool sizes based on miss rates
  5. Use global pool for most cases:
    • Simpler API
    • One pool is usually sufficient
    • Create custom pools only for isolation
See also
buffer_pool.h
buffer_pool.c

Macro Definition Documentation

◆ BUFFER_POOL_MAGIC

#define BUFFER_POOL_MAGIC   MAGIC_BUFFER_POOL_VALID

#include <buffer_pool.h>

Magic value to identify pooled buffers - Alias for backward compatibility.

Definition at line 53 of file buffer_pool.h.

◆ BUFFER_POOL_MAGIC_FALLBACK

#define BUFFER_POOL_MAGIC_FALLBACK   MAGIC_BUFFER_POOL_FALLBACK

#include <buffer_pool.h>

Magic value for malloc fallback buffers (not in pool) - Alias for backward compatibility.

Definition at line 55 of file buffer_pool.h.

◆ BUFFER_POOL_MAX_BYTES

#define BUFFER_POOL_MAX_BYTES   (337 * 1024 * 1024)

#include <buffer_pool.h>

Maximum total bytes the pool can hold (337 MB)

Definition at line 41 of file buffer_pool.h.

◆ BUFFER_POOL_MAX_SINGLE_SIZE

#define BUFFER_POOL_MAX_SINGLE_SIZE   (4 * 1024 * 1024)

#include <buffer_pool.h>

Maximum single buffer size to pool (larger allocations use malloc directly)

Definition at line 50 of file buffer_pool.h.

◆ BUFFER_POOL_MIN_SIZE

#define BUFFER_POOL_MIN_SIZE   64

#include <buffer_pool.h>

Minimum buffer size to pool (smaller allocations use malloc directly)

Definition at line 47 of file buffer_pool.h.

◆ BUFFER_POOL_SHRINK_DELAY_NS

#define BUFFER_POOL_SHRINK_DELAY_NS   5000000000ULL

#include <buffer_pool.h>

Time before unused buffers are freed (5 seconds in nanoseconds)

Definition at line 44 of file buffer_pool.h.

◆ POOL_ALLOC

#define POOL_ALLOC (   size)    buffer_pool_alloc(NULL, (size))

#include <buffer_pool.h>

Definition at line 172 of file buffer_pool.h.

◆ POOL_FREE

#define POOL_FREE (   data,
  size 
)    buffer_pool_free(NULL, (data), (size))

#include <buffer_pool.h>

Definition at line 173 of file buffer_pool.h.

Typedef Documentation

◆ buffer_node_t

typedef struct buffer_node buffer_node_t

#include <buffer_pool.h>

Node header embedded before user data.

Layout in memory: [buffer_node_t header][user data...] ^– pointer returned to caller

◆ buffer_pool_t

typedef struct buffer_pool buffer_pool_t

#include <buffer_pool.h>

Unified buffer pool with lock-free fast path.

Function Documentation

◆ buffer_pool_alloc()

void * buffer_pool_alloc ( buffer_pool_t *  pool,
size_t  size 
)

#include <buffer_pool.h>

Allocate a buffer from the pool (lock-free fast path)

Parameters
poolBuffer pool (NULL = use global pool)
sizeSize of buffer to allocate
Returns
Allocated buffer, or NULL on failure

Definition at line 122 of file buffer_pool.c.

122 {
123 // Use global pool if none specified
124 if (!pool) {
126 }
127
128 // Size out of range - use malloc with node header for consistent cleanup
129 if (!pool || size < BUFFER_POOL_MIN_SIZE || size > BUFFER_POOL_MAX_SINGLE_SIZE) {
130 size_t total_size = sizeof(buffer_node_t) + size;
131 buffer_node_t *node = SAFE_MALLOC(total_size, buffer_node_t *);
132 node->magic = MAGIC_BUFFER_POOL_FALLBACK; // Different magic for fallbacks
133 node->_pad = 0;
134 node->size = size;
135 atomic_ptr_store(&node->next, NULL); // Initialize atomic pointer field
137 node->pool = NULL; // No pool for fallbacks
138
139 if (pool) {
141 }
142 return data_from_node(node);
143 }
144
145 // Try to pop from lock-free stack (LIFO)
146 void *node_ptr = atomic_ptr_load(&pool->free_list);
147 buffer_node_t *node = (buffer_node_t *)node_ptr;
148 while (node) {
149 void *next_ptr = atomic_ptr_load(&node->next);
150 if (atomic_ptr_cas(&pool->free_list, &node_ptr, next_ptr)) {
151 // Successfully popped - check if it's big enough
152 if (node->size >= size) {
153 // Reuse this buffer
154 atomic_ptr_store(&node->next, NULL);
155 size_t node_size = node->size;
158 update_peak(&pool->peak_bytes, atomic_load_u64(&pool->used_bytes));
159 return data_from_node(node);
160 } else {
161 // Too small - push it back and allocate new
162 // (This is rare with LIFO - usually we get similar sizes)
163 void *head = atomic_ptr_load(&pool->free_list);
164 do {
165 atomic_ptr_store(&node->next, head);
166 } while (!atomic_ptr_cas(&pool->free_list, &head, (void *)node));
167 break; // Fall through to allocate new
168 }
169 }
170 // CAS failed - reload and retry
172 }
173
174 // Check if we can allocate more
175 size_t total_size = sizeof(buffer_node_t) + size;
177
178 // Atomically try to reserve space
179 while (current + total_size <= pool->max_bytes) {
180 if (atomic_cas_u64(&pool->current_bytes, &current, current + total_size)) {
181 // Reserved space - now allocate
182 // Allocate node + data in one chunk for cache efficiency
183 node = SAFE_MALLOC_ALIGNED(total_size, 64, buffer_node_t *);
184 if (!node) {
185 // Undo reservation
188 return SAFE_MALLOC(size, void *);
189 }
190
192 node->_pad = 0;
193 node->size = size;
194 atomic_ptr_store(&node->next, NULL); // Initialize atomic pointer field
196 node->pool = pool;
197
200 update_peak(&pool->peak_bytes, atomic_load_u64(&pool->used_bytes));
202
203 return data_from_node(node);
204 }
205 // CAS failed - someone else allocated, reload and check again
206 }
207
208 // Pool at capacity - fall back to malloc with node header for consistent cleanup
209 total_size = sizeof(buffer_node_t) + size;
210 node = SAFE_MALLOC(total_size, buffer_node_t *);
211 if (!node) {
212 return NULL;
213 }
215 node->_pad = 0;
216 node->size = size;
218 node->pool = pool;
219
221 return data_from_node(node);
222}
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
bool atomic_cas_u64(atomic_t *a, uint64_t *expected, uint64_t new_value)
Atomically compare-and-swap a uint64_t.
Definition atomic.c:264
void atomic_ptr_store(atomic_ptr_t *a, void *value)
Atomically store a pointer.
Definition atomic.c:280
void * atomic_ptr_load(atomic_ptr_t *a)
Atomically load a pointer.
Definition atomic.c:272
uint64_t atomic_fetch_sub_u64(atomic_t *a, uint64_t delta)
Atomically subtract from a uint64_t and return the previous value.
Definition atomic.c:256
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
bool atomic_ptr_cas(atomic_ptr_t *a, void **expected, void *new_value)
Atomically compare-and-swap a pointer.
Definition atomic.c:287
buffer_pool_t * buffer_pool_get_global(void)
#define BUFFER_POOL_MAX_SINGLE_SIZE
Maximum single buffer size to pool (larger allocations use malloc directly)
Definition buffer_pool.h:50
#define SAFE_MALLOC_ALIGNED(size, alignment, cast)
Definition common.h:349
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define MAGIC_BUFFER_POOL_VALID
Magic number for valid pooled buffers (0xBF00B001)
Definition magic.h:23
#define MAGIC_BUFFER_POOL_FALLBACK
Magic number for malloc fallback buffers (0xBF00FA11)
Definition magic.h:26
atomic_t returned_at_ns
Timestamp when returned to free list (nanoseconds)
Definition buffer_pool.h:76
uint32_t _pad
Padding for alignment.
Definition buffer_pool.h:73
uint32_t magic
Magic to identify pooled buffers.
Definition buffer_pool.h:72
struct buffer_pool * pool
Owning pool (for free)
Definition buffer_pool.h:77
atomic_t malloc_fallbacks
Allocations that bypassed pool.
atomic_t used_bytes
Bytes currently in use (stored as uint64_t)
Definition buffer_pool.h:94
atomic_t peak_bytes
Peak bytes in use (stored as uint64_t)
Definition buffer_pool.h:95
atomic_t current_bytes
Total bytes in pool (stored as uint64_t)
Definition buffer_pool.h:93
atomic_t allocs
New buffer allocations.
Definition buffer_pool.h:99
atomic_t peak_pool_bytes
Peak bytes in pool (stored as uint64_t)
Definition buffer_pool.h:96

References buffer_node::_pad, allocs, atomic_cas_u64(), atomic_fetch_add_u64(), atomic_fetch_sub_u64(), atomic_load_u64(), atomic_ptr_cas(), atomic_ptr_load(), atomic_ptr_store(), atomic_store_u64(), buffer_pool_get_global(), BUFFER_POOL_MAX_SINGLE_SIZE, current_bytes, free_list, hits, buffer_node::magic, MAGIC_BUFFER_POOL_FALLBACK, MAGIC_BUFFER_POOL_VALID, malloc_fallbacks, buffer_node::next, peak_bytes, peak_pool_bytes, buffer_node::pool, pool, buffer_node::returned_at_ns, SAFE_MALLOC, SAFE_MALLOC_ALIGNED, buffer_node::size, and used_bytes.

Referenced by acip_send_ascii_frame(), acip_send_audio_batch(), acip_send_audio_opus_batch(), acip_send_error(), acip_send_image_frame(), acip_send_image_frame_h265(), acip_send_remote_log(), acip_server_receive_and_dispatch(), av_send_audio_opus_batch(), client_handle_auth_challenge(), client_handle_crypto_parameters(), client_handle_handshake_complete(), client_handle_key_exchange_init(), framebuffer_peek_latest_multi_frame(), framebuffer_write_frame(), framebuffer_write_multi_frame(), image_new_from_pool(), packet_decrypt_envelope(), packet_queue_enqueue(), packet_queue_enqueue_packet(), packet_receive(), process_encrypted_packet(), receive_packet_secure_with_timeout(), send_image_frame_packet(), send_packet_secure(), threaded_send_audio_opus(), threaded_send_image_frame_h265(), and video_frame_buffer_create().

◆ buffer_pool_cleanup_global()

void buffer_pool_cleanup_global ( void  )

#include <buffer_pool.h>

Definition at line 421 of file buffer_pool.c.

421 {
422 if (lifecycle_shutdown(&g_global_pool_state.lifecycle)) {
423 if (g_global_pool_state.pool) {
424 buffer_pool_log_stats(g_global_pool_state.pool, "Global (final)");
425 buffer_pool_destroy(g_global_pool_state.pool);
426 g_global_pool_state.pool = NULL;
427 }
428 }
429}
void buffer_pool_destroy(buffer_pool_t *pool)
Destroy a buffer pool and free all memory.
void buffer_pool_log_stats(buffer_pool_t *pool, const char *name)
Log pool statistics.
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108

References buffer_pool_destroy(), buffer_pool_log_stats(), and lifecycle_shutdown().

Referenced by asciichat_shared_destroy().

◆ buffer_pool_create()

buffer_pool_t * buffer_pool_create ( size_t  max_bytes,
uint64_t  shrink_delay_ns 
)

#include <buffer_pool.h>

Create a new buffer pool.

Parameters
max_bytesMaximum bytes the pool can hold (0 = use default)
shrink_delay_nsTime before unused buffers freed in nanoseconds (0 = use default)
Returns
New buffer pool, or NULL on failure

Definition at line 51 of file buffer_pool.c.

51 {
53 if (!pool) {
54 SET_ERRNO(ERROR_MEMORY, "Failed to allocate buffer pool");
55 return NULL;
56 }
57
58 if (mutex_init(&pool->shrink_mutex, "buffer_pool_shrink") != 0) {
59 SET_ERRNO(ERROR_THREAD, "Failed to initialize shrink mutex");
61 return NULL;
62 }
63
64 pool->max_bytes = max_bytes > 0 ? max_bytes : BUFFER_POOL_MAX_BYTES;
65 pool->shrink_delay_ns = shrink_delay_ns > 0 ? shrink_delay_ns : BUFFER_POOL_SHRINK_DELAY_NS;
66
67 // Initialize atomic pointer fields
69
70 // Initialize atomic counter fields
80
81 char pretty_max[64];
82 format_bytes_pretty(pool->max_bytes, pretty_max, sizeof(pretty_max));
83 log_dev("Created buffer pool (max: %s, shrink: %llu ns, lock-free)", pretty_max,
84 (unsigned long long)pool->shrink_delay_ns);
85
86 // Register pool atomics with named registry for debugging and --sync-state output
87 NAMED_REGISTER_ATOMIC_PTR(&pool->free_list, "buffer_pool.global_free_list_head_ptr", NULL);
88 NAMED_REGISTER_ATOMIC(&pool->current_bytes, "buffer_pool.global_current_bytes_in_pool", NULL);
89 NAMED_REGISTER_ATOMIC(&pool->used_bytes, "buffer_pool.global_bytes_currently_in_use", NULL);
90 NAMED_REGISTER_ATOMIC(&pool->peak_bytes, "buffer_pool.global_peak_bytes_in_use", NULL);
91 NAMED_REGISTER_ATOMIC(&pool->peak_pool_bytes, "buffer_pool.global_peak_bytes_in_pool", NULL);
92 NAMED_REGISTER_ATOMIC(&pool->hits, "buffer_pool.global_allocations_from_free_list", NULL);
93 NAMED_REGISTER_ATOMIC(&pool->allocs, "buffer_pool.global_new_buffer_allocations", NULL);
94 NAMED_REGISTER_ATOMIC(&pool->returns, "buffer_pool.global_buffers_returned_to_pool", NULL);
95 NAMED_REGISTER_ATOMIC(&pool->shrink_freed, "buffer_pool.global_buffers_freed_by_shrink", NULL);
96 NAMED_REGISTER_ATOMIC(&pool->malloc_fallbacks, "buffer_pool.global_allocations_bypassing_pool", NULL);
97
98 // Register sync primitives for debugging
99 NAMED_REGISTER_MUTEX(&pool->shrink_mutex, "shrink_mutex", (uintptr_t)(const void *)(pool));
100
101 return pool;
102}
#define BUFFER_POOL_MAX_BYTES
Maximum total bytes the pool can hold (337 MB)
Definition buffer_pool.h:41
#define BUFFER_POOL_SHRINK_DELAY_NS
Time before unused buffers are freed (5 seconds in nanoseconds)
Definition buffer_pool.h:44
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
#define NAMED_REGISTER_ATOMIC(a, name, parent_ptr)
Register an atomic_t with automatic format specifier.
#define NAMED_REGISTER_ATOMIC_PTR(a, name, parent_ptr)
Register an _Atomic(void *) with automatic format specifier.
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
@ ERROR_THREAD
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Format byte count into human-readable string.
Definition util/format.c:10
size_t max_bytes
Maximum total bytes allowed.
Definition buffer_pool.h:89
uint64_t shrink_delay_ns
Time before unused buffers freed (nanoseconds)
Definition buffer_pool.h:90
mutex_t shrink_mutex
Only used for shrinking.
Definition buffer_pool.h:87
atomic_t shrink_freed
Buffers freed by shrink.

References allocs, atomic_ptr_store(), atomic_store_u64(), BUFFER_POOL_MAX_BYTES, BUFFER_POOL_SHRINK_DELAY_NS, current_bytes, ERROR_MEMORY, ERROR_THREAD, format_bytes_pretty(), free_list, hits, log_dev, malloc_fallbacks, max_bytes, mutex_init(), NAMED_REGISTER_ATOMIC, NAMED_REGISTER_ATOMIC_PTR, NAMED_REGISTER_MUTEX, peak_bytes, peak_pool_bytes, pool, returns, SAFE_CALLOC, SAFE_FREE, SET_ERRNO, shrink_delay_ns, shrink_freed, shrink_mutex, and used_bytes.

Referenced by buffer_pool_init_global(), and packet_queue_create_with_pools().

◆ buffer_pool_destroy()

void buffer_pool_destroy ( buffer_pool_t *  pool)

#include <buffer_pool.h>

Destroy a buffer pool and free all memory.

Parameters
poolPool to destroy

Definition at line 104 of file buffer_pool.c.

104 {
105 if (!pool)
106 return;
107
109
110 // Drain the free list
112 while (node) {
113 buffer_node_t *next = atomic_ptr_load(&node->next);
114 SAFE_FREE(node); // Node and data are one allocation
115 node = next;
116 }
117
120}
#define NAMED_UNREGISTER(ptr)
Unregister a pointer.
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22

References atomic_ptr_load(), free_list, mutex_destroy(), NAMED_UNREGISTER, buffer_node::next, pool, SAFE_FREE, and shrink_mutex.

Referenced by buffer_pool_cleanup_global(), and packet_queue_destroy().

◆ buffer_pool_free()

void buffer_pool_free ( buffer_pool_t *  pool,
const void *  data,
size_t  size 
)

#include <buffer_pool.h>

Free a buffer back to the pool (lock-free)

Parameters
poolBuffer pool (NULL = auto-detect from buffer)
dataBuffer to free
sizeSize of buffer (used for fallback, can be 0 if pooled)

Definition at line 224 of file buffer_pool.c.

224 {
225 (void)size; // Size parameter not needed with header-based detection
226
227 if (!data)
228 return;
229
230 // All buffer_pool allocations have headers, safe to check magic
231 // Cast away const since we're just reading the header, not modifying data
232 buffer_node_t *node = node_from_data((void *)data);
233
234 // If it's a malloc fallback (has fallback magic), free the node directly
236 SAFE_FREE(node); // Free the node (includes header + data)
237 return;
238 }
239
240 // If it's not a pooled buffer (no valid magic), it's external - use platform free
242 // Cast away const for free() which expects non-const void *
243 free((void *)data); // Unknown allocation, just free the data pointer
244 return;
245 }
246
247 // It's a pooled buffer - return to pool
248 // Use the pool stored in the node if none provided
249 if (!pool) {
250 pool = node->pool;
251 }
252
253 if (!pool) {
254 // Shouldn't happen, but safety
255 log_error("Pooled buffer has no pool reference!");
256 return;
257 }
258
259 // Update stats
262
263 // Set return timestamp
265
266 // Push to lock-free stack
267 void *head = atomic_ptr_load(&pool->free_list);
268 do {
269 atomic_ptr_store(&node->next, head);
270 } while (!atomic_ptr_cas(&pool->free_list, &head, (void *)node));
271
272 // Periodically trigger shrink (every 100 returns)
274 if (returns % 100 == 0) {
276 }
277}
void buffer_pool_shrink(buffer_pool_t *pool)
Force shrink the pool (free old unused buffers)
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define IS_MAGIC_VALID(magic, expected)
Check if magic number is valid (true if matches expected)
Definition magic.h:36

References atomic_fetch_add_u64(), atomic_fetch_sub_u64(), atomic_load_u64(), atomic_ptr_cas(), atomic_ptr_load(), atomic_ptr_store(), atomic_store_u64(), buffer_pool_shrink(), free_list, IS_MAGIC_VALID, log_error, buffer_node::magic, MAGIC_BUFFER_POOL_FALLBACK, MAGIC_BUFFER_POOL_VALID, buffer_node::next, buffer_node::pool, pool, buffer_node::returned_at_ns, returns, SAFE_FREE, buffer_node::size, time_get_ns(), and used_bytes.

Referenced by __attribute__(), acds_client_handler(), acds_session_create(), acds_session_join(), acds_session_lookup(), acds_websocket_client_handler(), acip_client_receive_and_dispatch(), acip_send_ascii_frame(), acip_send_audio_batch(), acip_send_audio_opus_batch(), acip_send_error(), acip_send_image_frame(), acip_send_image_frame_h265(), acip_send_remote_log(), acip_server_receive_and_dispatch(), add_client(), audio_ring_buffer_destroy(), av_send_audio_opus_batch(), client_crypto_handshake(), client_handle_crypto_parameters(), client_receive_thread(), discovery_session_process(), discovery_session_start(), framebuffer_clear(), framebuffer_write_frame(), framebuffer_write_multi_frame(), image_destroy(), image_destroy_to_pool(), packet_decrypt_envelope(), packet_queue_enqueue(), packet_queue_enqueue_packet(), packet_queue_free_packet(), packet_queue_try_dequeue(), packet_receive(), packet_receive_via_transport(), process_encrypted_packet(), receive_packet_secure_with_timeout(), send_image_frame_packet(), send_packet_secure(), server_crypto_handshake(), threaded_send_audio_opus(), threaded_send_image_frame_h265(), and video_frame_buffer_destroy().

◆ buffer_pool_get_global()

buffer_pool_t * buffer_pool_get_global ( void  )

#include <buffer_pool.h>

Definition at line 431 of file buffer_pool.c.

431 {
432 return g_global_pool_state.pool;
433}

Referenced by buffer_pool_alloc(), packet_queue_enqueue(), packet_queue_enqueue_packet(), video_frame_buffer_create(), and video_frame_buffer_destroy().

◆ buffer_pool_get_stats()

void buffer_pool_get_stats ( buffer_pool_t *  pool,
size_t *  current_bytes,
size_t *  used_bytes,
size_t *  free_bytes 
)

#include <buffer_pool.h>

Get pool statistics (atomic reads)

Definition at line 344 of file buffer_pool.c.

344 {
345 if (!pool) {
346 if (current_bytes)
347 *current_bytes = 0;
348 if (used_bytes)
349 *used_bytes = 0;
350 if (free_bytes)
351 *free_bytes = 0;
352 return;
353 }
354
355 size_t current = atomic_load_u64(&pool->current_bytes);
356 size_t used = atomic_load_u64(&pool->used_bytes);
357
358 if (current_bytes)
359 *current_bytes = current;
360 if (used_bytes)
361 *used_bytes = used;
362 if (free_bytes)
363 *free_bytes = (current > used) ? (current - used) : 0;
364}

References atomic_load_u64(), current_bytes, pool, and used_bytes.

◆ buffer_pool_init_global()

void buffer_pool_init_global ( void  )

#include <buffer_pool.h>

Definition at line 412 of file buffer_pool.c.

412 {
413 if (lifecycle_init(&g_global_pool_state.lifecycle, "buffer_pool")) {
414 g_global_pool_state.pool = buffer_pool_create(0, 0);
415 if (g_global_pool_state.pool) {
416 log_dev("Initialized global buffer pool");
417 }
418 }
419}
buffer_pool_t * buffer_pool_create(size_t max_bytes, uint64_t shrink_delay_ns)
Create a new buffer pool.
Definition buffer_pool.c:51
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26

References buffer_pool_create(), lifecycle_init(), and log_dev.

Referenced by asciichat_shared_init().

◆ buffer_pool_log_stats()

void buffer_pool_log_stats ( buffer_pool_t *  pool,
const char *  name 
)

#include <buffer_pool.h>

Log pool statistics.

Definition at line 366 of file buffer_pool.c.

366 {
367 if (!pool)
368 return;
369
370 size_t current = atomic_load_u64(&pool->current_bytes);
371 size_t used = atomic_load_u64(&pool->used_bytes);
372 size_t peak = atomic_load_u64(&pool->peak_bytes);
373 size_t peak_pool = atomic_load_u64(&pool->peak_pool_bytes);
377 uint64_t shrink_freed = atomic_load_u64(&pool->shrink_freed);
379
380 char pretty_current[64], pretty_used[64], pretty_free[64];
381 char pretty_peak[64], pretty_peak_pool[64], pretty_max[64];
382
383 format_bytes_pretty(current, pretty_current, sizeof(pretty_current));
384 format_bytes_pretty(used, pretty_used, sizeof(pretty_used));
385 format_bytes_pretty(current > used ? current - used : 0, pretty_free, sizeof(pretty_free));
386 format_bytes_pretty(peak, pretty_peak, sizeof(pretty_peak));
387 format_bytes_pretty(peak_pool, pretty_peak_pool, sizeof(pretty_peak_pool));
388 format_bytes_pretty(pool->max_bytes, pretty_max, sizeof(pretty_max));
389
390 uint64_t total_requests = hits + allocs + fallbacks;
391 double hit_rate = total_requests > 0 ? (double)hits * 100.0 / (double)total_requests : 0;
392
393 log_debug("=== Buffer Pool: %s ===", name ? name : "unnamed");
394 log_debug(" Current: %s / %s capacity (peak used: %s)", pretty_current, pretty_max, pretty_peak_pool);
395 log_debug(" Buffers: %s used, %s free (peak: %s)", pretty_used, pretty_free, pretty_peak);
396 log_debug(" Hits: %llu (%.1f%%), Allocs: %llu, Fallbacks: %llu", (unsigned long long)hits, hit_rate,
397 (unsigned long long)allocs, (unsigned long long)fallbacks);
398 log_debug(" Returns: %llu, Shrink freed: %llu", (unsigned long long)returns, (unsigned long long)shrink_freed);
399}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548

References allocs, atomic_load_u64(), current_bytes, format_bytes_pretty(), hits, log_debug, malloc_fallbacks, max_bytes, peak_bytes, peak_pool_bytes, pool, returns, shrink_freed, and used_bytes.

Referenced by buffer_pool_cleanup_global(), and packet_queue_destroy().

◆ buffer_pool_shrink()

void buffer_pool_shrink ( buffer_pool_t *  pool)

#include <buffer_pool.h>

Force shrink the pool (free old unused buffers)

Parameters
poolBuffer pool
Note
This is the only operation that takes a lock

Definition at line 279 of file buffer_pool.c.

279 {
280 if (!pool || pool->shrink_delay_ns == 0)
281 return;
282
283 // Only one thread can shrink at a time
284 if (mutex_trylock(&pool->shrink_mutex) != 0) {
285 return; // Another thread is shrinking
286 }
287
288 uint64_t now = time_get_ns();
289 uint64_t cutoff = (now > pool->shrink_delay_ns) ? (now - pool->shrink_delay_ns) : 0;
290
291 // Atomically swap out the entire free list
293
294 // Partition into keep and free lists
295 buffer_node_t *keep_list = NULL;
296 buffer_node_t *free_list = NULL;
297
298 while (list) {
299 buffer_node_t *next = atomic_ptr_load(&list->next);
300 uint64_t returned_at = atomic_load_u64(&list->returned_at_ns);
301
302 if (returned_at < cutoff) {
303 // Old buffer - add to free list
304 atomic_ptr_store(&list->next, (void *)free_list);
305 free_list = list;
306 } else {
307 // Recent buffer - keep it
308 atomic_ptr_store(&list->next, (void *)keep_list);
309 keep_list = list;
310 }
311 list = next;
312 }
313
314 // Push kept buffers back to free list
315 if (keep_list) {
316 // Find tail
317 buffer_node_t *tail = keep_list;
318 void *next_ptr = atomic_ptr_load(&tail->next);
319 while (next_ptr) {
320 tail = (buffer_node_t *)next_ptr;
321 next_ptr = atomic_ptr_load(&tail->next);
322 }
323
324 // Atomically prepend to current free list
325 void *head = atomic_ptr_load(&pool->free_list);
326 while (!atomic_ptr_cas(&pool->free_list, &head, (void *)keep_list)) {
327 atomic_ptr_store(&tail->next, head);
328 }
329 }
330
331 // Free old buffers
332 while (free_list) {
333 buffer_node_t *next = atomic_ptr_load(&free_list->next);
334 size_t total_size = sizeof(buffer_node_t) + free_list->size;
337 SAFE_FREE(free_list);
338 free_list = next;
339 }
340
342}
void * atomic_ptr_exchange(atomic_ptr_t *a, void *new_value)
Atomically exchange a pointer and return the old value.
Definition atomic.c:295
#define mutex_trylock(mutex)
Try to lock a mutex without blocking (with debug tracking in debug builds)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)

References atomic_fetch_add_u64(), atomic_fetch_sub_u64(), atomic_load_u64(), atomic_ptr_cas(), atomic_ptr_exchange(), atomic_ptr_load(), atomic_ptr_store(), current_bytes, free_list, mutex_trylock, mutex_unlock, buffer_node::next, pool, buffer_node::returned_at_ns, SAFE_FREE, shrink_delay_ns, shrink_freed, shrink_mutex, buffer_node::size, and time_get_ns().

Referenced by buffer_pool_free().