ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
buffer_pool.c
Go to the documentation of this file.
1
7#include <ascii-chat/atomic.h>
9#include <ascii-chat/common.h>
18#include <stdlib.h>
19#include <string.h>
20
21/* ============================================================================
22 * Internal Helpers
23 * ============================================================================
24 */
25
27static inline buffer_node_t *node_from_data(void *data) {
28 return (buffer_node_t *)((char *)data - sizeof(buffer_node_t));
29}
30
32static inline void *data_from_node(buffer_node_t *node) {
33 return (void *)((char *)node + sizeof(buffer_node_t));
34}
35
37static inline void update_peak(atomic_t *peak, uint64_t value) {
38 uint64_t old = atomic_load_u64(peak);
39 while (value > old) {
40 if (atomic_cas_u64(peak, &old, value)) {
41 break;
42 }
43 }
44}
45
46/* ============================================================================
47 * Buffer Pool Implementation
48 * ============================================================================
49 */
50
51buffer_pool_t *buffer_pool_create(size_t max_bytes, uint64_t shrink_delay_ns) {
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}
103
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}
121
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}
223
224void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size) {
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}
278
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}
343
344void buffer_pool_get_stats(buffer_pool_t *pool, size_t *current_bytes, size_t *used_bytes, size_t *free_bytes) {
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}
365
366void buffer_pool_log_stats(buffer_pool_t *pool, const char *name) {
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}
400
401/* ============================================================================
402 * Global Buffer Pool
403 * ============================================================================
404 */
405
406static struct {
410} g_global_pool_state = {.pool = NULL, .lifecycle = LIFECYCLE_INIT};
411
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}
420
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}
430
432 return g_global_pool_state.pool;
433}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
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_exchange(atomic_ptr_t *a, void *new_value)
Atomically exchange a pointer and return the old value.
Definition atomic.c:295
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
⚛️ Atomic operations abstraction layer with debug tracking
lifecycle_t lifecycle
buffer_pool_t * pool
mutex_t mutex
🗃️ Lock-Free Unified Memory Buffer Pool with Lazy Allocation
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
buffer_pool_t * buffer_pool_get_global(void)
#define BUFFER_POOL_MAX_BYTES
Maximum total bytes the pool can hold (337 MB)
Definition buffer_pool.h:41
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_destroy(buffer_pool_t *pool)
Destroy a buffer pool and free all memory.
void buffer_pool_cleanup_global(void)
#define BUFFER_POOL_MAX_SINGLE_SIZE
Maximum single buffer size to pool (larger allocations use malloc directly)
Definition buffer_pool.h:50
void buffer_pool_log_stats(buffer_pool_t *pool, const char *name)
Log pool statistics.
#define BUFFER_POOL_SHRINK_DELAY_NS
Time before unused buffers are freed (5 seconds in nanoseconds)
Definition buffer_pool.h:44
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_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
void buffer_pool_init_global(void)
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
struct buffer_node buffer_node_t
Node header embedded before user data.
#define SAFE_MALLOC_ALIGNED(size, alignment, cast)
Definition common.h:349
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
#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_UNREGISTER(ptr)
Unregister a pointer.
#define NAMED_REGISTER_MUTEX(mutex, name, parent_ptr)
Register a mutex with automatic format specifier.
#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
@ ERROR_THREAD
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
#define mutex_trylock(mutex)
Try to lock a mutex without blocking (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
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
Platform initialization and static synchronization helpers.
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
Magic number validation constants and macros.
#define IS_MAGIC_VALID(magic, expected)
Check if magic number is valid (true if matches expected)
Definition magic.h:36
#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 value wrapper for integral/boolean types.
Definition atomic.h:76
Node header embedded before user data.
Definition buffer_pool.h:71
atomic_t returned_at_ns
Timestamp when returned to free list (nanoseconds)
Definition buffer_pool.h:76
atomic_ptr_t next
Next in free list (lock-free)
Definition buffer_pool.h:75
uint32_t _pad
Padding for alignment.
Definition buffer_pool.h:73
uint32_t magic
Magic to identify pooled buffers.
Definition buffer_pool.h:72
size_t size
Size of user data portion.
Definition buffer_pool.h:74
struct buffer_pool * pool
Owning pool (for free)
Definition buffer_pool.h:77
Unified buffer pool with lock-free fast path.
Definition buffer_pool.h:85
size_t max_bytes
Maximum total bytes allowed.
Definition buffer_pool.h:89
atomic_t malloc_fallbacks
Allocations that bypassed pool.
uint64_t shrink_delay_ns
Time before unused buffers freed (nanoseconds)
Definition buffer_pool.h:90
atomic_t hits
Allocations from free list.
Definition buffer_pool.h:98
mutex_t shrink_mutex
Only used for shrinking.
Definition buffer_pool.h:87
atomic_ptr_t free_list
Lock-free stack of available buffers (void* cast from buffer_node_t*)
Definition buffer_pool.h:86
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 shrink_freed
Buffers freed by shrink.
atomic_t returns
Buffers returned to free list.
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
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Cross-platform system functions interface for ascii-chat.
⏱️ High-precision timing utilities using sokol_time.h and uthash
📊 String Formatting Utilities