ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
debug/memory.c
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
8#include <stdbool.h>
9
10#ifdef DEBUG_MEMORY
11
12#include <stdlib.h>
13#include <string.h>
14#include <errno.h>
15#include <inttypes.h>
16#include <unistd.h>
17#include <time.h>
18
24#include <ascii-chat/common.h>
33#include <ascii-chat/uthash.h>
39#include <ascii-chat/log/log.h>
42
43typedef struct mem_block {
44 void *ptr;
45 size_t size;
46 char file[BUFFER_SIZE_SMALL];
47 int line;
48 uint64_t tid; // Allocating thread ID (for site lookup)
49 char thread_name[256]; // Thread name from named registry
50 bool is_aligned;
51 struct mem_block *next;
52} mem_block_t;
53
54// Allocation site cache - tracks per-site backtraces
55typedef struct alloc_site {
56 char key[BUFFER_SIZE_SMALL + 32]; // "file:line:tid" - uthash key
57 size_t live_count; // Live (unfreed) allocations from this site
58 size_t live_bytes; // Live bytes from this site
59 size_t total_count; // Total ever allocated from this site
60 backtrace_t backtrace; // Captured+symbolized once on first alloc from site
61 char thread_name[256]; // Thread name for this site (cached at first alloc)
62 UT_hash_handle hh;
63} alloc_site_t;
64
65#define MEM_SITE_CACHE_MAX_KEYS 1024
66#define MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY 256
67
68// Non-static for shared library compatibility (still thread-local)
69__thread bool g_in_debug_memory = false;
70
71// Allocation site cache (protected by g_mem.mutex)
72static alloc_site_t *g_site_cache = NULL;
73static size_t g_site_count = 0;
74
92typedef struct {
93 // Configuration (static, in g_suppression_config[])
94 const char *file;
95 int line;
96 int expected_count; // Expected allocations per thread
97 size_t expected_bytes; // Expected total bytes per thread
98 const char *reason; // Why these allocations are expected/harmless
99
100 // Runtime counter (dynamic, in g_suppression_counters[])
101 // Key format: "file:line:tid"
102 char key[BUFFER_SIZE_SMALL + 32];
103 int count;
104 size_t bytes;
105} debug_memory_suppression_t;
106
107// Static configuration of expected suppressions
108static const debug_memory_suppression_t g_suppression_config[] = {
109 {.file = "lib/options/colorscheme.c",
110 .line = 585,
111 .expected_count = 8,
112 .expected_bytes = 47,
113 .reason = "8 16-color ANSI code strings",
114 .key = {0}},
115 {.file = "lib/options/colorscheme.c",
116 .line = 602,
117 .expected_count = 8,
118 .expected_bytes = 88,
119 .reason = "8 256-color ANSI code strings",
120 .key = {0}},
121 {.file = "lib/options/colorscheme.c",
122 .line = 619,
123 .expected_count = 8,
124 .expected_bytes = 144,
125 .reason = "8 truecolor ANSI code strings",
126 .key = {0}},
127 // Intentional singletons - cleaned up at atexit (before memory report)
128 {.file = "lib/util/path.c",
129 .line = 1224,
130 .expected_count = 1,
131 .expected_bytes = 43,
132 .reason = "Path normalization singleton (freed at shutdown)",
133 .key = {0}},
134 {.file = NULL, .line = 0, .expected_count = 0, .expected_bytes = 0, .reason = NULL, .key = {0}} // Sentinel
135};
136
137// Runtime counters tracking per-thread allocations (cleared at report boundaries)
138#define MAX_SUPPRESSION_COUNTERS 64
139static debug_memory_suppression_t g_suppression_counters[MAX_SUPPRESSION_COUNTERS];
140static size_t g_suppression_counter_count = 0;
141
145static void reset_suppression_counters(void) {
146 memset(g_suppression_counters, 0, sizeof(g_suppression_counters));
147 g_suppression_counter_count = 0;
148}
149
156// Helper function to acquire mutex with polling instead of blocking.
157// mutex_lock() can be unsafe during shutdown when signals may arrive.
158// Use trylock polling instead to avoid hanging indefinitely.
159static bool acquire_mutex_with_polling(mutex_t *mutex, int timeout_ms) {
160 int max_retries = (timeout_ms + 9) / 10; // 10ms per retry
161 for (int retry = 0; retry < max_retries; retry++) {
162 int lock_result = mutex_trylock(mutex);
163 if (lock_result == 0) {
164 return true;
165 }
166 if (lock_result != EBUSY) {
167 return false; // Unexpected error
168 }
169 platform_sleep_ms(10); // Sleep 10ms before retry
170 }
171 return false; // Timeout
172}
173
174// Helper: Capture thread name from named registry
175static void capture_thread_name(uint64_t tid, char *thread_name_buf, size_t buf_size) {
176 // Check if this is the main thread (saved during initialization)
178 if (main_tid != 0 && tid == main_tid) {
179 safe_snprintf(thread_name_buf, buf_size, "thread/main");
180 return;
181 }
182
183 // Try to look up thread name from registry using the tid directly
184 const char *name = named_get((uintptr_t)tid);
185 if (name) {
186 // Found a registered name, format it with the type
187 safe_snprintf(thread_name_buf, buf_size, "thread/%s", name);
188 } else {
189 // Not registered - format using NAMED_GET_BY_INT
190 char tid_buf[64];
191 NAMED_GET_BY_INT(tid, tid_buf, sizeof(tid_buf));
192 safe_snprintf(thread_name_buf, buf_size, "thread/%s", tid_buf);
193 }
194}
195
196static bool should_ignore_allocation(const char *file, int line, uint64_t tid, size_t size) {
197 // file is already the normalized relative path from curr->file
198 // (set during debug_malloc/debug_calloc using extract_project_relative_path)
199 // Do NOT call extract_project_relative_path again - it won't work on already-relative paths
200
201 for (size_t i = 0; g_suppression_config[i].file != NULL; i++) {
202 if (line == g_suppression_config[i].line && strcmp(file, g_suppression_config[i].file) == 0) {
203 // Found matching suppression entry - check if we've exceeded expected count for this thread
204 char key[BUFFER_SIZE_SMALL + 32];
205 safe_snprintf(key, sizeof(key), "%s:%d:%lu", file, line, tid);
206
207 // Look up existing counter for this site/thread combination
208 debug_memory_suppression_t *counter = NULL;
209 for (size_t j = 0; j < g_suppression_counter_count; j++) {
210 if (strcmp(g_suppression_counters[j].key, key) == 0) {
211 counter = &g_suppression_counters[j];
212 break;
213 }
214 }
215
216 // Create new counter if needed
217 if (!counter) {
218 if (g_suppression_counter_count < MAX_SUPPRESSION_COUNTERS) {
219 counter = &g_suppression_counters[g_suppression_counter_count];
220 SAFE_STRNCPY(counter->key, key, sizeof(counter->key) - 1);
221 counter->count = 0;
222 counter->bytes = 0;
223 g_suppression_counter_count++;
224 } else {
225 // Counter array full - report as leak to avoid silent failures
226 return false;
227 }
228 }
229
230 // Check if we've exceeded expected count for this thread
231 if (counter->count < g_suppression_config[i].expected_count) {
232 counter->count++;
233 counter->bytes += size;
234 return true; // Ignore this allocation
235 }
236 // Exceeded expected count - report as leak!
237 return false;
238 }
239 }
240 return false;
241}
242
246static alloc_site_t *get_or_create_site(const char *file, int line) {
248 const char *relative_file = extract_project_relative_path(file);
249
250 char key[BUFFER_SIZE_SMALL + 32];
251 safe_snprintf(key, sizeof(key), "%s:%d:%lu", relative_file, line, tid);
252
253 // Try to find existing site
254 alloc_site_t *site = NULL;
255 HASH_FIND_STR(g_site_cache, key, site);
256 if (site) {
257 return site;
258 }
259
260 // Check if we've exceeded cache capacity
261 if (g_site_count >= MEM_SITE_CACHE_MAX_KEYS) {
262 log_warn_every(LOG_RATE_FAST, "Allocation site cache full (%zu keys), not tracking %s:%d (tid: 0x%lx)",
263 MEM_SITE_CACHE_MAX_KEYS, relative_file, line, tid);
264 return NULL;
265 }
266
267 // Create new site entry
268 site = (alloc_site_t *)malloc(sizeof(alloc_site_t));
269 if (!site) {
270 return NULL;
271 }
272
273 memset(site, 0, sizeof(alloc_site_t));
274 SAFE_STRNCPY(site->key, key, sizeof(site->key) - 1);
275
276 // Capture backtrace addresses only (fast)
277 // Symbolization is deferred to exit time (memory report) to avoid 50ms+ per site during startup
278 backtrace_capture(&site->backtrace);
279
280 // Capture thread name at site creation time
281 capture_thread_name(tid, site->thread_name, sizeof(site->thread_name));
282
283 // Add to hash table
284 HASH_ADD_STR(g_site_cache, key, site);
285 g_site_count++;
286
287 return site;
288}
289
293static alloc_site_t *lookup_site(const char *file, int line, uint64_t tid) {
294 const char *relative_file = extract_project_relative_path(file);
295 char key[BUFFER_SIZE_SMALL + 32];
296 safe_snprintf(key, sizeof(key), "%s:%d:%lu", relative_file, line, tid);
297
298 alloc_site_t *site = NULL;
299 HASH_FIND_STR(g_site_cache, key, site);
300 return site;
301}
302
303/* Forward declaration for initialization */
304static atomic_t g_debug_mem_initialized = {0};
305
306static struct {
307 mem_block_t *head;
308 atomic_t total_allocated;
309 atomic_t total_freed;
310 atomic_t current_usage;
311 atomic_t peak_usage;
312 atomic_t malloc_calls;
313 atomic_t free_calls;
314 atomic_t calloc_calls;
315 atomic_t realloc_calls;
318 bool quiet_mode;
319} g_mem = {.head = NULL,
320 .total_allocated = {0},
321 .total_freed = {0},
322 .current_usage = {0},
323 .peak_usage = {0},
324 .malloc_calls = {0},
325 .free_calls = {0},
326 .calloc_calls = {0},
327 .realloc_calls = {0},
328 .lifecycle = LIFECYCLE_INIT,
329 .quiet_mode = false};
330
331#undef malloc
332#undef free
333#undef calloc
334#undef realloc
335
336/* Initialize debug memory system once at startup */
337void debug_memory_ensure_init(void) {
338 if (atomic_load_bool(&g_debug_mem_initialized)) {
339 return;
340 }
341
342 /* Use lifecycle_init_once() for proper thread-safe initialization */
343 if (lifecycle_init_once(&g_mem.lifecycle)) {
344 /* This thread won the race; do the actual initialization */
345 if (mutex_init(&g_mem.mutex, "debug_memory") != 0) {
346 lifecycle_init_abort(&g_mem.lifecycle);
347 return;
348 }
349 atomic_store_bool(&g_debug_mem_initialized, true);
350 lifecycle_init_commit(&g_mem.lifecycle);
351 } else {
352 /* Another thread is initializing or already initialized;
353 * spin briefly to wait for initialization to complete */
354 int attempts = 0;
355 while (!atomic_load_bool(&g_debug_mem_initialized) && attempts < 1000) {
356 attempts++;
357 /* Brief busy-wait */
358 for (volatile int i = 0; i < 10; i++)
359 ;
360 }
361 }
362}
363
364static bool ensure_mutex_initialized(void) {
365 if (atomic_load_bool(&g_debug_mem_initialized)) {
366 return true;
367 }
368
369 return false;
370}
371
372void *debug_malloc(size_t size, const char *file, int line) {
373 void *ptr = (void *)malloc(size);
374 if (!ptr)
375 return NULL;
376
377 if (g_in_debug_memory) {
378 return ptr;
379 }
380
381 g_in_debug_memory = true;
382
383 /* Initialize mutex on first allocation */
384 if (!atomic_load_bool(&g_debug_mem_initialized)) {
386 }
387
388 atomic_fetch_add_u64(&g_mem.malloc_calls, 1);
389 atomic_fetch_add_u64(&g_mem.total_allocated, size);
390 size_t new_usage = atomic_fetch_add_u64(&g_mem.current_usage, size) + size;
391
392 uint64_t peak = atomic_load_u64(&g_mem.peak_usage);
393 while (new_usage > peak) {
394 if (atomic_cas_u64(&g_mem.peak_usage, &peak, new_usage))
395 break;
396 }
397
398 bool have_mutex = atomic_load_bool(&g_debug_mem_initialized);
399 if (have_mutex) {
400 mutex_lock(&g_mem.mutex);
401
402 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
403 if (block) {
404 block->ptr = ptr;
405 block->size = size;
406 block->is_aligned = false;
407 const char *normalized_file = extract_project_relative_path(file);
408 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
409 block->line = line;
410 block->tid = asciichat_thread_current_id();
411 capture_thread_name(block->tid, block->thread_name, sizeof(block->thread_name));
412 block->next = g_mem.head;
413 g_mem.head = block;
414
415 // Update allocation site cache
416 alloc_site_t *site = get_or_create_site(normalized_file, line);
417 if (site) {
418 site->live_count++;
419 site->live_bytes += size;
420 site->total_count++;
421 if (site->live_count == MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY) {
422 // Only warn if this allocation site is not suppressed (expected/benign allocations)
423 if (!should_ignore_allocation(normalized_file, line, asciichat_thread_current_id(), size)) {
424 log_warn("%s:%d:%lu — %d live allocations, possible memory accumulation at site (total: %zu bytes, this "
425 "alloc: %zu bytes)",
426 normalized_file, line, asciichat_thread_current_id(), MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY,
427 site->live_bytes, size);
428 }
429 }
430 }
431 }
432
433 mutex_unlock(&g_mem.mutex);
434 }
435
436 g_in_debug_memory = false;
437 return ptr;
438}
439
440void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
441 if (!ptr)
442 return;
443
444 if (g_in_debug_memory) {
445 return;
446 }
447
448 g_in_debug_memory = true;
449
450 /* Initialize mutex on first allocation */
451 if (!atomic_load_bool(&g_debug_mem_initialized)) {
453 }
454
455 atomic_fetch_add_u64(&g_mem.malloc_calls, 1);
456 atomic_fetch_add_u64(&g_mem.total_allocated, size);
457 size_t new_usage = atomic_fetch_add_u64(&g_mem.current_usage, size) + size;
458
459 uint64_t peak = atomic_load_u64(&g_mem.peak_usage);
460 while (new_usage > peak) {
461 if (atomic_cas_u64(&g_mem.peak_usage, &peak, new_usage))
462 break;
463 }
464
465 bool have_mutex = atomic_load_bool(&g_debug_mem_initialized);
466 if (have_mutex) {
467 mutex_lock(&g_mem.mutex);
468
469 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
470 if (block) {
471 block->ptr = ptr;
472 block->size = size;
473 block->is_aligned = true;
474 const char *normalized_file = extract_project_relative_path(file);
475 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
476 block->line = line;
477 block->tid = asciichat_thread_current_id();
478 capture_thread_name(block->tid, block->thread_name, sizeof(block->thread_name));
479 block->next = g_mem.head;
480 g_mem.head = block;
481
482 // Update allocation site cache
483 alloc_site_t *site = get_or_create_site(normalized_file, line);
484 if (site) {
485 site->live_count++;
486 site->live_bytes += size;
487 site->total_count++;
488 if (site->live_count == MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY) {
489 // Only warn if this allocation site is not suppressed (expected/benign allocations)
490 if (!should_ignore_allocation(normalized_file, line, asciichat_thread_current_id(), size)) {
491 log_warn("%s:%d:%lu — %d live allocations, possible memory accumulation at site (total: %zu bytes, this "
492 "alloc: %zu bytes)",
493 normalized_file, line, asciichat_thread_current_id(), MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY,
494 site->live_bytes, size);
495 }
496 }
497 }
498 }
499
500 mutex_unlock(&g_mem.mutex);
501 }
502
503 g_in_debug_memory = false;
504}
505
506void debug_free(void *ptr, const char *file, int line) {
507 if (!ptr)
508 return;
509
510 if (g_in_debug_memory) {
511 free(ptr);
512 return;
513 }
514
515 g_in_debug_memory = true;
516
517 /* Initialize mutex on first free */
518 if (!atomic_load_bool(&g_debug_mem_initialized)) {
520 }
521
522 atomic_fetch_add_u64(&g_mem.free_calls, 1);
523
524 size_t freed_size = 0;
525 bool found = false;
526#ifdef _WIN32
527 bool was_aligned = false;
528#endif
529
530 bool have_mutex = atomic_load_bool(&g_debug_mem_initialized);
531 if (have_mutex) {
532 mutex_lock(&g_mem.mutex);
533
534 mem_block_t *prev = NULL;
535 mem_block_t *curr = g_mem.head;
536
537 while (curr) {
538 if (curr->ptr == ptr) {
539 if (prev) {
540 prev->next = curr->next;
541 } else {
542 g_mem.head = curr->next;
543 }
544
545 freed_size = curr->size;
546 found = true;
547#ifdef _WIN32
548 was_aligned = curr->is_aligned;
549#endif
550
551 // Update allocation site cache counters
552 alloc_site_t *site = lookup_site(curr->file, curr->line, curr->tid);
553 if (site && site->live_count > 0) {
554 site->live_count--;
555 if (site->live_bytes >= freed_size) {
556 site->live_bytes -= freed_size;
557 } else {
558 site->live_bytes = 0; // Prevent underflow
559 }
560 }
561
562 free(curr);
563 break;
564 }
565 prev = curr;
566 curr = curr->next;
567 }
568
569 if (!found) {
570 log_warn_every(LOG_RATE_FAST, "Freeing untracked pointer %p at %s:%d", ptr, file, line);
571 // Don't print backtrace - log_warn_every already rate-limits the warning
572 }
573
574 mutex_unlock(&g_mem.mutex);
575 } else {
576 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while freeing %p at %s:%d", ptr, file, line);
577 }
578
579 if (found) {
580 atomic_fetch_add_u64(&g_mem.total_freed, freed_size);
581 atomic_fetch_sub_u64(&g_mem.current_usage, freed_size);
582 }
583
584#ifdef _WIN32
585 if (was_aligned) {
586 _aligned_free(ptr);
587 } else {
588 free(ptr);
589 }
590#else
591 free(ptr);
592#endif
593
594 g_in_debug_memory = false;
595}
596
597void *debug_calloc(size_t count, size_t size, const char *file, int line) {
598 size_t total = count * size;
599 void *ptr = calloc(count, size);
600 if (!ptr)
601 return NULL;
602
603 if (g_in_debug_memory) {
604 return ptr;
605 }
606
607 g_in_debug_memory = true;
608
609 /* Initialize mutex on first calloc */
610 if (!atomic_load_bool(&g_debug_mem_initialized)) {
612 }
613
614 atomic_fetch_add_u64(&g_mem.calloc_calls, 1);
615 atomic_fetch_add_u64(&g_mem.total_allocated, total);
616 size_t new_usage = atomic_fetch_add_u64(&g_mem.current_usage, total) + total;
617
618 uint64_t peak = atomic_load_u64(&g_mem.peak_usage);
619 while (new_usage > peak) {
620 if (atomic_cas_u64(&g_mem.peak_usage, &peak, new_usage))
621 break;
622 }
623
624 bool have_mutex = atomic_load_bool(&g_debug_mem_initialized);
625 if (have_mutex) {
626 mutex_lock(&g_mem.mutex);
627
628 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
629 if (block) {
630 block->ptr = ptr;
631 block->size = total;
632 block->is_aligned = false;
633 const char *normalized_file = extract_project_relative_path(file);
634 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
635 block->line = line;
636 block->tid = asciichat_thread_current_id();
637 capture_thread_name(block->tid, block->thread_name, sizeof(block->thread_name));
638 block->next = g_mem.head;
639 g_mem.head = block;
640
641 // Update allocation site cache
642 alloc_site_t *site = get_or_create_site(normalized_file, line);
643 if (site) {
644 site->live_count++;
645 site->live_bytes += total;
646 site->total_count++;
647 if (site->live_count == MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY) {
648 // Only warn if this allocation site is not suppressed (expected/benign allocations)
649 if (!should_ignore_allocation(normalized_file, line, asciichat_thread_current_id(), size)) {
650 log_warn("%s:%d:%lu — %d live allocations, possible memory accumulation at site (total: %zu bytes, this "
651 "alloc: %zu bytes)",
652 normalized_file, line, asciichat_thread_current_id(), MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY,
653 site->live_bytes, total);
654 }
655 }
656 }
657 }
658
659 mutex_unlock(&g_mem.mutex);
660 }
661
662 g_in_debug_memory = false;
663 return ptr;
664}
665
710void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
711 // Prevent recursion if we're already in debug memory logic
712 if (g_in_debug_memory) {
713 return realloc(ptr, size);
714 }
715
716 g_in_debug_memory = true;
717
718 // Track number of realloc calls
719 atomic_fetch_add_u64(&g_mem.realloc_calls, 1);
720
721 // If ptr == NULL, realloc behaves like malloc
722 if (ptr == NULL) {
723 g_in_debug_memory = false;
724 return debug_malloc(size, file, line);
725 }
726 // If size == 0, realloc behaves like free
727 if (size == 0) {
728 g_in_debug_memory = false;
729 debug_free(ptr, file, line);
730 return NULL;
731 }
732
733 /* Initialize mutex on first realloc */
734 if (!atomic_load_bool(&g_debug_mem_initialized)) {
736 }
737
738 // Look up old allocation size from tracking list
739 size_t old_size = 0;
740 bool have_mutex = atomic_load_bool(&g_debug_mem_initialized);
741
742 if (have_mutex) {
743 mutex_lock(&g_mem.mutex);
744
745 // Find the existing allocation block in the linked list
746 mem_block_t *curr = g_mem.head;
747 while (curr && curr->ptr != ptr) {
748 curr = curr->next;
749 }
750 if (curr) {
751 old_size = curr->size;
752 }
753
754 mutex_unlock(&g_mem.mutex);
755 } else {
756 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while reallocating %p at %s:%d", ptr, file, line);
757 }
758
759 // Perform actual reallocation
760 void *new_ptr = SAFE_REALLOC(ptr, size, void *);
761 if (!new_ptr) {
762 g_in_debug_memory = false;
763 return NULL;
764 }
765
766 // Update memory statistics based on size change
767 if (old_size > 0) {
768 // Block was tracked - update statistics
769 if (size >= old_size) {
770 // Growing: track additional allocation
771 size_t delta = size - old_size;
772 atomic_fetch_add_u64(&g_mem.total_allocated, delta);
773 size_t new_usage = atomic_fetch_add_u64(&g_mem.current_usage, delta) + delta;
774
775 // Update peak usage if new usage exceeds previous peak
776 uint64_t peak = atomic_load_u64(&g_mem.peak_usage);
777 while (new_usage > peak) {
778 if (atomic_cas_u64(&g_mem.peak_usage, &peak, new_usage))
779 break;
780 }
781 } else {
782 // Shrinking: track freed memory
783 size_t delta = old_size - size;
784 atomic_fetch_add_u64(&g_mem.total_freed, delta);
785 atomic_fetch_sub_u64(&g_mem.current_usage, delta);
786 }
787 } else {
788 // Block was not tracked - treat as new allocation
789 atomic_fetch_add_u64(&g_mem.total_allocated, size);
790 size_t new_usage = atomic_fetch_add_u64(&g_mem.current_usage, size) + size;
791
792 // Update peak usage
793 uint64_t peak = atomic_load_u64(&g_mem.peak_usage);
794 while (new_usage > peak) {
795 if (atomic_cas_u64(&g_mem.peak_usage, &peak, new_usage))
796 break;
797 }
798 }
799
800 // Update tracking list with new pointer and metadata
801 if (ensure_mutex_initialized()) {
802 mutex_lock(&g_mem.mutex);
803
804 // Find existing block in linked list
805 mem_block_t *curr = g_mem.head;
806 while (curr && curr->ptr != ptr) {
807 curr = curr->next;
808 }
809
810 if (curr) {
811 // Update site cache: decrement old site, increment new site
812 alloc_site_t *old_site = lookup_site(curr->file, curr->line, curr->tid);
813 if (old_site && old_site->live_count > 0) {
814 old_site->live_count--;
815 old_site->live_bytes -= curr->size;
816 }
817
818 // Update block with new metadata
819 curr->ptr = new_ptr;
820 const char *normalized_file = extract_project_relative_path(file);
821 SAFE_STRNCPY(curr->file, normalized_file, sizeof(curr->file) - 1);
822 curr->file[sizeof(curr->file) - 1] = '\0';
823 curr->line = line;
824 curr->tid = asciichat_thread_current_id();
825 curr->size = size;
826 curr->is_aligned = false;
827
828 // Add to new site
829 alloc_site_t *new_site = get_or_create_site(normalized_file, line);
830 if (new_site) {
831 new_site->live_count++;
832 new_site->live_bytes += size;
833 new_site->total_count++;
834 if (new_site->live_count == MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY) {
835 log_warn("%s:%d:%lu — %d live allocations, possible memory accumulation", normalized_file, line,
836 asciichat_thread_current_id(), MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY);
837 }
838 }
839 } else {
840 // Block not found - create new tracking entry
841 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
842 if (block) {
843 block->ptr = new_ptr;
844 block->size = size;
845 block->is_aligned = false;
846 const char *normalized_file = extract_project_relative_path(file);
847 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
848 block->line = line;
849 block->tid = asciichat_thread_current_id();
850 block->next = g_mem.head;
851 g_mem.head = block;
852
853 // Add to site cache
854 alloc_site_t *site = get_or_create_site(normalized_file, line);
855 if (site) {
856 site->live_count++;
857 site->live_bytes += size;
858 site->total_count++;
859 if (site->live_count == MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY) {
860 log_warn("%s:%d:%lu — %d live allocations, possible memory accumulation", normalized_file, line,
861 asciichat_thread_current_id(), MEM_SITE_CACHE_MAX_ALLOCS_PER_KEY);
862 }
863 }
864 }
865 }
866
867 mutex_unlock(&g_mem.mutex);
868 } else {
869 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while updating realloc block %p -> %p at %s:%d", ptr,
870 new_ptr, file, line);
871 }
872
873 g_in_debug_memory = false;
874 return new_ptr;
875}
876
877void debug_memory_set_quiet_mode(bool quiet) {
878 g_mem.quiet_mode = quiet;
879}
880
881void debug_memory_report(void) {
882 // Check for usage error BEFORE cleanup clears it
884
885 // Always clean up errno, even if we're not printing
887
888 // Skip memory report if an action flag was passed (for clean action output)
889 // unless explicitly forced via ASCII_CHAT_MEMORY_DEBUG environment variable
890 if (has_action_flag() && !SAFE_GETENV("ASCII_CHAT_MEMORY_DEBUG")) {
891 return;
892 }
893
894 // Skip memory report on command-line usage errors for clean error output
895 if (error == ERROR_USAGE) {
896 return;
897 }
898
899 bool quiet = g_mem.quiet_mode;
900
901 // Reset suppression counters at start of report
902 reset_suppression_counters();
903
904 if (!quiet) {
905// Build report in buffer, then output once to stderr and log file
906#define REPORT_BUFFER_SIZE (256 * 1024) // 256KB for full memory report
907 char *report_buffer = malloc(REPORT_BUFFER_SIZE);
908 if (!report_buffer) {
909 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "Failed to allocate memory for report buffer\n"));
910 return;
911 }
912 size_t report_len = 0;
913
914#define APPEND_REPORT(fmt, ...) \
915 do { \
916 size_t remaining = REPORT_BUFFER_SIZE - report_len; \
917 if (remaining > 1) { \
918 int written = safe_snprintf(report_buffer + report_len, remaining, fmt, ##__VA_ARGS__); \
919 if (written > 0) \
920 report_len += written; \
921 } \
922 } while (0)
923
924 APPEND_REPORT("=== Memory Report ===\n");
925
926 size_t total_allocated = atomic_load_u64(&g_mem.total_allocated);
927 size_t total_freed = atomic_load_u64(&g_mem.total_freed);
928 size_t current_usage = atomic_load_u64(&g_mem.current_usage);
929 size_t peak_usage = atomic_load_u64(&g_mem.peak_usage);
930 size_t malloc_calls = atomic_load_u64(&g_mem.malloc_calls);
931 size_t calloc_calls = atomic_load_u64(&g_mem.calloc_calls);
932 size_t free_calls = atomic_load_u64(&g_mem.free_calls);
933
934 // Calculate total size and count of suppressed allocations
935 size_t suppressed_bytes = 0;
936 size_t suppressed_count = 0;
937 if (g_mem.head) {
938 if (ensure_mutex_initialized()) {
939 // Use polling instead of blocking mutex_lock to avoid hangs during shutdown.
940 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
941 goto skip_memory_iter;
942 }
943
944 // Now we have the lock - iterate memory blocks
945 mem_block_t *curr = g_mem.head;
946 while (curr) {
947 if (should_ignore_allocation(curr->file, curr->line, curr->tid, curr->size)) {
948 suppressed_bytes += curr->size;
949 suppressed_count++;
950 }
951 curr = curr->next;
952 }
953 mutex_unlock(&g_mem.mutex);
954 }
955 }
956
957 skip_memory_iter:
958
959 // Adjust current usage to exclude suppressed allocations
960 size_t adjusted_current_usage = (current_usage >= suppressed_bytes) ? (current_usage - suppressed_bytes) : 0;
961
962 // Adjust total allocated for display to exclude suppressions (so it matches total freed)
963 size_t adjusted_total_allocated =
964 (total_allocated >= suppressed_bytes) ? (total_allocated - suppressed_bytes) : total_allocated;
965
966 // Reset suppression counters after counting suppressed bytes
967 reset_suppression_counters();
968
969 // Count actual unfreed allocations early so we can use it to filter suppression warnings
970 size_t unfreed_count = 0;
971 if (g_mem.head) {
972 if (ensure_mutex_initialized()) {
973 if (acquire_mutex_with_polling(&g_mem.mutex, 100)) {
974 mem_block_t *curr = g_mem.head;
975 while (curr) {
976 if (!should_ignore_allocation(curr->file, curr->line, curr->tid, curr->size)) {
977 unfreed_count++;
978 }
979 curr = curr->next;
980 }
981 mutex_unlock(&g_mem.mutex);
982 }
983 }
984 }
985
986 // Only warn about suppression mismatches if there are outstanding allocations
987 if (unfreed_count > 0) {
988 for (size_t i = 0; g_suppression_config[i].file != NULL; i++) {
989 // Sum counts and bytes from all threads for this file:line combination
990 int total_count = 0;
991 size_t total_bytes = 0;
992 for (size_t j = 0; j < g_suppression_counter_count; j++) {
993 // Parse counter key "file:line:tid" to extract file and line
994 char *last_colon = strrchr(g_suppression_counters[j].key, ':');
995 if (last_colon) {
996 char *second_colon = last_colon - 1;
997 while (second_colon > g_suppression_counters[j].key && *second_colon != ':') {
998 second_colon--;
999 }
1000 if (second_colon > g_suppression_counters[j].key) {
1001 int line_from_key = 0;
1002 SAFE_SSCANF(second_colon + 1, "%d", &line_from_key);
1003 size_t file_len = second_colon - g_suppression_counters[j].key;
1004 if (file_len == strlen(g_suppression_config[i].file) && line_from_key == g_suppression_config[i].line &&
1005 strncmp(g_suppression_counters[j].key, g_suppression_config[i].file, file_len) == 0) {
1006 total_count += g_suppression_counters[j].count;
1007 total_bytes += g_suppression_counters[j].bytes;
1008 }
1009 }
1010 }
1011 }
1012
1013 // A suppression entry describes live allocations that are expected to
1014 // remain at report time. If shutdown already freed every allocation at
1015 // the site, there is nothing to suppress and no mismatch to report.
1016 if (total_count > 0 && (total_count != g_suppression_config[i].expected_count ||
1017 total_bytes != g_suppression_config[i].expected_bytes)) {
1018 char mismatch_details[256] = {0};
1019 if (total_count != g_suppression_config[i].expected_count) {
1020 snprintf(mismatch_details, sizeof(mismatch_details), "Count mismatch: expected %d, found %d",
1021 g_suppression_config[i].expected_count, total_count);
1022 }
1023 if (total_bytes != g_suppression_config[i].expected_bytes) {
1024 size_t len = strlen(mismatch_details);
1025 if (len > 0) {
1026 snprintf(mismatch_details + len, sizeof(mismatch_details) - len,
1027 " Bytes mismatch: expected %zu, found %zu", g_suppression_config[i].expected_bytes, total_bytes);
1028 } else {
1029 snprintf(mismatch_details, sizeof(mismatch_details), "Bytes mismatch: expected %zu, found %zu",
1030 g_suppression_config[i].expected_bytes, total_bytes);
1031 }
1032 }
1033 APPEND_REPORT("%s %s:%d %s\n", colored_string(LOG_COLOR_ERROR, "WARNING: Suppression mismatch detected"),
1034 g_suppression_config[i].file, g_suppression_config[i].line, mismatch_details);
1035 }
1036 }
1037 }
1038
1039 char pretty_total[64];
1040 char pretty_freed[64];
1041 char pretty_current[64];
1042 char pretty_peak[64];
1043 format_bytes_pretty(adjusted_total_allocated, pretty_total, sizeof(pretty_total));
1044 format_bytes_pretty(total_freed, pretty_freed, sizeof(pretty_freed));
1045 format_bytes_pretty(adjusted_current_usage, pretty_current, sizeof(pretty_current));
1046 format_bytes_pretty(peak_usage, pretty_peak, sizeof(pretty_peak));
1047
1048 // Calculate max label width for column alignment
1049 const char *label_total = "Total allocated:";
1050 const char *label_freed = "Total freed:";
1051 const char *label_current = "Current usage:";
1052 const char *label_peak = "Peak usage:";
1053 const char *label_malloc = "malloc calls:";
1054 const char *label_calloc = "calloc calls:";
1055 const char *label_free = "free calls:";
1056 const char *label_suppressions = "suppressions:";
1057 const char *label_diff = "unfreed allocations:";
1058
1059 size_t max_label_width = 0;
1060 max_label_width = MAX(max_label_width, strlen(label_total));
1061 max_label_width = MAX(max_label_width, strlen(label_freed));
1062 max_label_width = MAX(max_label_width, strlen(label_current));
1063 max_label_width = MAX(max_label_width, strlen(label_peak));
1064 max_label_width = MAX(max_label_width, strlen(label_malloc));
1065 max_label_width = MAX(max_label_width, strlen(label_calloc));
1066 max_label_width = MAX(max_label_width, strlen(label_free));
1067 max_label_width = MAX(max_label_width, strlen(label_suppressions));
1068 max_label_width = MAX(max_label_width, strlen(label_diff));
1069
1070#define APPEND_MEM_LINE(label, value_str) \
1071 do { \
1072 APPEND_REPORT("%s", colored_string(LOG_COLOR_GREY, label)); \
1073 for (size_t i = strlen(label); i < max_label_width; i++) { \
1074 APPEND_REPORT(" "); \
1075 } \
1076 APPEND_REPORT(" %s\n", value_str); \
1077 } while (0)
1078
1079#define APPEND_MEM_LINE_COLORED(label, value_str, color) \
1080 do { \
1081 APPEND_REPORT("%s", colored_string(LOG_COLOR_GREY, label)); \
1082 for (size_t i = strlen(label); i < max_label_width; i++) { \
1083 APPEND_REPORT(" "); \
1084 } \
1085 APPEND_REPORT(" %s\n", colored_string(color, value_str)); \
1086 } while (0)
1087
1088 // Colorize total allocated/freed: green if they match, red if they don't (memory leak)
1089 // We've already adjusted allocated to exclude suppressions, so now compare directly
1090 log_color_t alloc_freed_color = (adjusted_total_allocated == total_freed) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
1091 APPEND_MEM_LINE_COLORED(label_total, pretty_total, alloc_freed_color);
1092 APPEND_MEM_LINE_COLORED(label_freed, pretty_freed, alloc_freed_color);
1093
1094 // Colorize current usage: green if 0 (no leaks), red if any unfreed memory (leak detected)
1095 log_color_t current_color = (adjusted_current_usage == 0) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
1096 APPEND_MEM_LINE_COLORED(label_current, pretty_current, current_color);
1097
1098 // Colorize peak usage: green if 0-50 MB, yellow if 50-80 MB, red if above 80 MB
1099 log_color_t peak_color = LOG_COLOR_INFO; // Default green
1100 if (peak_usage >= (80 * 1024 * 1024)) {
1101 peak_color = LOG_COLOR_ERROR; // Red if >= 80 MB
1102 } else if (peak_usage >= (50 * 1024 * 1024)) {
1103 peak_color = LOG_COLOR_WARN; // Yellow if >= 50 MB
1104 }
1105 APPEND_MEM_LINE_COLORED(label_peak, pretty_peak, peak_color);
1106
1107 // unfreed_count already calculated earlier for suppression warning filtering
1108
1109 // Colorize malloc/calloc/free calls: green if unfreed == 0, red if unfreed != 0
1110 log_color_t calls_color = (unfreed_count == 0) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
1111 char malloc_str[32];
1112 safe_snprintf(malloc_str, sizeof(malloc_str), "%zu", malloc_calls);
1113 APPEND_MEM_LINE_COLORED(label_malloc, malloc_str, calls_color);
1114
1115 char calloc_str[32];
1116 safe_snprintf(calloc_str, sizeof(calloc_str), "%zu", calloc_calls);
1117 APPEND_MEM_LINE_COLORED(label_calloc, calloc_str, calls_color);
1118
1119 char free_str[32];
1120 safe_snprintf(free_str, sizeof(free_str), "%zu", free_calls);
1121 APPEND_MEM_LINE_COLORED(label_free, free_str, calls_color);
1122
1123 // Colorize suppressions: green if working properly, red if >= 1MB or >= 100 allocations
1124 if (suppressed_count > 0) {
1125 char pretty_suppressed[64];
1126 format_bytes_pretty(suppressed_bytes, pretty_suppressed, sizeof(pretty_suppressed));
1127 char suppressions_str[128];
1128 safe_snprintf(suppressions_str, sizeof(suppressions_str), "%zu (%s)", suppressed_count, pretty_suppressed);
1129
1130 log_color_t suppressions_color = LOG_COLOR_INFO; // Green by default (working properly)
1131 if (suppressed_bytes >= (1024 * 1024) || suppressed_count >= 100) {
1132 suppressions_color = LOG_COLOR_ERROR; // Red if >= 1MB or >= 100 allocations
1133 }
1134 APPEND_MEM_LINE_COLORED(label_suppressions, suppressions_str, suppressions_color);
1135 }
1136
1137 char diff_str[32];
1138 safe_snprintf(diff_str, sizeof(diff_str), "%zu", unfreed_count);
1139 APPEND_REPORT("%s", colored_string(LOG_COLOR_GREY, label_diff));
1140 for (size_t i = strlen(label_diff); i < max_label_width; i++) {
1141 APPEND_REPORT(" ");
1142 }
1143 APPEND_REPORT(" %s\n", colored_string(unfreed_count == 0 ? LOG_COLOR_INFO : LOG_COLOR_ERROR, diff_str));
1144
1145#undef APPEND_MEM_LINE
1146#undef APPEND_MEM_LINE_COLORED
1147
1148 // Only show "Current allocations:" section if there are actual leaks
1149 if (unfreed_count > 0) {
1150 // Reset counters before printing pass (after counting pass used them)
1151 reset_suppression_counters();
1152
1153 // Print "Current allocations:" header (count already shown in "unfreed allocations" above)
1154 APPEND_REPORT("\n%s\n", colored_string(LOG_COLOR_DEV, "Current allocations:"));
1155 }
1156
1157 if (g_site_cache && unfreed_count > 0) {
1158 if (ensure_mutex_initialized()) {
1159 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
1160 goto skip_allocations_list;
1161 }
1162
1163 // Iterate site cache with timeout (max 500ms to avoid hanging during shutdown)
1164 uint64_t iteration_start_ns = time_get_ns();
1165 const uint64_t max_iteration_ns = 500000000; // 500ms timeout
1166 alloc_site_t *site, *tmp;
1167 HASH_ITER(hh, g_site_cache, site, tmp) {
1168 // Check timeout every iteration
1169 if (time_elapsed_ns(iteration_start_ns, time_get_ns()) > max_iteration_ns) {
1170 APPEND_REPORT(" (printing truncated - timeout)\n");
1171 break;
1172 }
1173 // Skip sites with no live allocations
1174 if (site->live_count == 0) {
1175 continue;
1176 }
1177
1178 // Parse site key to extract file and line for ignore checking
1179 // Key format: "file:line:tid" (where file is the relative path)
1180 char file[BUFFER_SIZE_SMALL];
1181 memset(file, 0, sizeof(file));
1182 int line = 0;
1183 uint64_t tid = 0;
1184 bool parse_success = false;
1185
1186 // Strategy: Find both colons by scanning for the rightmost two ':'
1187 // 1. Find the LAST ':' (between line and tid)
1188 // 2. Find the SECOND-LAST ':' (between file and line)
1189 // 3. Extract each component
1190
1191 // Count colons to verify we have exactly 2
1192 int colon_count = 0;
1193 for (const char *p = site->key; *p; p++) {
1194 if (*p == ':')
1195 colon_count++;
1196 }
1197
1198 if (colon_count == 2) {
1199 // Find the two colons from right to left
1200 char *last_colon = strrchr(site->key, ':'); // Last colon
1201 char *first_colon = strchr(site->key, ':'); // First colon
1202
1203 if (last_colon && first_colon && first_colon < last_colon) {
1204 // Extract components: file is from start to first_colon
1205 size_t file_len = first_colon - site->key;
1206
1207 if (file_len > 0 && file_len < sizeof(file)) {
1208 SAFE_MEMCPY(file, sizeof(file), site->key, file_len);
1209 file[file_len] = '\0';
1210
1211 // Extract line (between first and last colon)
1212 if (SAFE_SSCANF(first_colon + 1, "%d", &line) == 1 &&
1213 SAFE_SSCANF(last_colon + 1, "%" SCNu64, &tid) == 1) {
1214 parse_success = true;
1215 }
1216 } else if (file_len >= sizeof(file)) {
1217 // Filename too long, truncate with ellipsis
1218 size_t max_len = sizeof(file) - 4;
1219 SAFE_MEMCPY(file, sizeof(file), site->key, max_len);
1220 file[max_len] = '\0';
1221 int snprintf_result = SAFE_SNPRINTF(file + max_len, sizeof(file) - max_len, "%s", "...");
1222 if (snprintf_result < 0) {
1223 log_error("Failed to append ellipsis to filename: snprintf returned %d", snprintf_result);
1224 }
1225
1226 if (SAFE_SSCANF(first_colon + 1, "%d", &line) == 1 &&
1227 SAFE_SSCANF(last_colon + 1, "%" SCNu64, &tid) == 1) {
1228 parse_success = true;
1229 }
1230 }
1231 }
1232 }
1233
1234 // If parsing failed, use a descriptive fallback
1235 if (!parse_success) {
1236 safe_snprintf(file, sizeof(file), "<key:%s>", site->key);
1237 }
1238
1239 // Skip ignored allocations
1240 if (should_ignore_allocation(file, line, tid, site->live_bytes)) {
1241 continue;
1242 }
1243
1244 char pretty_bytes[64];
1245 format_bytes_pretty(site->live_bytes, pretty_bytes, sizeof(pretty_bytes));
1246
1247 char line_str[32];
1248 safe_snprintf(line_str, sizeof(line_str), "%d", line);
1249 char count_str[32];
1250 safe_snprintf(count_str, sizeof(count_str), "%zu", site->live_count);
1251
1252 // Determine color based on byte size (1 MB and over=red, KB=yellow, B=light blue)
1253 log_color_t size_color = LOG_COLOR_DEBUG;
1254 if (strstr(pretty_bytes, "MB") || strstr(pretty_bytes, "GB") || strstr(pretty_bytes, "TB") ||
1255 strstr(pretty_bytes, "PB") || strstr(pretty_bytes, "EB")) {
1256 size_color = LOG_COLOR_ERROR;
1257 } else if (strstr(pretty_bytes, "KB")) {
1258 size_color = LOG_COLOR_WARN;
1259 }
1260
1261 // Colorize thread name separately to avoid rotating buffer conflicts
1262 const char *colored_thread_name = colorize_named_string(site->thread_name);
1263
1264 // Print site summary with thread name (captured at site creation, colorized)
1265 APPEND_REPORT(" - %s:%s [%s] %s live %s total\n", colored_string(LOG_COLOR_GREY, file),
1266 colored_string(LOG_COLOR_FATAL, line_str), colored_thread_name,
1267 colored_string(size_color, count_str), colored_string(size_color, pretty_bytes));
1268
1269 // Don't synchronously symbolize backtraces during memory report - symbolization is slow
1270 // and can cause hangs during shutdown. Instead, backtraces will be symbolized asynchronously
1271 // by the debug sync thread. Just print whatever symbols are available.
1272 if (site->backtrace.symbols != NULL && site->backtrace.count > 0) {
1273 APPEND_REPORT(" Backtrace (%d frames):\n", site->backtrace.count);
1274 for (int i = 0; i < site->backtrace.count; i++) {
1275 APPEND_REPORT(" [%d] %s\n", i, site->backtrace.symbols[i]);
1276 }
1277 }
1278 }
1279
1280 mutex_unlock(&g_mem.mutex);
1281 } else {
1282 APPEND_REPORT("\n%s\n",
1284 "Current allocations unavailable: failed to initialize debug memory mutex"));
1285 }
1286 }
1287
1288 skip_allocations_list:
1289
1290 // SKIP cleanup site cache at shutdown
1291 // We used to clean up the hash table here, but HASH_DEL inside HASH_ITER causes
1292 // undefined behavior (modifying hash table during iteration = infinite loop/hang).
1293 // Since we're exiting anyway, skip this cleanup - the OS will reclaim memory.
1294
1295 // Write to stderr (colored output)
1296 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "%s", report_buffer));
1297
1298 // Write to log file (for SIGUSR2 debugging persistence) - strip ANSI codes
1299 char *stripped = ansi_strip_escapes(report_buffer, report_len);
1300 if (stripped) {
1301 log_file_msg("%s", stripped);
1302 free(stripped);
1303 }
1304
1305 free(report_buffer);
1306#undef REPORT_BUFFER_SIZE
1307#undef APPEND_REPORT
1308 }
1309}
1310
1311// ============================================================================
1312// Memory Report Debug Thread (triggered via SIGUSR2)
1313// ============================================================================
1314
1317
1318typedef struct {
1319 volatile bool should_run;
1320 volatile bool should_exit;
1321 volatile bool signal_triggered; // Flag set by SIGUSR2 handler
1322 mutex_t mutex; // Protects access to flags
1323 cond_t cond; // Wakes thread when signal arrives
1324 bool initialized; // Tracks if mutex/cond are initialized
1325} debug_memory_request_t;
1326
1327static debug_memory_request_t g_debug_memory_request = {false, false, false, {0}, {0}, false};
1328static asciichat_thread_t g_debug_memory_thread;
1329
1336static void *debug_memory_thread_fn(void *arg) {
1337 (void)arg;
1338
1339 while (!g_debug_memory_request.should_exit) {
1340 mutex_lock(&g_debug_memory_request.mutex);
1341
1342 if ((g_debug_memory_request.should_run || g_debug_memory_request.signal_triggered) &&
1343 !g_debug_memory_request.should_exit) {
1344 mutex_unlock(&g_debug_memory_request.mutex);
1346 mutex_lock(&g_debug_memory_request.mutex);
1347 g_debug_memory_request.should_run = false;
1348 g_debug_memory_request.signal_triggered = false;
1349 }
1350
1351 // Wait for work or signal, with 100ms timeout to check should_exit
1352 if (!g_debug_memory_request.should_exit) {
1353 cond_timedwait(&g_debug_memory_request.cond, &g_debug_memory_request.mutex, 100000000); // 100ms
1354 }
1355 mutex_unlock(&g_debug_memory_request.mutex);
1356 }
1357 return NULL;
1358}
1359
1363int debug_memory_thread_init(void) {
1364 return 0;
1365}
1366
1370int debug_memory_thread_start(void) {
1371 if (!g_debug_memory_request.initialized) {
1372 mutex_init(&g_debug_memory_request.mutex, "debug_memory_state");
1373 cond_init(&g_debug_memory_request.cond, "debug_memory_signal");
1374 g_debug_memory_request.initialized = true;
1375 }
1376
1377 g_debug_memory_request.should_exit = false;
1378 int err = asciichat_thread_create(&g_debug_memory_thread, "debug_memory", debug_memory_thread_fn, NULL);
1379 return err;
1380}
1381
1385void debug_memory_trigger_report(void) {
1386 g_debug_memory_request.signal_triggered = true;
1387 cond_signal(&g_debug_memory_request.cond);
1388}
1389
1393void debug_memory_thread_cleanup(void) {
1394 // Only join if thread was actually created
1395 if (!g_debug_memory_request.initialized) {
1396 return;
1397 }
1398
1399 g_debug_memory_request.initialized = false; // Prevent double-join
1400
1401 // Set exit flag and signal thread - don't acquire mutex to avoid deadlock
1402 // The thread reads should_exit without locking, and cond_signal is safe without mutex
1403 g_debug_memory_request.should_exit = true;
1404 cond_signal(&g_debug_memory_request.cond);
1405
1406 // Use timeout join to prevent indefinite hang if debug thread is stuck
1407 // Matches pattern used in debug_sync_cleanup_thread() which has 1 second timeout
1408 int join_result = asciichat_thread_join_timeout(&g_debug_memory_thread, NULL, 1000000000ULL); // 1 second timeout
1409 if (join_result != 0) {
1410 // Thread didn't exit cleanly, but we still need to proceed with shutdown
1411 // Log a warning if logging is still available
1412 }
1413}
1414
1416 // Allocation backtraces are symbolized when the memory report is rendered.
1417}
1418
1419#else
1420
1421// ============================================================================
1422// Release Build Stubs (for when DEBUG_MEMORY is not defined or NDEBUG is set)
1423// ============================================================================
1424
1425#include <stdlib.h>
1426
1428 // No-op in release builds
1429}
1430
1432 (void)quiet;
1433 // No-op in release builds
1434}
1435
1437 // No-op in release builds
1438}
1439
1441 return 0;
1442}
1443
1445 return 0;
1446}
1447
1449 // No-op in release builds
1450}
1451
1453 // No-op in release builds
1454}
1455
1456void *debug_malloc(size_t size, const char *file, int line) {
1457 (void)file;
1458 (void)line;
1459 return malloc(size);
1460}
1461
1462void *debug_calloc(size_t count, size_t size, const char *file, int line) {
1463 (void)file;
1464 (void)line;
1465 return calloc(count, size);
1466}
1467
1468void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
1469 (void)file;
1470 (void)line;
1471 return realloc(ptr, size);
1472}
1473
1474void debug_free(void *ptr, const char *file, int line) {
1475 (void)file;
1476 (void)line;
1477 free(ptr);
1478}
1479
1480void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
1481 (void)ptr;
1482 (void)size;
1483 (void)file;
1484 (void)line;
1485}
1486
1488
1490
1491#endif
ANSI escape sequence utilities and fast color code generation.
lifecycle_t lifecycle
mutex_t mutex
⚠️‼️ 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_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
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
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
Common buffer size definitions.
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Cross-platform condition variable interface for ascii-chat.
void backtrace_capture(backtrace_t *bt)
Capture current stack into bt.
📍 Backtrace capture, symbolization, and formatting
void debug_memory_thread_cleanup(void)
void * debug_malloc(size_t size, const char *file, int line)
void debug_sync_symbolize_allocations(void)
void debug_memory_trigger_report(void)
void * debug_realloc(void *ptr, size_t size, const char *file, int line)
void debug_memory_report(void)
void debug_track_aligned(void *ptr, size_t size, const char *file, int line)
void * debug_calloc(size_t count, size_t size, const char *file, int line)
int debug_memory_thread_init(void)
void debug_memory_set_quiet_mode(bool quiet)
int debug_memory_thread_start(void)
void debug_free(void *ptr, const char *file, int line)
bool g_in_debug_memory
🔍 Memory debugging helpers for tracking allocations in debug builds
Named object registry for debugging — log identifiable resource names.
Cross-platform error handling utilities.
Error and exit codes - unified status values (0-255)
char * ansi_strip_escapes(const char *input, size_t input_len)
Strip all ANSI escape sequences from a string.
Definition ansi.c:23
#define BUFFER_SIZE_SMALL
Small buffer size (256 bytes)
#define SAFE_REALLOC(ptr, size, cast)
Definition common.h:284
#define SAFE_SSCANF(str, format,...)
Definition common.h:440
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_GETENV(name)
Definition common.h:434
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
unsigned long long uint64_t
Definition common.h:59
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468
#define NAMED_GET_BY_INT(id, buffer, size)
Get name of an integer ID or format as hex fallback.
const char * named_get(uintptr_t key)
Look up the registered name for a resource.
uint64_t debug_sync_get_main_thread_id(void)
Get the main thread ID for memory reporting.
Definition sync.c:649
void debug_memory_ensure_init(void)
Init the lifecycle of the debug memory module.
#define GET_ERRNO()
Get current error code (0 if no error)
void asciichat_errno_destroy(void)
Cleanup error system resources.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_USAGE
Definition error_codes.h:53
#define LOG_RATE_FAST
Log rate limit: 1 second (1,000,000 microseconds)
Definition log_rates.h:26
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
log_color_t
Color enum for logging - indexes into color arrays.
Definition log/log.h:130
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
const char * colorize_named_string(const char *name_str)
Colorize a "type/name (0xaddress)" string for display in memory reports.
Definition log/named.c:972
void log_file_msg(const char *fmt,...)
Log to file only, no stderr output.
Definition log/log.c:1467
@ LOG_COLOR_GREY
Definition log/log.h:137
@ LOG_COLOR_FATAL
Definition log/log.h:136
@ LOG_COLOR_ERROR
Definition log/log.h:135
@ LOG_COLOR_DEV
Definition log/log.h:131
@ LOG_COLOR_DEBUG
Definition log/log.h:132
@ LOG_COLOR_INFO
Definition log/log.h:133
@ LOG_COLOR_WARN
Definition log/log.h:134
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
uint64_t time_elapsed_ns(uint64_t start_ns, uint64_t end_ns)
Calculate elapsed time with wraparound safety.
Definition util/time.c:150
bool has_action_flag(void)
Check if an action flag was detected.
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:172
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
#define mutex_trylock(mutex)
Try to lock a mutex without blocking (with debug tracking in debug builds)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
#define mutex_lock(mutex)
Lock a mutex (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
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
#define SAFE_IGNORE_PRINTF_RESULT(expr)
Definition system.h:525
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
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)
#define cond_timedwait(cond, mutex, timeout_ns)
Wait on a condition variable with timeout (with debug tracking in debug builds)
Definition cond.h:300
uint64_t asciichat_thread_current_id(void)
Get the current thread's unique numeric ID.
Definition threading.c:92
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
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
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:456
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🧵 Cross-platform thread interface for ascii-chat
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t
void lifecycle_init_abort(lifecycle_t *lc)
Definition lifecycle.c:99
void lifecycle_init_commit(lifecycle_t *lc)
Definition lifecycle.c:90
bool lifecycle_init_once(lifecycle_t *lc)
Definition lifecycle.c:52
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
📝 Logging API with multiple log levels and terminal output control
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
Format named objects in log messages (replaces hex addresses with type/name descriptions)
bool should_exit(void)
Definition misc.c:131
bool initialized
Definition mmap.c:38
#define MAX(a, b)
Definition param.h:10
📂 Path Manipulation Utilities
Cross-platform memory allocation utilities.
Cross-platform mutex interface for ascii-chat.
ClangTool/LibTooling compatibility shim for stdbool.h.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63
Mutex type (POSIX: pthread_mutex_t with debug tracking)
🔒 Synchronization primitive debugging (mutexes, rwlocks, condition variables)
Cross-platform system functions interface for ascii-chat.
🖥️ Cross-platform terminal interface for ascii-chat
⏱️ High-precision timing utilities using sokol_time.h and uthash
📊 String Formatting Utilities
🔤 String Manipulation and Shell Escaping Utilities