ascii-chat 0.8.38
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 <ascii-chat/debug/memory.h>
15#include <ascii-chat/common.h>
16#include <ascii-chat/common/buffer_sizes.h>
17#include <ascii-chat/common/error_codes.h>
18#include <ascii-chat/asciichat_errno.h>
19#include <ascii-chat/platform/mutex.h>
20#include <ascii-chat/platform/system.h>
21#include <ascii-chat/platform/memory.h>
22#include <ascii-chat/platform/terminal.h>
23#include <ascii-chat/util/format.h>
24#include <ascii-chat/util/path.h>
25#include <ascii-chat/util/string.h>
26#include <ascii-chat/util/time.h>
27#include <ascii-chat/log/logging.h>
28#include <ascii-chat/options/options.h>
29
30typedef struct mem_block {
31 void *ptr;
32 size_t size;
33 char file[BUFFER_SIZE_SMALL];
34 int line;
35 bool is_aligned;
36 void *backtrace_ptrs[16]; // Store up to 16 return addresses
37 int backtrace_count; // Number of frames captured
38 struct mem_block *next;
39} mem_block_t;
40
41// Non-static for shared library compatibility (still thread-local)
42__thread bool g_in_debug_memory = false;
43
55typedef struct {
56 const char *file;
57 int line;
58 int expected_count; // Exact number of allocations expected from this location
59} ignore_entry_t;
60
61static const ignore_entry_t g_ignore_list[] = {
62 {"lib/options/colorscheme.c", 579, 8}, // 8 16-color ANSI strings (cleaned after report)
63 {"lib/options/colorscheme.c", 596, 8}, // 8 256-color ANSI strings (cleaned after report)
64 {"lib/options/colorscheme.c", 613, 8}, // 8 truecolor ANSI strings (cleaned after report)
65 {"lib/util/path.c", 1203, 1}, // Normalized path allocation (caller responsibility to free)
66 {NULL, 0, 0} // Sentinel
67};
68
69// Track seen count for each ignore entry (reset at start of memory report)
70static int g_ignore_counts[32] = {0};
71
75static void reset_ignore_counters(void) {
76 memset(g_ignore_counts, 0, sizeof(g_ignore_counts));
77}
78
85// Helper function to acquire mutex with polling instead of blocking.
86// mutex_lock() can be unsafe during shutdown when signals may arrive.
87// Use trylock polling instead to avoid hanging indefinitely.
88static bool acquire_mutex_with_polling(mutex_t *mutex, int timeout_ms) {
89 int max_retries = (timeout_ms + 9) / 10; // 10ms per retry
90 for (int retry = 0; retry < max_retries; retry++) {
91 int lock_result = mutex_trylock(mutex);
92 if (lock_result == 0) {
93 return true;
94 }
95 if (lock_result != EBUSY) {
96 return false; // Unexpected error
97 }
98 platform_sleep_ms(10); // Sleep 10ms before retry
99 }
100 return false; // Timeout
101}
102
103static bool should_ignore_allocation(const char *file, int line) {
104 // file is already the normalized relative path from curr->file
105 // (set during debug_malloc/debug_calloc using extract_project_relative_path)
106 // Do NOT call extract_project_relative_path again - it won't work on already-relative paths
107
108 for (size_t i = 0; g_ignore_list[i].file != NULL; i++) {
109 if (line == g_ignore_list[i].line && strcmp(file, g_ignore_list[i].file) == 0) {
110 // Found matching ignore entry - check if we've exceeded expected count
111 if (g_ignore_counts[i] < g_ignore_list[i].expected_count) {
112 g_ignore_counts[i]++;
113 return true; // Ignore this allocation
114 }
115 // Exceeded expected count - report as leak!
116 return false;
117 }
118 }
119 return false;
120}
121
122static struct {
123 mem_block_t *head;
124 atomic_size_t total_allocated;
125 atomic_size_t total_freed;
126 atomic_size_t current_usage;
127 atomic_size_t peak_usage;
128 atomic_size_t malloc_calls;
129 atomic_size_t free_calls;
130 atomic_size_t calloc_calls;
131 atomic_size_t realloc_calls;
132 mutex_t mutex;
133 atomic_int mutex_state;
134 bool quiet_mode;
135} g_mem = {.head = NULL,
136 .total_allocated = 0,
137 .total_freed = 0,
138 .current_usage = 0,
139 .peak_usage = 0,
140 .malloc_calls = 0,
141 .free_calls = 0,
142 .calloc_calls = 0,
143 .realloc_calls = 0,
144 .mutex_state = 0,
145 .quiet_mode = false};
146
147#undef malloc
148#undef free
149#undef calloc
150#undef realloc
151
152static atomic_flag g_logged_mutex_init_failure = ATOMIC_FLAG_INIT;
153
154static bool ensure_mutex_initialized(void) {
155 for (;;) {
156 int state = atomic_load_explicit(&g_mem.mutex_state, memory_order_acquire);
157 if (state == 2) {
158 return true;
159 }
160
161 if (state == 0) {
162 int expected = 0;
163 if (atomic_compare_exchange_strong_explicit(&g_mem.mutex_state, &expected, 1, memory_order_acq_rel,
164 memory_order_acquire)) {
165 if (mutex_init(&g_mem.mutex) == 0) {
166 atomic_store_explicit(&g_mem.mutex_state, 2, memory_order_release);
167 return true;
168 }
169
170 atomic_store_explicit(&g_mem.mutex_state, 0, memory_order_release);
171 if (!atomic_flag_test_and_set(&g_logged_mutex_init_failure)) {
172 log_error("Failed to initialize debug memory mutex; memory tracking will run without locking");
173 }
174 return false;
175 }
176 continue;
177 }
178
180 }
181}
182
183void *debug_malloc(size_t size, const char *file, int line) {
184 void *ptr = (void *)malloc(size);
185 if (!ptr)
186 return NULL;
187
188 if (g_in_debug_memory) {
189 return ptr;
190 }
191
192 g_in_debug_memory = true;
193
194 atomic_fetch_add(&g_mem.malloc_calls, 1);
195 atomic_fetch_add(&g_mem.total_allocated, size);
196 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
197
198 size_t peak = atomic_load(&g_mem.peak_usage);
199 while (new_usage > peak) {
200 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
201 break;
202 }
203
204 bool have_mutex = ensure_mutex_initialized();
205 if (have_mutex) {
206 mutex_lock(&g_mem.mutex);
207
208 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
209 if (block) {
210 block->ptr = ptr;
211 block->size = size;
212 block->is_aligned = false;
213 const char *normalized_file = extract_project_relative_path(file);
214 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
215 block->line = line;
216 // Capture backtrace (skip 1 frame for this function)
217 block->backtrace_count = platform_backtrace(block->backtrace_ptrs, 16);
218 // Ensure valid backtrace count
219 if (block->backtrace_count < 0) {
220 block->backtrace_count = 0;
221 }
222 block->next = g_mem.head;
223 g_mem.head = block;
224 }
225
226 mutex_unlock(&g_mem.mutex);
227 }
228
229 g_in_debug_memory = false;
230 return ptr;
231}
232
233void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
234 if (!ptr)
235 return;
236
237 if (g_in_debug_memory) {
238 return;
239 }
240
241 g_in_debug_memory = true;
242
243 atomic_fetch_add(&g_mem.malloc_calls, 1);
244 atomic_fetch_add(&g_mem.total_allocated, size);
245 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
246
247 size_t peak = atomic_load(&g_mem.peak_usage);
248 while (new_usage > peak) {
249 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
250 break;
251 }
252
253 bool have_mutex = ensure_mutex_initialized();
254 if (have_mutex) {
255 mutex_lock(&g_mem.mutex);
256
257 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
258 if (block) {
259 block->ptr = ptr;
260 block->size = size;
261 block->is_aligned = true;
262 const char *normalized_file = extract_project_relative_path(file);
263 SAFE_STRNCPY(block->file, normalized_file, sizeof(block->file) - 1);
264 block->line = line;
265 // Capture backtrace (skip 1 frame for this function)
266 block->backtrace_count = platform_backtrace(block->backtrace_ptrs, 16);
267 // Ensure valid backtrace count
268 if (block->backtrace_count < 0) {
269 block->backtrace_count = 0;
270 }
271 block->next = g_mem.head;
272 g_mem.head = block;
273 }
274
275 mutex_unlock(&g_mem.mutex);
276 }
277
278 g_in_debug_memory = false;
279}
280
281void debug_free(void *ptr, const char *file, int line) {
282 if (!ptr)
283 return;
284
285 if (g_in_debug_memory) {
286 free(ptr);
287 return;
288 }
289
290 g_in_debug_memory = true;
291
292 atomic_fetch_add(&g_mem.free_calls, 1);
293
294 size_t freed_size = 0;
295 bool found = false;
296#ifdef _WIN32
297 bool was_aligned = false;
298#endif
299
300 bool have_mutex = ensure_mutex_initialized();
301 if (have_mutex) {
302 mutex_lock(&g_mem.mutex);
303
304 mem_block_t *prev = NULL;
305 mem_block_t *curr = g_mem.head;
306
307 while (curr) {
308 if (curr->ptr == ptr) {
309 if (prev) {
310 prev->next = curr->next;
311 } else {
312 g_mem.head = curr->next;
313 }
314
315 freed_size = curr->size;
316 found = true;
317#ifdef _WIN32
318 was_aligned = curr->is_aligned;
319#endif
320 free(curr);
321 break;
322 }
323 prev = curr;
324 curr = curr->next;
325 }
326
327 if (!found) {
328 log_warn_every(LOG_RATE_FAST, "Freeing untracked pointer %p at %s:%d", ptr, file, line);
329 // Don't print backtrace - log_warn_every already rate-limits the warning
330 }
331
332 mutex_unlock(&g_mem.mutex);
333 } else {
334 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while freeing %p at %s:%d", ptr, file, line);
335 }
336
337 if (found) {
338 atomic_fetch_add(&g_mem.total_freed, freed_size);
339 atomic_fetch_sub(&g_mem.current_usage, freed_size);
340 }
341
342#ifdef _WIN32
343 if (was_aligned) {
344 _aligned_free(ptr);
345 } else {
346 free(ptr);
347 }
348#else
349 free(ptr);
350#endif
351
352 g_in_debug_memory = false;
353}
354
355void *debug_calloc(size_t count, size_t size, const char *file, int line) {
356 size_t total = count * size;
357 void *ptr = calloc(count, size);
358 if (!ptr)
359 return NULL;
360
361 if (g_in_debug_memory) {
362 return ptr;
363 }
364
365 g_in_debug_memory = true;
366
367 atomic_fetch_add(&g_mem.calloc_calls, 1);
368 atomic_fetch_add(&g_mem.total_allocated, total);
369 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, total) + total;
370
371 size_t peak = atomic_load(&g_mem.peak_usage);
372 while (new_usage > peak) {
373 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
374 break;
375 }
376
377 bool have_mutex = ensure_mutex_initialized();
378 if (have_mutex) {
379 mutex_lock(&g_mem.mutex);
380
381 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
382 if (block) {
383 block->ptr = ptr;
384 block->size = total;
385 block->is_aligned = false;
386 SAFE_STRNCPY(block->file, file, sizeof(block->file) - 1);
387 block->line = line;
388 block->next = g_mem.head;
389 g_mem.head = block;
390 }
391
392 mutex_unlock(&g_mem.mutex);
393 }
394
395 g_in_debug_memory = false;
396 return ptr;
397}
398
443void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
444 // Prevent recursion if we're already in debug memory logic
445 if (g_in_debug_memory) {
446 return realloc(ptr, size);
447 }
448
449 g_in_debug_memory = true;
450
451 // Track number of realloc calls
452 atomic_fetch_add(&g_mem.realloc_calls, 1);
453
454 // If ptr == NULL, realloc behaves like malloc
455 if (ptr == NULL) {
456 g_in_debug_memory = false;
457 return debug_malloc(size, file, line);
458 }
459 // If size == 0, realloc behaves like free
460 if (size == 0) {
461 g_in_debug_memory = false;
462 debug_free(ptr, file, line);
463 return NULL;
464 }
465
466 // Look up old allocation size from tracking list
467 size_t old_size = 0;
468 bool have_mutex = ensure_mutex_initialized();
469
470 if (have_mutex) {
471 mutex_lock(&g_mem.mutex);
472
473 // Find the existing allocation block in the linked list
474 mem_block_t *curr = g_mem.head;
475 while (curr && curr->ptr != ptr) {
476 curr = curr->next;
477 }
478 if (curr) {
479 old_size = curr->size;
480 }
481
482 mutex_unlock(&g_mem.mutex);
483 } else {
484 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while reallocating %p at %s:%d", ptr, file, line);
485 }
486
487 // Perform actual reallocation
488 void *new_ptr = SAFE_REALLOC(ptr, size, void *);
489 if (!new_ptr) {
490 g_in_debug_memory = false;
491 return NULL;
492 }
493
494 // Update memory statistics based on size change
495 if (old_size > 0) {
496 // Block was tracked - update statistics
497 if (size >= old_size) {
498 // Growing: track additional allocation
499 size_t delta = size - old_size;
500 atomic_fetch_add(&g_mem.total_allocated, delta);
501 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, delta) + delta;
502
503 // Update peak usage if new usage exceeds previous peak
504 size_t peak = atomic_load(&g_mem.peak_usage);
505 while (new_usage > peak) {
506 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
507 break;
508 }
509 } else {
510 // Shrinking: track freed memory
511 size_t delta = old_size - size;
512 atomic_fetch_add(&g_mem.total_freed, delta);
513 atomic_fetch_sub(&g_mem.current_usage, delta);
514 }
515 } else {
516 // Block was not tracked - treat as new allocation
517 atomic_fetch_add(&g_mem.total_allocated, size);
518 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
519
520 // Update peak usage
521 size_t peak = atomic_load(&g_mem.peak_usage);
522 while (new_usage > peak) {
523 if (atomic_compare_exchange_weak(&g_mem.peak_usage, &peak, new_usage))
524 break;
525 }
526 }
527
528 // Update tracking list with new pointer and metadata
529 if (ensure_mutex_initialized()) {
530 mutex_lock(&g_mem.mutex);
531
532 // Find existing block in linked list
533 mem_block_t *curr = g_mem.head;
534 while (curr && curr->ptr != ptr) {
535 curr = curr->next;
536 }
537
538 if (curr) {
539 // Update existing block with new metadata
540 curr->ptr = new_ptr;
541 curr->size = size;
542 curr->is_aligned = false;
543 SAFE_STRNCPY(curr->file, file, sizeof(curr->file) - 1);
544 curr->file[sizeof(curr->file) - 1] = '\0';
545 curr->line = line;
546 } else {
547 // Block not found - create new tracking entry
548 mem_block_t *block = (mem_block_t *)malloc(sizeof(mem_block_t));
549 if (block) {
550 block->ptr = new_ptr;
551 block->size = size;
552 block->is_aligned = false;
553 SAFE_STRNCPY(block->file, file, sizeof(block->file) - 1);
554 block->line = line;
555 block->next = g_mem.head;
556 g_mem.head = block;
557 }
558 }
559
560 mutex_unlock(&g_mem.mutex);
561 } else {
562 log_warn_every(LOG_RATE_FAST, "Debug memory mutex unavailable while updating realloc block %p -> %p at %s:%d", ptr,
563 new_ptr, file, line);
564 }
565
566 g_in_debug_memory = false;
567 return new_ptr;
568}
569
570void debug_memory_set_quiet_mode(bool quiet) {
571 g_mem.quiet_mode = quiet;
572}
573
574void debug_memory_report(void) {
575 // Guard against multiple calls during shutdown
576 static bool report_done = false;
577 if (report_done) {
578 return;
579 }
580
581 // Check for usage error BEFORE cleanup clears it
582 asciichat_error_t error = GET_ERRNO();
583
585 report_done = true;
586
587 // Skip memory report if an action flag was passed (for clean action output)
588 // unless explicitly forced via ASCII_CHAT_MEMORY_DEBUG environment variable
589 if (has_action_flag() && !SAFE_GETENV("ASCII_CHAT_MEMORY_DEBUG")) {
590 return;
591 }
592
593 // Skip memory report on command-line usage errors for clean error output
594 if (error == ERROR_USAGE) {
595 return;
596 }
597
598 bool quiet = g_mem.quiet_mode;
599
600 // Reset ignore counters at start of report
601 reset_ignore_counters();
602
603 if (!quiet && terminal_is_interactive()) {
604 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "\n%s\n", colored_string(LOG_COLOR_DEV, "=== Memory Report ===")));
605
606 size_t total_allocated = atomic_load(&g_mem.total_allocated);
607 size_t total_freed = atomic_load(&g_mem.total_freed);
608 size_t current_usage = atomic_load(&g_mem.current_usage);
609 size_t peak_usage = atomic_load(&g_mem.peak_usage);
610 size_t malloc_calls = atomic_load(&g_mem.malloc_calls);
611 size_t calloc_calls = atomic_load(&g_mem.calloc_calls);
612 size_t free_calls = atomic_load(&g_mem.free_calls);
613
614 // Calculate total size and count of suppressed allocations
615 size_t suppressed_bytes = 0;
616 size_t suppressed_count = 0;
617 if (g_mem.head) {
618 if (ensure_mutex_initialized()) {
619 // Use polling instead of blocking mutex_lock to avoid hangs during shutdown.
620 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
621 goto skip_memory_iter;
622 }
623
624 // Now we have the lock - iterate memory blocks
625 mem_block_t *curr = g_mem.head;
626 while (curr) {
627 if (should_ignore_allocation(curr->file, curr->line)) {
628 suppressed_bytes += curr->size;
629 suppressed_count++;
630 }
631 curr = curr->next;
632 }
633 mutex_unlock(&g_mem.mutex);
634 }
635 }
636
637 skip_memory_iter:
638
639 // Adjust current usage to exclude suppressed allocations
640 size_t adjusted_current_usage = (current_usage >= suppressed_bytes) ? (current_usage - suppressed_bytes) : 0;
641
642 // Adjust total allocated for display to exclude suppressions (so it matches total freed)
643 size_t adjusted_total_allocated =
644 (total_allocated >= suppressed_bytes) ? (total_allocated - suppressed_bytes) : total_allocated;
645
646 // Reset ignore counters after counting suppressed bytes
647 reset_ignore_counters();
648
649 // Count actual unfreed allocations early so we can use it to filter suppression warnings
650 size_t unfreed_count = 0;
651 if (g_mem.head) {
652 if (ensure_mutex_initialized()) {
653 if (acquire_mutex_with_polling(&g_mem.mutex, 100)) {
654 mem_block_t *curr = g_mem.head;
655 while (curr) {
656 if (!should_ignore_allocation(curr->file, curr->line)) {
657 unfreed_count++;
658 }
659 curr = curr->next;
660 }
661 mutex_unlock(&g_mem.mutex);
662 }
663 }
664 }
665
666 // Only warn about suppression mismatches if there are outstanding allocations
667 if (unfreed_count > 0) {
668 for (size_t i = 0; g_ignore_list[i].file != NULL; i++) {
669 if (g_ignore_counts[i] != g_ignore_list[i].expected_count) {
670 SAFE_IGNORE_PRINTF_RESULT(
671 safe_fprintf(stderr, "%s\n", colored_string(LOG_COLOR_ERROR, "WARNING: Suppression mismatch detected")));
672 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " %s:%d - expected %d, found %d\n", g_ignore_list[i].file,
673 g_ignore_list[i].line, g_ignore_list[i].expected_count,
674 g_ignore_counts[i]));
675 }
676 }
677 }
678
679 char pretty_total[64];
680 char pretty_freed[64];
681 char pretty_current[64];
682 char pretty_peak[64];
683 format_bytes_pretty(adjusted_total_allocated, pretty_total, sizeof(pretty_total));
684 format_bytes_pretty(total_freed, pretty_freed, sizeof(pretty_freed));
685 format_bytes_pretty(adjusted_current_usage, pretty_current, sizeof(pretty_current));
686 format_bytes_pretty(peak_usage, pretty_peak, sizeof(pretty_peak));
687
688 // Calculate max label width for column alignment
689 const char *label_total = "Total allocated:";
690 const char *label_freed = "Total freed:";
691 const char *label_current = "Current usage:";
692 const char *label_peak = "Peak usage:";
693 const char *label_malloc = "malloc calls:";
694 const char *label_calloc = "calloc calls:";
695 const char *label_free = "free calls:";
696 const char *label_suppressions = "suppressions:";
697 const char *label_diff = "unfreed allocations:";
698
699 size_t max_label_width = 0;
700 max_label_width = MAX(max_label_width, strlen(label_total));
701 max_label_width = MAX(max_label_width, strlen(label_freed));
702 max_label_width = MAX(max_label_width, strlen(label_current));
703 max_label_width = MAX(max_label_width, strlen(label_peak));
704 max_label_width = MAX(max_label_width, strlen(label_malloc));
705 max_label_width = MAX(max_label_width, strlen(label_calloc));
706 max_label_width = MAX(max_label_width, strlen(label_free));
707 max_label_width = MAX(max_label_width, strlen(label_suppressions));
708 max_label_width = MAX(max_label_width, strlen(label_diff));
709
710#define PRINT_MEM_LINE(label, value_str) \
711 do { \
712 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "%s", colored_string(LOG_COLOR_GREY, label))); \
713 for (size_t i = strlen(label); i < max_label_width; i++) { \
714 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " ")); \
715 } \
716 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " %s\n", value_str)); \
717 } while (0)
718
719#define PRINT_MEM_LINE_COLORED(label, value_str, color) \
720 do { \
721 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "%s", colored_string(LOG_COLOR_GREY, label))); \
722 for (size_t i = strlen(label); i < max_label_width; i++) { \
723 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " ")); \
724 } \
725 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " %s\n", colored_string(color, value_str))); \
726 } while (0)
727
728 // Colorize total allocated/freed: green if they match, red if they don't (memory leak)
729 // We've already adjusted allocated to exclude suppressions, so now compare directly
730 log_color_t alloc_freed_color = (adjusted_total_allocated == total_freed) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
731 PRINT_MEM_LINE_COLORED(label_total, pretty_total, alloc_freed_color);
732 PRINT_MEM_LINE_COLORED(label_freed, pretty_freed, alloc_freed_color);
733
734 // Colorize current usage: green if 0 (no leaks), red if any unfreed memory (leak detected)
735 log_color_t current_color = (adjusted_current_usage == 0) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
736 PRINT_MEM_LINE_COLORED(label_current, pretty_current, current_color);
737
738 // Colorize peak usage: green if 0-50 MB, yellow if 50-80 MB, red if above 80 MB
739 log_color_t peak_color = LOG_COLOR_INFO; // Default green
740 if (peak_usage >= (80 * 1024 * 1024)) {
741 peak_color = LOG_COLOR_ERROR; // Red if >= 80 MB
742 } else if (peak_usage >= (50 * 1024 * 1024)) {
743 peak_color = LOG_COLOR_WARN; // Yellow if >= 50 MB
744 }
745 PRINT_MEM_LINE_COLORED(label_peak, pretty_peak, peak_color);
746
747 // unfreed_count already calculated earlier for suppression warning filtering
748
749 // Colorize malloc/calloc/free calls: green if unfreed == 0, red if unfreed != 0
750 log_color_t calls_color = (unfreed_count == 0) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
751 char malloc_str[32];
752 safe_snprintf(malloc_str, sizeof(malloc_str), "%zu", malloc_calls);
753 PRINT_MEM_LINE_COLORED(label_malloc, malloc_str, calls_color);
754
755 char calloc_str[32];
756 safe_snprintf(calloc_str, sizeof(calloc_str), "%zu", calloc_calls);
757 PRINT_MEM_LINE_COLORED(label_calloc, calloc_str, calls_color);
758
759 char free_str[32];
760 safe_snprintf(free_str, sizeof(free_str), "%zu", free_calls);
761 PRINT_MEM_LINE_COLORED(label_free, free_str, calls_color);
762
763 // Colorize suppressions: green if working properly, red if >= 1MB or >= 100 allocations
764 if (suppressed_count > 0) {
765 char pretty_suppressed[64];
766 format_bytes_pretty(suppressed_bytes, pretty_suppressed, sizeof(pretty_suppressed));
767 char suppressions_str[128];
768 safe_snprintf(suppressions_str, sizeof(suppressions_str), "%zu (%s)", suppressed_count, pretty_suppressed);
769
770 log_color_t suppressions_color = LOG_COLOR_INFO; // Green by default (working properly)
771 if (suppressed_bytes >= (1024 * 1024) || suppressed_count >= 100) {
772 suppressions_color = LOG_COLOR_ERROR; // Red if >= 1MB or >= 100 allocations
773 }
774 PRINT_MEM_LINE_COLORED(label_suppressions, suppressions_str, suppressions_color);
775 }
776
777 char diff_str[32];
778 safe_snprintf(diff_str, sizeof(diff_str), "%zu", unfreed_count);
779 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "%s", colored_string(LOG_COLOR_GREY, label_diff)));
780 for (size_t i = strlen(label_diff); i < max_label_width; i++) {
781 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " "));
782 }
783 SAFE_IGNORE_PRINTF_RESULT(
784 safe_fprintf(stderr, " %s\n", colored_string(unfreed_count == 0 ? LOG_COLOR_INFO : LOG_COLOR_ERROR, diff_str)));
785
786#undef PRINT_MEM_LINE
787#undef PRINT_MEM_LINE_COLORED
788
789 // Only show "Current allocations:" section if there are actual leaks
790 if (unfreed_count > 0) {
791 // Reset counters before printing pass (after counting pass used them)
792 reset_ignore_counters();
793
794 // Print "Current allocations:" header (count already shown in "unfreed allocations" above)
795 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, "\n%s\n", colored_string(LOG_COLOR_DEV, "Current allocations:")));
796 }
797
798 if (g_mem.head && unfreed_count > 0) {
799 if (ensure_mutex_initialized()) {
800 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
801 goto skip_allocations_list;
802 }
803
804 // Check if we should print backtraces
805 const char *print_backtrace = SAFE_GETENV("ASCII_CHAT_MEMORY_REPORT_BACKTRACE");
806 int backtrace_count = 0;
807 int backtrace_limit = (print_backtrace != NULL) ? 5 : 0; // Only print first 5 backtraces to save time
808
809 mem_block_t *curr = g_mem.head;
810 while (curr) {
811 // Skip ignored allocations (e.g., PCRE2 singletons)
812 if (should_ignore_allocation(curr->file, curr->line)) {
813 curr = curr->next;
814 continue;
815 }
816
817 char pretty_size[64];
818 format_bytes_pretty(curr->size, pretty_size, sizeof(pretty_size));
819 const char *file_location = curr->file; // Already normalized relative path
820
821 // Determine color based on unit (1 MB and over=red, KB=yellow, B=light blue)
822 log_color_t size_color = LOG_COLOR_DEBUG; // Default to light blue for bytes
823 if (strstr(pretty_size, "MB") || strstr(pretty_size, "GB") || strstr(pretty_size, "TB") ||
824 strstr(pretty_size, "PB") || strstr(pretty_size, "EB")) {
825 size_color = LOG_COLOR_ERROR; // Red for 1 MB and over (MB, GB, TB, PB, EB)
826 } else if (strstr(pretty_size, "KB")) {
827 size_color = LOG_COLOR_WARN; // Yellow for kilobytes
828 } else if (strstr(pretty_size, " B")) {
829 size_color = LOG_COLOR_DEBUG; // Light blue for bytes
830 }
831
832 char line_str[32];
833 safe_snprintf(line_str, sizeof(line_str), "%d", curr->line);
834 SAFE_IGNORE_PRINTF_RESULT(
835 safe_fprintf(stderr, " - %s:%s - %s\n", colored_string(LOG_COLOR_GREY, file_location),
836 colored_string(LOG_COLOR_FATAL, line_str), colored_string(size_color, pretty_size)));
837
838 // Print backtrace if environment variable is set and we haven't hit the limit
839 if (backtrace_count < backtrace_limit && curr->backtrace_count > 0) {
840 backtrace_count++;
841 // Unlock mutex before calling platform_backtrace_symbols to avoid deadlock
842 // when backtrace symbol resolution allocates memory.
843 mutex_unlock(&g_mem.mutex);
844
845 // Print backtrace with symbol names
846 char **symbols = platform_backtrace_symbols(curr->backtrace_ptrs, curr->backtrace_count);
847 if (symbols) {
848 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " Backtrace (%d frames):\n", curr->backtrace_count));
849 for (int i = 0; i < curr->backtrace_count; i++) {
850 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " [%d] %s\n", i, symbols[i]));
851 }
853 }
854
855 // Re-acquire mutex for next iteration (with polling, not blocking)
856 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
857 break; // Exit loop if we can't re-acquire the lock
858 }
859 }
860
861 curr = curr->next;
862 }
863
864 mutex_unlock(&g_mem.mutex);
865 } else {
866 SAFE_IGNORE_PRINTF_RESULT(
867 safe_fprintf(stderr, "\n%s\n",
868 colored_string(LOG_COLOR_ERROR,
869 "Current allocations unavailable: failed to initialize debug memory mutex")));
870 }
871 }
872
873 skip_allocations_list:
874 }
875}
876
877#elif defined(DEBUG_MEMORY)
878
879void debug_memory_set_quiet_mode(bool quiet) {
880 (void)quiet;
881}
882
883void debug_memory_report(void) {}
884
885void *debug_malloc(size_t size, const char *file, int line) {
886 (void)file;
887 (void)line;
888 return malloc(size);
889}
890
891void *debug_calloc(size_t count, size_t size, const char *file, int line) {
892 (void)file;
893 (void)line;
894 return calloc(count, size);
895}
896
897void *debug_realloc(void *ptr, size_t size, const char *file, int line) {
898 (void)file;
899 (void)line;
900 return realloc(ptr, size);
901}
902
903void debug_free(void *ptr, const char *file, int line) {
904 (void)file;
905 (void)line;
906 free(ptr);
907}
908
909void debug_track_aligned(void *ptr, size_t size, const char *file, int line) {
910 (void)ptr;
911 (void)size;
912 (void)file;
913 (void)line;
914}
915
916#endif
void asciichat_errno_destroy(void)
bool has_action_flag(void)
Check if an action flag was detected.
const char * extract_project_relative_path(const char *file)
Definition path.c:410
bool terminal_is_interactive(void)
void platform_sleep_ms(unsigned int ms)
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
Definition system.c:480
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
int mutex_init(mutex_t *mutex)
Definition threading.c:16
void format_bytes_pretty(size_t bytes, char *out, size_t out_capacity)
Definition util/format.c:10
const char * colored_string(log_color_t color, const char *text)
int platform_backtrace(void **buffer, int size)
Definition util.c:14
void platform_backtrace_symbols_destroy(char **symbols)
Definition util.c:26
char ** platform_backtrace_symbols(void *const *buffer, int size)
Definition util.c:20