8#if defined(DEBUG_MEMORY) && !defined(NDEBUG)
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>
30typedef struct mem_block {
33 char file[BUFFER_SIZE_SMALL];
36 void *backtrace_ptrs[16];
38 struct mem_block *next;
42__thread
bool g_in_debug_memory =
false;
61static const ignore_entry_t g_ignore_list[] = {
62 {
"lib/options/colorscheme.c", 579, 8},
63 {
"lib/options/colorscheme.c", 596, 8},
64 {
"lib/options/colorscheme.c", 613, 8},
65 {
"lib/util/path.c", 1203, 1},
70static int g_ignore_counts[32] = {0};
75static void reset_ignore_counters(
void) {
76 memset(g_ignore_counts, 0,
sizeof(g_ignore_counts));
88static bool acquire_mutex_with_polling(mutex_t *mutex,
int timeout_ms) {
89 int max_retries = (timeout_ms + 9) / 10;
90 for (
int retry = 0; retry < max_retries; retry++) {
91 int lock_result = mutex_trylock(mutex);
92 if (lock_result == 0) {
95 if (lock_result != EBUSY) {
103static bool should_ignore_allocation(
const char *file,
int line) {
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) {
111 if (g_ignore_counts[i] < g_ignore_list[i].expected_count) {
112 g_ignore_counts[i]++;
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;
133 atomic_int mutex_state;
135} g_mem = {.head = NULL,
136 .total_allocated = 0,
145 .quiet_mode =
false};
152static atomic_flag g_logged_mutex_init_failure = ATOMIC_FLAG_INIT;
154static bool ensure_mutex_initialized(
void) {
156 int state = atomic_load_explicit(&g_mem.mutex_state, memory_order_acquire);
163 if (atomic_compare_exchange_strong_explicit(&g_mem.mutex_state, &expected, 1, memory_order_acq_rel,
164 memory_order_acquire)) {
166 atomic_store_explicit(&g_mem.mutex_state, 2, memory_order_release);
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");
183void *debug_malloc(
size_t size,
const char *file,
int line) {
184 void *ptr = (
void *)malloc(size);
188 if (g_in_debug_memory) {
192 g_in_debug_memory =
true;
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;
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))
204 bool have_mutex = ensure_mutex_initialized();
206 mutex_lock(&g_mem.mutex);
208 mem_block_t *block = (mem_block_t *)malloc(
sizeof(mem_block_t));
212 block->is_aligned =
false;
214 SAFE_STRNCPY(block->file, normalized_file,
sizeof(block->file) - 1);
219 if (block->backtrace_count < 0) {
220 block->backtrace_count = 0;
222 block->next = g_mem.head;
226 mutex_unlock(&g_mem.mutex);
229 g_in_debug_memory =
false;
233void debug_track_aligned(
void *ptr,
size_t size,
const char *file,
int line) {
237 if (g_in_debug_memory) {
241 g_in_debug_memory =
true;
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;
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))
253 bool have_mutex = ensure_mutex_initialized();
255 mutex_lock(&g_mem.mutex);
257 mem_block_t *block = (mem_block_t *)malloc(
sizeof(mem_block_t));
261 block->is_aligned =
true;
263 SAFE_STRNCPY(block->file, normalized_file,
sizeof(block->file) - 1);
268 if (block->backtrace_count < 0) {
269 block->backtrace_count = 0;
271 block->next = g_mem.head;
275 mutex_unlock(&g_mem.mutex);
278 g_in_debug_memory =
false;
281void debug_free(
void *ptr,
const char *file,
int line) {
285 if (g_in_debug_memory) {
290 g_in_debug_memory =
true;
292 atomic_fetch_add(&g_mem.free_calls, 1);
294 size_t freed_size = 0;
297 bool was_aligned =
false;
300 bool have_mutex = ensure_mutex_initialized();
302 mutex_lock(&g_mem.mutex);
304 mem_block_t *prev = NULL;
305 mem_block_t *curr = g_mem.head;
308 if (curr->ptr == ptr) {
310 prev->next = curr->next;
312 g_mem.head = curr->next;
315 freed_size = curr->size;
318 was_aligned = curr->is_aligned;
328 log_warn_every(LOG_RATE_FAST,
"Freeing untracked pointer %p at %s:%d", ptr, file, line);
332 mutex_unlock(&g_mem.mutex);
334 log_warn_every(LOG_RATE_FAST,
"Debug memory mutex unavailable while freeing %p at %s:%d", ptr, file, line);
338 atomic_fetch_add(&g_mem.total_freed, freed_size);
339 atomic_fetch_sub(&g_mem.current_usage, freed_size);
352 g_in_debug_memory =
false;
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);
361 if (g_in_debug_memory) {
365 g_in_debug_memory =
true;
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;
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))
377 bool have_mutex = ensure_mutex_initialized();
379 mutex_lock(&g_mem.mutex);
381 mem_block_t *block = (mem_block_t *)malloc(
sizeof(mem_block_t));
385 block->is_aligned =
false;
386 SAFE_STRNCPY(block->file, file,
sizeof(block->file) - 1);
388 block->next = g_mem.head;
392 mutex_unlock(&g_mem.mutex);
395 g_in_debug_memory =
false;
443void *debug_realloc(
void *ptr,
size_t size,
const char *file,
int line) {
445 if (g_in_debug_memory) {
446 return realloc(ptr, size);
449 g_in_debug_memory =
true;
452 atomic_fetch_add(&g_mem.realloc_calls, 1);
456 g_in_debug_memory =
false;
457 return debug_malloc(size, file, line);
461 g_in_debug_memory =
false;
462 debug_free(ptr, file, line);
468 bool have_mutex = ensure_mutex_initialized();
471 mutex_lock(&g_mem.mutex);
474 mem_block_t *curr = g_mem.head;
475 while (curr && curr->ptr != ptr) {
479 old_size = curr->size;
482 mutex_unlock(&g_mem.mutex);
484 log_warn_every(LOG_RATE_FAST,
"Debug memory mutex unavailable while reallocating %p at %s:%d", ptr, file, line);
488 void *new_ptr = SAFE_REALLOC(ptr, size,
void *);
490 g_in_debug_memory =
false;
497 if (size >= old_size) {
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;
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))
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);
517 atomic_fetch_add(&g_mem.total_allocated, size);
518 size_t new_usage = atomic_fetch_add(&g_mem.current_usage, size) + size;
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))
529 if (ensure_mutex_initialized()) {
530 mutex_lock(&g_mem.mutex);
533 mem_block_t *curr = g_mem.head;
534 while (curr && curr->ptr != ptr) {
542 curr->is_aligned =
false;
543 SAFE_STRNCPY(curr->file, file,
sizeof(curr->file) - 1);
544 curr->file[
sizeof(curr->file) - 1] =
'\0';
548 mem_block_t *block = (mem_block_t *)malloc(
sizeof(mem_block_t));
550 block->ptr = new_ptr;
552 block->is_aligned =
false;
553 SAFE_STRNCPY(block->file, file,
sizeof(block->file) - 1);
555 block->next = g_mem.head;
560 mutex_unlock(&g_mem.mutex);
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);
566 g_in_debug_memory =
false;
570void debug_memory_set_quiet_mode(
bool quiet) {
571 g_mem.quiet_mode = quiet;
574void debug_memory_report(
void) {
576 static bool report_done =
false;
582 asciichat_error_t error = GET_ERRNO();
594 if (error == ERROR_USAGE) {
598 bool quiet = g_mem.quiet_mode;
601 reset_ignore_counters();
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);
615 size_t suppressed_bytes = 0;
616 size_t suppressed_count = 0;
618 if (ensure_mutex_initialized()) {
620 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
621 goto skip_memory_iter;
625 mem_block_t *curr = g_mem.head;
627 if (should_ignore_allocation(curr->file, curr->line)) {
628 suppressed_bytes += curr->size;
633 mutex_unlock(&g_mem.mutex);
640 size_t adjusted_current_usage = (current_usage >= suppressed_bytes) ? (current_usage - suppressed_bytes) : 0;
643 size_t adjusted_total_allocated =
644 (total_allocated >= suppressed_bytes) ? (total_allocated - suppressed_bytes) : total_allocated;
647 reset_ignore_counters();
650 size_t unfreed_count = 0;
652 if (ensure_mutex_initialized()) {
653 if (acquire_mutex_with_polling(&g_mem.mutex, 100)) {
654 mem_block_t *curr = g_mem.head;
656 if (!should_ignore_allocation(curr->file, curr->line)) {
661 mutex_unlock(&g_mem.mutex);
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(
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]));
679 char pretty_total[64];
680 char pretty_freed[64];
681 char pretty_current[64];
682 char pretty_peak[64];
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:";
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));
710#define PRINT_MEM_LINE(label, value_str) \
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, " ")); \
716 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " %s\n", value_str)); \
719#define PRINT_MEM_LINE_COLORED(label, value_str, color) \
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, " ")); \
725 SAFE_IGNORE_PRINTF_RESULT(safe_fprintf(stderr, " %s\n", colored_string(color, value_str))); \
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);
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);
739 log_color_t peak_color = LOG_COLOR_INFO;
740 if (peak_usage >= (80 * 1024 * 1024)) {
741 peak_color = LOG_COLOR_ERROR;
742 }
else if (peak_usage >= (50 * 1024 * 1024)) {
743 peak_color = LOG_COLOR_WARN;
745 PRINT_MEM_LINE_COLORED(label_peak, pretty_peak, peak_color);
750 log_color_t calls_color = (unfreed_count == 0) ? LOG_COLOR_INFO : LOG_COLOR_ERROR;
752 safe_snprintf(malloc_str,
sizeof(malloc_str),
"%zu", malloc_calls);
753 PRINT_MEM_LINE_COLORED(label_malloc, malloc_str, calls_color);
756 safe_snprintf(calloc_str,
sizeof(calloc_str),
"%zu", calloc_calls);
757 PRINT_MEM_LINE_COLORED(label_calloc, calloc_str, calls_color);
760 safe_snprintf(free_str,
sizeof(free_str),
"%zu", free_calls);
761 PRINT_MEM_LINE_COLORED(label_free, free_str, calls_color);
764 if (suppressed_count > 0) {
765 char pretty_suppressed[64];
767 char suppressions_str[128];
768 safe_snprintf(suppressions_str,
sizeof(suppressions_str),
"%zu (%s)", suppressed_count, pretty_suppressed);
770 log_color_t suppressions_color = LOG_COLOR_INFO;
771 if (suppressed_bytes >= (1024 * 1024) || suppressed_count >= 100) {
772 suppressions_color = LOG_COLOR_ERROR;
774 PRINT_MEM_LINE_COLORED(label_suppressions, suppressions_str, suppressions_color);
778 safe_snprintf(diff_str,
sizeof(diff_str),
"%zu", unfreed_count);
780 for (
size_t i = strlen(label_diff); i < max_label_width; i++) {
783 SAFE_IGNORE_PRINTF_RESULT(
787#undef PRINT_MEM_LINE_COLORED
790 if (unfreed_count > 0) {
792 reset_ignore_counters();
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;
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;
809 mem_block_t *curr = g_mem.head;
812 if (should_ignore_allocation(curr->file, curr->line)) {
817 char pretty_size[64];
819 const char *file_location = curr->file;
822 log_color_t size_color = LOG_COLOR_DEBUG;
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;
826 }
else if (strstr(pretty_size,
"KB")) {
827 size_color = LOG_COLOR_WARN;
828 }
else if (strstr(pretty_size,
" B")) {
829 size_color = LOG_COLOR_DEBUG;
834 SAFE_IGNORE_PRINTF_RESULT(
839 if (backtrace_count < backtrace_limit && curr->backtrace_count > 0) {
843 mutex_unlock(&g_mem.mutex);
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]));
856 if (!acquire_mutex_with_polling(&g_mem.mutex, 100)) {
864 mutex_unlock(&g_mem.mutex);
866 SAFE_IGNORE_PRINTF_RESULT(
869 "Current allocations unavailable: failed to initialize debug memory mutex")));
873 skip_allocations_list:
877#elif defined(DEBUG_MEMORY)
879void debug_memory_set_quiet_mode(
bool quiet) {
883void debug_memory_report(
void) {}
885void *debug_malloc(
size_t size,
const char *file,
int line) {
891void *debug_calloc(
size_t count,
size_t size,
const char *file,
int line) {
894 return calloc(count, size);
897void *debug_realloc(
void *ptr,
size_t size,
const char *file,
int line) {
900 return realloc(ptr, size);
903void debug_free(
void *ptr,
const char *file,
int line) {
909void debug_track_aligned(
void *ptr,
size_t size,
const char *file,
int line) {
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)
int safe_fprintf(FILE *stream, const char *format,...)
Safe formatted output to file stream.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
int mutex_init(mutex_t *mutex)
const char * colored_string(log_color_t color, const char *text)
int platform_backtrace(void **buffer, int size)
void platform_backtrace_symbols_destroy(char **symbols)
char ** platform_backtrace_symbols(void *const *buffer, int size)