ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
debug/memory.c
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
8#if defined(DEBUG_MEMORY) && !defined(NDEBUG)
9
10#include <stdatomic.h>
11#include <stdlib.h>
12#include <string.h>
13
14#include "debug/memory.h"
15#include "common.h"
16#include "asciichat_errno.h"
17#include "platform/mutex.h"
18#include "platform/system.h"
19#include "platform/memory.h"
20#include "util/format.h"
21#include "util/path.h"
22
23typedef struct mem_block {
24 void *ptr;
25 size_t size;
26 char file[256];
27 int line;
28 bool is_aligned;
29 struct mem_block *next;
30} mem_block_t;
31
32static __thread bool g_in_debug_memory = false;
33
34static struct {
35 mem_block_t *head;
36 atomic_size_t total_allocated;
37 atomic_size_t total_freed;
38 atomic_size_t current_usage;
39 atomic_size_t peak_usage;
40 atomic_size_t malloc_calls;
41 atomic_size_t free_calls;
42 atomic_size_t calloc_calls;
43 atomic_size_t realloc_calls;
44 mutex_t mutex;
45 atomic_int mutex_state;
46 bool quiet_mode;
47} g_mem = {.head = NULL,
48 .total_allocated = 0,
49 .total_freed = 0,
50 .current_usage = 0,
51 .peak_usage = 0,
52 .malloc_calls = 0,
53 .free_calls = 0,
54 .calloc_calls = 0,
55 .realloc_calls = 0,
56 .mutex_state = 0,
57 .quiet_mode = false};
58
59#undef malloc
60#undef free
61#undef calloc
62#undef realloc
63
64static atomic_flag g_logged_mutex_init_failure = ATOMIC_FLAG_INIT;
65
66static bool ensure_mutex_initialized(void) {
67 for (;;) {
68 int state = atomic_load_explicit(&g_mem.mutex_state, memory_order_acquire);
69 if (state == 2) {
70 return true;
71 }
72
73 if (state == 0) {
74 int expected = 0;
75 if (atomic_compare_exchange_strong_explicit(&g_mem.mutex_state, &expected, 1, memory_order_acq_rel,
76 memory_order_acquire)) {
77 if (mutex_init(&g_mem.mutex) == 0) {
78 atomic_store_explicit(&g_mem.mutex_state, 2, memory_order_release);
79 return true;
80 }
81
82 atomic_store_explicit(&g_mem.mutex_state, 0, memory_order_release);
83 if (!atomic_flag_test_and_set(&g_logged_mutex_init_failure)) {
84 log_error("Failed to initialize debug memory mutex; memory tracking will run without locking");
85 }
86 return false;
87 }
88 continue;
89 }
90
92 }
93}
94
95void *debug_malloc(size_t size, const char *file, int line) {
96 void *ptr = (void *)malloc(size);
97 if (!ptr)
98 return NULL;
99
100 if (g_in_debug_memory) {
101 return ptr;
102 }
103
104 g_in_debug_memory = true;
105
106 atomic_fetch_add(&g_mem.malloc_calls, 1);
107 atomic_fetch_add(&g_mem.total_allocated, size);
108 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
109
110 size_t peak = atomic_load(&g_mem.peak_usage);
111 while (new_usage > peak) {
112 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
113 break;
114 }
115
116 bool have_mutex = ensure_mutex_initialized();
117 if (have_mutex) {
118 mutex_lock(&g_mem.mutex);
119
120 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
121 if (block) {
122 block->ptr = ptr;
123 block->size = size;
124 block->is_aligned = false;
125 const char *normalized_file = extract_project_relative_path(file);
126 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
127 block->line = line;
128 block->next = g_mem.head;
129 g_mem.head = block;
130 }
131
132 mutex_unlock(&g_mem.mutex);
133 }
134
135 g_in_debug_memory = false;
136 return ptr;
137}
138
139void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
140 if (!ptr)
141 return;
142
143 if (g_in_debug_memory) {
144 return;
145 }
146
147 g_in_debug_memory = true;
148
149 atomic_fetch_add(&g_mem.malloc_calls, 1);
150 atomic_fetch_add(&g_mem.total_allocated, size);
151 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
152
153 size_t peak = atomic_load(&g_mem.peak_usage);
154 while (new_usage > peak) {
155 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
156 break;
157 }
158
159 bool have_mutex = ensure_mutex_initialized();
160 if (have_mutex) {
161 mutex_lock(&g_mem.mutex);
162
163 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
164 if (block) {
165 block->ptr = ptr;
166 block->size = size;
167 block->is_aligned = true;
168 const char *normalized_file = extract_project_relative_path(file);
169 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
170 block->line = line;
171 block->next = g_mem.head;
172 g_mem.head = block;
173 }
174
175 mutex_unlock(&g_mem.mutex);
176 }
177
178 g_in_debug_memory = false;
179}
180
181void debug_free(void *ptr, const char *file, int line) {
182 if (!ptr)
183 return;
184
185 if (g_in_debug_memory) {
186 free(ptr);
187 return;
188 }
189
190 g_in_debug_memory = true;
191
192 atomic_fetch_add(&g_mem.free_calls, 1);
193
194 size_t freed_size = 0;
195 bool found = false;
196#ifdef _WIN32
197 bool was_aligned = false;
198#endif
199
200 bool have_mutex = ensure_mutex_initialized();
201 if (have_mutex) {
202 mutex_lock(&g_mem.mutex);
203
204 mem_block_t *prev = NULL;
205 mem_block_t *curr = g_mem.head;
206
207 while (curr) {
208 if (curr->ptr == ptr) {
209 if (prev) {
210 prev->next = curr->next;
211 } else {
212 g_mem.head = curr->next;
213 }
214
215 freed_size = curr->size;
216 found = true;
217#ifdef _WIN32
218 was_aligned = curr->is_aligned;
219#endif
220 free(curr);
221 break;
222 }
223 prev = curr;
224 curr = curr->next;
225 }
226
227 if (!found) {
228 log_warn_every(LOG_RATE_FAST, "Freeing untracked pointer %p at %s:%d", ptr, file, line);
230 }
231
232 mutex_unlock(&g_mem.mutex);
233 } else {
234 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while freeing %p at %s:%d", ptr, file, line);
235 }
236
237 if (found) {
238 atomic_fetch_add(&g_mem.total_freed, freed_size);
239 atomic_fetch_sub(&g_mem.current_usage, freed_size);
240 } else {
241 size_t real_size = platform_malloc_size(ptr);
242
243 if (real_size > 0) {
244 atomic_fetch_add(&g_mem.total_freed, real_size);
245 atomic_fetch_sub(&g_mem.current_usage, real_size);
246 }
247 }
248
249#ifdef _WIN32
250 if (was_aligned) {
251 _aligned_free(ptr);
252 } else {
253 free(ptr);
254 }
255#else
256 free(ptr);
257#endif
258
259 g_in_debug_memory = false;
260}
261
262void *debug_calloc(size_t count, size_t size, const char *file, int line) {
263 size_t total = count * size;
264 void *ptr = calloc(count, size);
265 if (!ptr)
266 return NULL;
267
268 if (g_in_debug_memory) {
269 return ptr;
270 }
271
272 g_in_debug_memory = true;
273
274 atomic_fetch_add(&g_mem.calloc_calls, 1);
275 atomic_fetch_add(&g_mem.total_allocated, total);
276 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, total) + total;
277
278 size_t peak = atomic_load(&g_mem.peak_usage);
279 while (new_usage > peak) {
280 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
281 break;
282 }
283
284 bool have_mutex = ensure_mutex_initialized();
285 if (have_mutex) {
286 mutex_lock(&g_mem.mutex);
287
288 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
289 if (block) {
290 block->ptr = ptr;
291 block->size = total;
292 block->is_aligned = false;
293 SAFE_STRNCPY(block->file, file, sizeof(block->file) - 1);
294 block->line = line;
295 block->next = g_mem.head;
296 g_mem.head = block;
297 }
298
299 mutex_unlock(&g_mem.mutex);
300 }
301
302 g_in_debug_memory = false;
303 return ptr;
304}
305
350void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
351 // Prevent recursion if we're already in debug memory logic
352 if (g_in_debug_memory) {
353 return realloc(ptr, size);
354 }
355
356 g_in_debug_memory = true;
357
358 // Track number of realloc calls
359 atomic_fetch_add(&g_mem.realloc_calls, 1);
360
361 // If ptr == NULL, realloc behaves like malloc
362 if (ptr == NULL) {
363 g_in_debug_memory = false;
364 return debug_malloc(size, file, line);
365 }
366 // If size == 0, realloc behaves like free
367 if (size == 0) {
368 g_in_debug_memory = false;
369 debug_free(ptr, file, line);
370 return NULL;
371 }
372
373 // Look up old allocation size from tracking list
374 size_t old_size = 0;
375 bool have_mutex = ensure_mutex_initialized();
376
377 if (have_mutex) {
378 mutex_lock(&g_mem.mutex);
379
380 // Find the existing allocation block in the linked list
381 mem_block_t *curr = g_mem.head;
382 while (curr && curr->ptr != ptr) {
383 curr = curr->next;
384 }
385 if (curr) {
386 old_size = curr->size;
387 }
388
389 mutex_unlock(&g_mem.mutex);
390 } else {
391 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while reallocating %p at %s:%d", ptr, file, line);
392 }
393
394 // Perform actual reallocation
395 void *new_ptr = SAFE_REALLOC(ptr, size, void *);
396 if (!new_ptr) {
397 g_in_debug_memory = false;
398 return NULL;
399 }
400
401 // Update memory statistics based on size change
402 if (old_size > 0) {
403 // Block was tracked - update statistics
404 if (size >= old_size) {
405 // Growing: track additional allocation
406 size_t delta = size - old_size;
407 atomic_fetch_add(&g_mem.total_allocated, delta);
408 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, delta) + delta;
409
410 // Update peak usage if new usage exceeds previous peak
411 size_t peak = atomic_load(&g_mem.peak_usage);
412 while (new_usage > peak) {
413 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
414 break;
415 }
416 } else {
417 // Shrinking: track freed memory
418 size_t delta = old_size - size;
419 atomic_fetch_add(&g_mem.total_freed, delta);
420 atomic_fetch_sub(&g_mem.current_usage, delta);
421 }
422 } else {
423 // Block was not tracked - treat as new allocation
424 atomic_fetch_add(&g_mem.total_allocated, size);
425 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
426
427 // Update peak usage
428 size_t peak = atomic_load(&g_mem.peak_usage);
429 while (new_usage > peak) {
430 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
431 break;
432 }
433 }
434
435 // Update tracking list with new pointer and metadata
436 if (ensure_mutex_initialized()) {
437 mutex_lock(&g_mem.mutex);
438
439 // Find existing block in linked list
440 mem_block_t *curr = g_mem.head;
441 while (curr && curr->ptr != ptr) {
442 curr = curr->next;
443 }
444
445 if (curr) {
446 // Update existing block with new metadata
447 curr->ptr = new_ptr;
448 curr->size = size;
449 curr->is_aligned = false;
450 SAFE_STRNCPY(curr->file, file, sizeof(curr->file) - 1);
451 curr->file[sizeof(curr->file) - 1] = '\0';
452 curr->line = line;
453 } else {
454 // Block not found - create new tracking entry
455 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
456 if (block) {
457 block->ptr = new_ptr;
458 block->size = size;
459 block->is_aligned = false;
460 SAFE_STRNCPY(block->file, file, sizeof(block->file) - 1);
461 block->line = line;
462 block->next = g_mem.head;
463 g_mem.head = block;
464 }
465 }
466
467 mutex_unlock(&g_mem.mutex);
468 } else {
469 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while updating realloc block %p -> %p at %s:%d", ptr,
470 new_ptr, file, line);
471 }
472
473 g_in_debug_memory = false;
474 return new_ptr;
475}
476
477void debug_memory_set_quiet_mode(bool quiet) {
478 g_mem.quiet_mode = quiet;
479}
480
481static const char *strip_project_path(const char *full_path) {
482 return extract_project_relative_path(full_path);
483}
484
485void debug_memory_report(void) {
487
488 bool quiet = g_mem.quiet_mode;
489 if (!quiet) {
490 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "\n=== Memory Report ===\n"));
491
492 size_t total_allocated = atomic_load(&g_mem.total_allocated);
493 size_t total_freed = atomic_load(&g_mem.total_freed);
494 size_t current_usage = atomic_load(&g_mem.current_usage);
495 size_t peak_usage = atomic_load(&g_mem.peak_usage);
496 size_t malloc_calls = atomic_load(&g_mem.malloc_calls);
497 size_t calloc_calls = atomic_load(&g_mem.calloc_calls);
498 size_t free_calls = atomic_load(&g_mem.free_calls);
499
500 char pretty_total[64];
501 char pretty_freed[64];
502 char pretty_current[64];
503 char pretty_peak[64];
504 format_bytes_pretty(total_allocated, pretty_total, sizeof(pretty_total));
505 format_bytes_pretty(total_freed, pretty_freed, sizeof(pretty_freed));
506 format_bytes_pretty(current_usage, pretty_current, sizeof(pretty_current));
507 format_bytes_pretty(peak_usage, pretty_peak, sizeof(pretty_peak));
508
509 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "Total allocated: %s\n", pretty_total));
510 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "Total freed: %s\n", pretty_freed));
511 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "Current usage: %s\n", pretty_current));
512 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "Peak usage: %s\n", pretty_peak));
513 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "malloc calls: %zu\n", malloc_calls));
514 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "calloc calls: %zu\n", calloc_calls));
515 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "free calls: %zu\n", free_calls));
516 size_t diff = (malloc_calls + calloc_calls) - free_calls;
517 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "(malloc calls + calloc calls) - free calls = %zu\n", diff));
518
519 if (g_mem.head) {
520 if (ensure_mutex_initialized()) {
521 mutex_lock(&g_mem.mutex);
522
523 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "\nCurrent allocations:\n"));
524 mem_block_t *curr = g_mem.head;
525 while (curr) {
526 char pretty_size[64];
527 format_bytes_pretty(curr->size, pretty_size, sizeof(pretty_size));
529 safe_fprintf(stderr, " - %s:%d - %s\n", strip_project_path(curr->file), curr->line, pretty_size));
530 curr = curr->next;
531 }
532
533 mutex_unlock(&g_mem.mutex);
534 } else {
536 safe_fprintf(stderr, "\nCurrent allocations unavailable: failed to initialize debug memory mutex\n"));
537 }
538 }
539 }
540}
541
542#elif defined(DEBUG_MEMORY)
543
544void debug_memory_set_quiet_mode(bool quiet) {
545 (void)quiet;
546}
547
548void debug_memory_report(void) {}
549
550void *debug_malloc(size_t size, const char *file, int line) {
551 (void)file;
552 (void)line;
553 return malloc(size);
554}
555
556void *debug_calloc(size_t count, size_t size, const char *file, int line) {
557 (void)file;
558 (void)line;
559 return calloc(count, size);
560}
561
562void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
563 (void)file;
564 (void)line;
565 return realloc(ptr, size);
566}
567
568void debug_free(void *ptr, const char *file, int line) {
569 (void)file;
570 (void)line;
571 free(ptr);
572}
573
574void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
575 (void)ptr;
576 (void)size;
577 (void)file;
578 (void)line;
579}
580
581#endif
⚠️‼️ Error and/or exit() when things go bad.
🔍 Memory debugging helpers for tracking allocations in debug builds
📊 String Formatting Utilities
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:228
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
void asciichat_errno_cleanup(void)
Cleanup error system resources.
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define log_error(...)
Log an ERROR message.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
int safe_fprintf(FILE *stream, const char *format,...)
Safe version of fprintf.
int mutex_init(mutex_t *mutex)
Initialize a mutex.
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
Definition mutex.h:140
pthread_mutex_t mutex_t
Mutex type (POSIX: pthread_mutex_t)
Definition mutex.h:38
#define SAFE_IGNORE_PRINTF_RESULT(expr)
Definition system.h:427
void platform_print_backtrace(int skip_frames)
Print a backtrace of the current call stack.
size_t platform_malloc_size(const void *ptr)
Get the size of an allocated memory block.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
Definition mutex.h:175
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Format byte count into human-readable string.
Definition format.c:10
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:127
Cross-platform mutex interface for ascii-chat.
📂 Path Manipulation Utilities
Cross-platform memory management utilities.
Cross-platform system functions interface for ascii-chat.