ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
lock.c
Go to the documentation of this file.
1
8// Header must be included even in release builds to get inline no-op stubs
9#include "debug/lock.h"
10
11#ifdef DEBUG_LOCKS
12// Only compile lock_debug implementation when DEBUG_LOCKS is defined
13// Without DEBUG_LOCKS, lock_debug.h provides inline no-op stubs
14
15#include "common.h"
16#include "asciichat_errno.h"
18#include "util/fnv1a.h"
19#include "util/time.h"
20#include "util/uthash.h"
21#include <stdlib.h>
22#include <time.h>
23#include <stdatomic.h>
24#include <string.h>
25
26#ifdef _WIN32
27#include <io.h>
28#include <conio.h>
29#else
30#include <sys/select.h>
31#include <unistd.h>
32#include <termios.h>
33#include <fcntl.h>
34#endif
35
36// ============================================================================
37// Constants
38// ============================================================================
39
47#define LOCK_HOLD_TIME_WARNING_MS 500
48
55#define SAFE_BUFFER_SIZE(buffer_size, offset) \
56 ((offset) < 0 || (size_t)(offset) >= (buffer_size) ? 0 : (buffer_size) - (size_t)(offset))
57
58// ============================================================================
59// Global State
60// ============================================================================
61
62lock_debug_manager_t g_lock_debug_manager = {0};
63atomic_bool g_initializing = false; // Flag to prevent tracking during initialization
64
65#ifndef _WIN32
66// Terminal state for POSIX systems to enable raw mode input
67static struct termios g_original_termios;
68static bool g_termios_saved = false;
69#endif
70
80static lock_record_t *create_lock_record(void *lock_address, lock_type_t lock_type, const char *file_name,
81 int line_number, const char *function_name) {
82 lock_record_t *record = SAFE_CALLOC(1, sizeof(lock_record_t), lock_record_t *);
83
84 // Fill in basic information
85 record->key = lock_record_key(lock_address, lock_type);
86 record->lock_address = lock_address;
87 record->lock_type = lock_type;
88 record->thread_id = asciichat_thread_current_id();
89 record->file_name = file_name;
90 record->line_number = line_number;
91 record->function_name = function_name;
92
93 // Get current time
94 if (clock_gettime(CLOCK_MONOTONIC, &record->acquisition_time) != 0) {
95 SET_ERRNO(ERROR_PLATFORM_INIT, "Failed to get acquisition time");
96 SAFE_FREE(record);
97 return NULL;
98 }
99
100 // Capture backtrace
101 // NOTE: platform_backtrace_symbols_free() safely handles the case where
102 // backtrace_symbols() uses system malloc() but mimalloc overrides free()
103 // by detecting our enhanced format and not freeing system-allocated memory
104 record->backtrace_size = platform_backtrace(record->backtrace_buffer, MAX_BACKTRACE_FRAMES);
105 if (record->backtrace_size > 0) {
106 record->backtrace_symbols = platform_backtrace_symbols(record->backtrace_buffer, record->backtrace_size);
107 if (!record->backtrace_symbols) {
108 static bool symbolize_error_logged = false;
109 if (!symbolize_error_logged) {
110 log_warn("Failed to symbolize backtrace for lock record (backtrace support may be unavailable)");
111 symbolize_error_logged = true;
112 }
113 }
114 } else {
115 // Backtrace unavailable (e.g., no libexecinfo)
116 // Only log this once to avoid spam
117 static bool backtrace_error_logged = false;
118 if (!backtrace_error_logged) {
119 log_debug("Backtrace not available for lock debugging");
120 backtrace_error_logged = true;
121 }
122 }
123
124 return record;
125}
126
131static void free_lock_record(lock_record_t *record) {
132 if (!record) {
133 SET_ERRNO(ERROR_INVALID_STATE, "Freeing NULL lock record");
134 return;
135 }
136
137 // Free symbolized backtrace
138 if (record->backtrace_symbols) {
139 platform_backtrace_symbols_free(record->backtrace_symbols);
140 }
141
142 SAFE_FREE(record);
143}
144
145// ============================================================================
146// Callback Functions
147// ============================================================================
148
154static void collect_lock_record_callback(lock_record_t *record, void *user_data) {
155 struct lock_collector {
156 uint32_t count;
157 char *buffer;
158 int *offset;
159 } *collector = (struct lock_collector *)user_data;
160
161 collector->count++;
162 int *offset = collector->offset;
163 char *buffer = collector->buffer;
164 size_t buffer_size = 32768; // Match the buffer size in lock_debug_print_state
165
166 // Print lock information
167 const char *lock_type_str;
168 switch (record->lock_type) {
169 case LOCK_TYPE_MUTEX:
170 lock_type_str = "MUTEX";
171 break;
172 case LOCK_TYPE_RWLOCK_READ:
173 lock_type_str = "RWLOCK_READ";
174 break;
175 case LOCK_TYPE_RWLOCK_WRITE:
176 lock_type_str = "RWLOCK_WRITE";
177 break;
178 default:
179 lock_type_str = "UNKNOWN";
180 break;
181 }
182
183 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), "Lock #%u: %s at %p\n",
184 collector->count, lock_type_str, record->lock_address);
185 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), " Thread ID: %llu\n",
186 (unsigned long long)record->thread_id);
187 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), " Acquired: %s:%d in %s()\n",
188 record->file_name, record->line_number, record->function_name);
189
190 // Calculate how long the lock has been held
191 struct timespec current_time;
192 if (clock_gettime(CLOCK_MONOTONIC, &current_time) == 0) {
193 long long held_sec = current_time.tv_sec - record->acquisition_time.tv_sec;
194 long held_nsec = current_time.tv_nsec - record->acquisition_time.tv_nsec;
195
196 // Handle nanosecond underflow
197 if (held_nsec < 0) {
198 held_sec--;
199 held_nsec += 1000000000;
200 }
201
202 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), " Held for: %lld.%09ld seconds\n",
203 held_sec, held_nsec);
204 } else {
205 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset),
206 " Acquired at: %lld.%09ld seconds (monotonic)\n", (long long)record->acquisition_time.tv_sec,
207 record->acquisition_time.tv_nsec);
208 }
209
210 // Print backtrace using platform symbol resolution with colored format
211 if (record->backtrace_size > 0) {
212 char **symbols = platform_backtrace_symbols(record->backtrace_buffer, record->backtrace_size);
213 *offset += platform_format_backtrace_symbols(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), "Call stack",
214 symbols, record->backtrace_size, 0, 0, NULL);
215 if (symbols) {
217 }
218 } else {
219 *offset += snprintf(buffer + *offset, SAFE_BUFFER_SIZE(buffer_size, *offset), " Call stack: <capture failed>\n");
220 }
221}
222
228static void cleanup_lock_record_callback(lock_record_t *record, void *user_data) {
229 uint32_t *count = (uint32_t *)user_data;
230
231 (*count)++;
232 free_lock_record(record);
233}
234
240static void print_usage_stats_callback(lock_usage_stats_t *stats, void *user_data) {
241 uint32_t *count = (uint32_t *)user_data;
242 (*count)++;
243
244 // Get lock type string
245 const char *lock_type_str;
246 switch (stats->lock_type) {
247 case LOCK_TYPE_MUTEX:
248 lock_type_str = "MUTEX";
249 break;
250 case LOCK_TYPE_RWLOCK_READ:
251 lock_type_str = "RWLOCK_READ";
252 break;
253 case LOCK_TYPE_RWLOCK_WRITE:
254 lock_type_str = "RWLOCK_WRITE";
255 break;
256 default:
257 lock_type_str = "UNKNOWN";
258 break;
259 }
260
261 // Calculate average hold time
262 uint64_t avg_hold_time_ns = stats->total_hold_time_ns / stats->total_acquisitions;
263
264 // Format all information into a single log message with newlines
265 char log_message[1024];
266 int offset = 0;
267
268 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
269 "Usage #%u: %s at %s:%d in %s()\n", *count, lock_type_str, stats->file_name,
270 stats->line_number, stats->function_name);
271 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
272 " Total acquisitions: %llu\n", (unsigned long long)stats->total_acquisitions);
273 offset +=
274 safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
275 " Total hold time: %llu.%03llu ms\n", (unsigned long long)(stats->total_hold_time_ns / 1000000),
276 (unsigned long long)((stats->total_hold_time_ns % 1000000) / 1000));
277 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
278 " Average hold time: %llu.%03llu ms\n", (unsigned long long)(avg_hold_time_ns / 1000000),
279 (unsigned long long)((avg_hold_time_ns % 1000000) / 1000));
280 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
281 " Max hold time: %llu.%03llu ms\n", (unsigned long long)(stats->max_hold_time_ns / 1000000),
282 (unsigned long long)((stats->max_hold_time_ns % 1000000) / 1000));
283 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
284 " Min hold time: %llu.%03llu ms\n", (unsigned long long)(stats->min_hold_time_ns / 1000000),
285 (unsigned long long)((stats->min_hold_time_ns % 1000000) / 1000));
286 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
287 " First acquisition: %lld.%09ld\n", (long long)stats->first_acquisition.tv_sec,
288 stats->first_acquisition.tv_nsec);
289 offset += safe_snprintf(log_message + offset, SAFE_BUFFER_SIZE(sizeof(log_message), offset),
290 " Last acquisition: %lld.%09ld", (long long)stats->last_acquisition.tv_sec,
291 stats->last_acquisition.tv_nsec);
292
293 log_info("%s", log_message);
294}
295
301static void cleanup_usage_stats_callback(lock_usage_stats_t *stats, void *user_data) {
302 UNUSED(user_data);
303 SAFE_FREE(stats);
304}
305
311void print_orphaned_release_callback(lock_record_t *record, void *user_data) {
312 uint32_t *count = (uint32_t *)user_data;
313 (*count)++;
314
315 // Print lock information
316 const char *lock_type_str;
317 switch (record->lock_type) {
318 case LOCK_TYPE_MUTEX:
319 lock_type_str = "MUTEX";
320 break;
321 case LOCK_TYPE_RWLOCK_READ:
322 lock_type_str = "RWLOCK_READ";
323 break;
324 case LOCK_TYPE_RWLOCK_WRITE:
325 lock_type_str = "RWLOCK_WRITE";
326 break;
327 default:
328 lock_type_str = "UNKNOWN";
329 break;
330 }
331
332 log_info("Orphaned Release #%u: %s at %p", *count, lock_type_str, record->lock_address);
333 log_info(" Thread ID: %llu", (unsigned long long)record->thread_id);
334 log_info(" Released: %s:%d in %s()", record->file_name, record->line_number, record->function_name);
335 log_info(" Released at: %lld.%09ld seconds (monotonic)", (long long)record->acquisition_time.tv_sec,
336 record->acquisition_time.tv_nsec);
337
338 // Print backtrace for the orphaned release
339 if (record->backtrace_size > 0) {
340 char **symbols = platform_backtrace_symbols(record->backtrace_buffer, record->backtrace_size);
341 platform_print_backtrace_symbols(" Release call stack", symbols, record->backtrace_size, 0, 0, NULL);
342 if (symbols) {
344 }
345 } else {
346 log_info(" Release call stack: <capture failed>");
347 }
348}
349
350// ============================================================================
351// Debug Thread Functions
352// ============================================================================
353
363static void check_long_held_locks(void) {
364 if (!atomic_load(&g_lock_debug_manager.initialized)) {
365 return;
366 }
367
368 // Acquire read lock for lock_records
369 rwlock_rdlock_impl(&g_lock_debug_manager.lock_records_lock);
370
371 struct timespec current_time;
372 if (clock_gettime(CLOCK_MONOTONIC, &current_time) != 0) {
373 rwlock_rdunlock_impl(&g_lock_debug_manager.lock_records_lock);
374 return;
375 }
376
377 // Iterate through all currently held locks
378 bool found_long_held_lock = false;
379 lock_record_t *entry, *tmp;
380 HASH_ITER(hash_handle, g_lock_debug_manager.lock_records, entry, tmp) {
381 // Calculate how long the lock has been held in nanoseconds
382 long long held_sec = current_time.tv_sec - entry->acquisition_time.tv_sec;
383 long held_nsec = current_time.tv_nsec - entry->acquisition_time.tv_nsec;
384
385 // Handle nanosecond underflow
386 if (held_nsec < 0) {
387 held_sec--;
388 held_nsec += 1000000000;
389 }
390
391 // Convert to nanoseconds (double for format_duration_ns)
392 double held_ns = (held_sec * 1000000000.0) + held_nsec;
393
394 // Only log if held longer than 100ms (100000000 nanoseconds)
395 const double WARNING_THRESHOLD_NS = 100000000.0; // 100ms in nanoseconds
396 if (held_ns > WARNING_THRESHOLD_NS) {
397 found_long_held_lock = true;
398
399 // Get lock type string
400 const char *lock_type_str;
401 switch (entry->lock_type) {
402 case LOCK_TYPE_MUTEX:
403 lock_type_str = "MUTEX";
404 break;
405 case LOCK_TYPE_RWLOCK_READ:
406 lock_type_str = "RWLOCK_READ";
407 break;
408 case LOCK_TYPE_RWLOCK_WRITE:
409 lock_type_str = "RWLOCK_WRITE";
410 break;
411 default:
412 lock_type_str = "UNKNOWN";
413 break;
414 }
415
416 // Format duration like STOP_TIMER_AND_LOG does
417 char duration_str[32];
418 format_duration_ns(held_ns, duration_str, sizeof(duration_str));
419
420 // Log warning with formatted duration
422 "Lock held for %s (threshold: 100ms) - %s at %p\n"
423 " Acquired: %s:%d in %s()\n"
424 " Thread ID: %llu",
425 duration_str, lock_type_str, entry->lock_address, entry->file_name, entry->line_number,
426 entry->function_name, (unsigned long long)entry->thread_id);
427 }
428 }
429
430 rwlock_rdunlock_impl(&g_lock_debug_manager.lock_records_lock);
431
432 // Print backtrace only once if any long-held locks were found
433 if (found_long_held_lock) {
435 }
436}
437
443static void *debug_thread_func(void *arg) {
444 UNUSED(arg);
445
446#ifndef _WIN32
447 // Set terminal to raw mode for immediate key detection
448 struct termios raw;
449 if (tcgetattr(STDIN_FILENO, &g_original_termios) == 0) {
450 g_termios_saved = true;
451 raw = g_original_termios;
452 raw.c_lflag &= ~((tcflag_t)(ICANON | ECHO)); // Disable canonical mode and echo
453 raw.c_cc[VMIN] = 0; // Non-blocking read
454 raw.c_cc[VTIME] = 0; // No timeout
455 if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) != 0) {
456 log_warn("Failed to set terminal to raw mode for lock debug");
457 g_termios_saved = false;
458 }
459 } else {
460 log_warn("Failed to get terminal attributes for lock debug");
461 }
462#endif
463
464 log_debug("Lock debug thread started - press '?' to print lock state");
465
466 while (atomic_load(&g_lock_debug_manager.debug_thread_running)) {
467 // Check for locks held > 100ms and log warnings
468 check_long_held_locks();
469
470 // Allow external trigger via flag (non-blocking)
471 if (atomic_exchange(&g_lock_debug_manager.should_print_locks, false)) {
472 lock_debug_print_state();
473 }
474
475 // Check for keyboard input first
476#ifdef _WIN32
477 if (_kbhit()) {
478 int ch = _getch();
479 if (ch == '?') {
480 lock_debug_print_state();
481 }
482 }
483
484 // Small sleep to prevent CPU spinning
486#else
487 // POSIX: use select() for non-blocking input (now in raw mode)
488 fd_set readfds;
489 struct timeval timeout;
490
491 FD_ZERO(&readfds);
492 FD_SET(STDIN_FILENO, &readfds);
493 timeout.tv_sec = 0;
494 timeout.tv_usec = 100000; // 100ms timeout
495
496 int result = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &timeout);
497 if (result > 0 && FD_ISSET(STDIN_FILENO, &readfds)) {
498 char input[2];
499 if (read(STDIN_FILENO, input, 1) == 1) {
500 if (input[0] == '?') {
501 lock_debug_print_state();
502 }
503 }
504 }
505#endif
506
508 }
509
510#ifndef _WIN32
511 // Restore terminal to original mode
512 if (g_termios_saved) {
513 tcsetattr(STDIN_FILENO, TCSANOW, &g_original_termios);
514 g_termios_saved = false;
515 }
516#endif
517
518 // Thread exiting
519 return NULL;
520}
521
522// ============================================================================
523// Public API Implementation
524// ============================================================================
525
526int lock_debug_init(void) {
527 log_info("Starting lock debug system initialization...");
528
529 if (atomic_load(&g_lock_debug_manager.initialized)) {
530 log_info("Lock debug system already initialized");
531 return 0; // Already initialized
532 }
533
534 // Set initialization flag to prevent tracking during init
535 atomic_store(&g_initializing, true);
536
537 // Initialize uthash hash tables to NULL (required)
538 g_lock_debug_manager.lock_records = NULL;
539 g_lock_debug_manager.usage_stats = NULL;
540 g_lock_debug_manager.orphaned_releases = NULL;
541
542 // Initialize rwlocks for thread safety (uthash requires external locking)
543 if (rwlock_init(&g_lock_debug_manager.lock_records_lock) != 0) {
544 atomic_store(&g_initializing, false);
545 SET_ERRNO(ERROR_THREAD, "Failed to initialize lock_records rwlock");
546 return -1;
547 }
548
549 if (rwlock_init(&g_lock_debug_manager.usage_stats_lock) != 0) {
550 rwlock_destroy(&g_lock_debug_manager.lock_records_lock);
551 atomic_store(&g_initializing, false);
552 SET_ERRNO(ERROR_THREAD, "Failed to initialize usage_stats rwlock");
553 return -1;
554 }
555
556 if (rwlock_init(&g_lock_debug_manager.orphaned_releases_lock) != 0) {
557 rwlock_destroy(&g_lock_debug_manager.lock_records_lock);
558 rwlock_destroy(&g_lock_debug_manager.usage_stats_lock);
559 atomic_store(&g_initializing, false);
560 SET_ERRNO(ERROR_THREAD, "Failed to initialize orphaned_releases rwlock");
561 return -1;
562 }
563
564 // Initialize atomic variables
565 atomic_store(&g_lock_debug_manager.total_locks_acquired, 0);
566 atomic_store(&g_lock_debug_manager.total_locks_released, 0);
567 atomic_store(&g_lock_debug_manager.current_locks_held, 0);
568 atomic_store(&g_lock_debug_manager.debug_thread_running, false);
569 atomic_store(&g_lock_debug_manager.debug_thread_created, false);
570 atomic_store(&g_lock_debug_manager.should_print_locks, false);
571
572 // Initialize thread handle to invalid value
573#ifdef _WIN32
574 g_lock_debug_manager.debug_thread = NULL;
575#else
576 // On POSIX, pthread_t doesn't have a standard "invalid" value
577 // but we'll rely on the debug_thread_running flag
578#endif
579
580 // Clear initialization flag FIRST, then mark as initialized
581 // This prevents race condition where initialized=true but initializing=true
582 atomic_store(&g_initializing, false);
583 atomic_store(&g_lock_debug_manager.initialized, true);
584
585 // Note: lock_debug_cleanup() will be called during normal shutdown sequence
586 // and lock_debug_cleanup_thread() will be called as one of the last things before exit
587
588 // log_info("Lock debug system initialized with uthash");
589 return 0;
590}
591
592int lock_debug_start_thread(void) {
593 if (!atomic_load(&g_lock_debug_manager.initialized)) {
594 return -1;
595 }
596
597 if (atomic_load(&g_lock_debug_manager.debug_thread_running)) {
598 return 0; // Already running
599 }
600
601 atomic_store(&g_lock_debug_manager.debug_thread_running, true);
602
603 int thread_result = asciichat_thread_create(&g_lock_debug_manager.debug_thread, debug_thread_func, NULL);
604
605 if (thread_result != 0) {
606 SET_ERRNO(ERROR_THREAD, "Failed to create lock debug thread: %d", thread_result);
607 atomic_store(&g_lock_debug_manager.debug_thread_running, false);
608 atomic_store(&g_lock_debug_manager.debug_thread_created, false);
609 return -1;
610 }
611
612 // Thread was successfully created
613 atomic_store(&g_lock_debug_manager.debug_thread_created, true);
614 return 0;
615}
616
617void lock_debug_trigger_print(void) {
618 if (atomic_load(&g_lock_debug_manager.initialized)) {
619 atomic_store(&g_lock_debug_manager.should_print_locks, true);
620 }
621}
622
623void lock_debug_cleanup(void) {
624 // Use atomic exchange to ensure cleanup only runs once
625 // This prevents double-cleanup from both atexit() and manual calls
626 bool was_initialized = atomic_exchange(&g_lock_debug_manager.initialized, false);
627 if (!was_initialized) {
628 return;
629 }
630
631 // Signal debug thread to stop but don't join it yet
632 // Thread joining will happen later in lock_debug_cleanup_thread()
633 if (atomic_load(&g_lock_debug_manager.debug_thread_running)) {
634 atomic_store(&g_lock_debug_manager.debug_thread_running, false);
635 }
636
637 // Clean up all remaining lock records
638 rwlock_wrlock_impl(&g_lock_debug_manager.lock_records_lock);
639 // Free all lock records using HASH_ITER
640 uint32_t lock_records_cleaned = 0;
641 lock_record_t *entry, *tmp;
642 HASH_ITER(hash_handle, g_lock_debug_manager.lock_records, entry, tmp) {
643 HASH_DELETE(hash_handle, g_lock_debug_manager.lock_records, entry);
644 cleanup_lock_record_callback(entry, &lock_records_cleaned);
645 }
646 if (lock_records_cleaned > 0) {
647 log_info("Cleaned up %u lock records", lock_records_cleaned);
648 }
649
650 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
651 rwlock_destroy(&g_lock_debug_manager.lock_records_lock);
652 g_lock_debug_manager.lock_records = NULL;
653
654 // Clean up usage statistics
655 rwlock_wrlock_impl(&g_lock_debug_manager.usage_stats_lock);
656 // Free all usage statistics using HASH_ITER
657 lock_usage_stats_t *stats_entry, *stats_tmp;
658 HASH_ITER(hash_handle, g_lock_debug_manager.usage_stats, stats_entry, stats_tmp) {
659 HASH_DELETE(hash_handle, g_lock_debug_manager.usage_stats, stats_entry);
660 cleanup_usage_stats_callback(stats_entry, NULL);
661 }
662
663 rwlock_wrunlock_impl(&g_lock_debug_manager.usage_stats_lock);
664 rwlock_destroy(&g_lock_debug_manager.usage_stats_lock);
665 g_lock_debug_manager.usage_stats = NULL;
666
667 // Clean up orphaned releases
668 rwlock_wrlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
669
670 // Free all orphaned release records using HASH_ITER
671 uint32_t orphaned_releases_cleaned = 0;
672 lock_record_t *orphan_entry, *orphan_tmp;
673 HASH_ITER(hash_handle, g_lock_debug_manager.orphaned_releases, orphan_entry, orphan_tmp) {
674 HASH_DELETE(hash_handle, g_lock_debug_manager.orphaned_releases, orphan_entry);
675 cleanup_lock_record_callback(orphan_entry, &orphaned_releases_cleaned);
676 }
677 if (orphaned_releases_cleaned > 0) {
678 log_info("Cleaned up %u orphaned release records", orphaned_releases_cleaned);
679 }
680
681 rwlock_wrunlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
682 rwlock_destroy(&g_lock_debug_manager.orphaned_releases_lock);
683 g_lock_debug_manager.orphaned_releases = NULL;
684
685 // initialized flag already set to false at the beginning via atomic_exchange
686 log_info("Lock debug system cleaned up");
687}
688
689void lock_debug_cleanup_thread(void) {
690 // Check if thread is/was running and join it
691 if (atomic_load(&g_lock_debug_manager.debug_thread_running)) {
692 atomic_store(&g_lock_debug_manager.debug_thread_running, false);
693 }
694
695#ifdef _WIN32
696 // On Windows, check if thread handle is valid before joining
697 if (g_lock_debug_manager.debug_thread != NULL) {
698 int join_result = asciichat_thread_join(&g_lock_debug_manager.debug_thread, NULL);
699 if (join_result == 0) {
700 // Thread handle is now NULL due to cleanup in asciichat_thread_join
701 } else {
702 // Force cleanup if join failed
703 g_lock_debug_manager.debug_thread = NULL;
704 }
705 }
706#else
707 // On POSIX, only attempt join if thread was actually created
708 // Use the debug_thread_created flag to reliably track if the thread exists
709 if (atomic_load(&g_lock_debug_manager.debug_thread_created)) {
710 asciichat_thread_join(&g_lock_debug_manager.debug_thread, NULL);
711 // Clear the flag after joining
712 atomic_store(&g_lock_debug_manager.debug_thread_created, false);
713 }
714
715 // Restore terminal to original mode if it was changed
716 if (g_termios_saved) {
717 tcsetattr(STDIN_FILENO, TCSANOW, &g_original_termios);
718 g_termios_saved = false;
719 }
720#endif
721}
722
723// ============================================================================
724// Common Helper Functions
725// ============================================================================
726
734static bool debug_should_skip_lock_tracking(void *lock_ptr, const char *file_name, const char *function_name) {
735 if (!lock_ptr || !file_name || !function_name) {
736 return true;
737 }
738
739 // Skip tracking if system is not fully initialized or during initialization
740 bool initialized = atomic_load(&g_lock_debug_manager.initialized);
741 bool initializing = atomic_load(&g_initializing);
742
743 if (!initialized || initializing) {
744 return true;
745 }
746
747 // Filter out ALL functions that our lock debug system uses internally
748 // to prevent infinite recursion
749 // Note: uthash uses macros (HASH_FIND_INT, etc.) so there are no function names to filter
750 if (strstr(function_name, "log_") != NULL || strstr(function_name, "platform_") != NULL ||
751 strstr(function_name, "create_lock_record") != NULL || strstr(function_name, "update_usage_stats") != NULL ||
752 strstr(function_name, "print_") != NULL || strstr(function_name, "debug_") != NULL ||
753 strstr(function_name, "lock_debug") != NULL || strstr(file_name, "symbols.c") != NULL ||
754 strstr(function_name, "ascii_thread") != NULL) {
755 return true;
756 }
757
758 return false;
759}
760
765static uint32_t debug_decrement_lock_counter(void) {
766 // Decrement using atomic fetch_sub which avoids type size issues
767 uint_fast32_t current = atomic_load(&g_lock_debug_manager.current_locks_held);
768 if (current > 0) {
769 uint_fast32_t prev = atomic_fetch_sub(&g_lock_debug_manager.current_locks_held, 1);
770 // If prev was already 0, we underflowed - add it back
771 if (prev == 0) {
772 atomic_fetch_add(&g_lock_debug_manager.current_locks_held, 1);
773 return 0;
774 }
775 return (uint32_t)(prev - 1);
776 }
777 return 0;
778}
779
789static bool debug_create_and_insert_lock_record(void *lock_address, lock_type_t lock_type, const char *file_name,
790 int line_number, const char *function_name) {
791#ifndef DEBUG_LOCKS
792 UNUSED(lock_address);
793 UNUSED(lock_type);
794 UNUSED(file_name);
795 UNUSED(line_number);
796 UNUSED(function_name);
797#endif
798
799 lock_record_t *record = create_lock_record(lock_address, lock_type, file_name, line_number, function_name);
800 if (record) {
801 // Acquire write lock to make the entire operation atomic
802 rwlock_wrlock_impl(&g_lock_debug_manager.lock_records_lock);
803
804 // Double-check cache is still initialized after acquiring lock
805 if (!atomic_load(&g_lock_debug_manager.initialized)) {
806 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
807 free_lock_record(record);
808 return false;
809 }
810
811 // Check if entry already exists
812 lock_record_t *existing = NULL;
813 HASH_FIND(hash_handle, g_lock_debug_manager.lock_records, &record->key, sizeof(record->key), existing);
814
815 if (existing) {
816 // Entry exists - this shouldn't happen, but handle it gracefully
817 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
818 free_lock_record(record);
819 return false;
820 }
821
822 // Add to hash table
823 HASH_ADD(hash_handle, g_lock_debug_manager.lock_records, key, sizeof(record->key), record);
824
825 // Release lock
826 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
827
828 atomic_fetch_add(&g_lock_debug_manager.total_locks_acquired, 1);
829 atomic_fetch_add(&g_lock_debug_manager.current_locks_held, 1);
830 return true;
831 }
832 // Record allocation failed
833 return false;
834}
835
846static bool debug_process_tracked_unlock(void *lock_ptr, uint32_t key, const char *lock_type_str, const char *file_name,
847 int line_number, const char *function_name) {
848#ifndef DEBUG_LOCKS
849 UNUSED(lock_ptr);
850 UNUSED(lock_type_str);
851 UNUSED(file_name);
852 UNUSED(line_number);
853 UNUSED(function_name);
854#endif
855
856 // Acquire write lock for removal
857 rwlock_wrlock_impl(&g_lock_debug_manager.lock_records_lock);
858
859 lock_record_t *record = NULL;
860 HASH_FIND(hash_handle, g_lock_debug_manager.lock_records, &key, sizeof(key), record);
861 if (record) {
862 // Calculate lock hold time BEFORE removing record
863 struct timespec current_time;
864 long long held_ms = 0;
865 if (clock_gettime(CLOCK_MONOTONIC, &current_time) == 0) {
866 long long held_sec = current_time.tv_sec - record->acquisition_time.tv_sec;
867 long held_nsec = current_time.tv_nsec - record->acquisition_time.tv_nsec;
868
869 // Handle nanosecond underflow
870 if (held_nsec < 0) {
871 held_sec--;
872 held_nsec += 1000000000;
873 }
874
875 held_ms = (held_sec * 1000) + (held_nsec / 1000000);
876
877 // Check if lock was held too long
878 if (held_ms > LOCK_HOLD_TIME_WARNING_MS) {
879 char duration_str[32];
880 format_duration_ms((double)held_ms, duration_str, sizeof(duration_str));
881 log_warn("Lock held for %s (threshold: %d ms) at %s:%d in %s()\n"
882 " Lock type: %s, address: %p",
883 duration_str, LOCK_HOLD_TIME_WARNING_MS, file_name, line_number, function_name, lock_type_str,
884 lock_ptr);
885
886 // Print backtrace from when lock was acquired
887 if (record->backtrace_size > 0 && record->backtrace_symbols) {
888 platform_print_backtrace_symbols("Backtrace from lock acquisition", record->backtrace_symbols,
889 record->backtrace_size, 0, 10, NULL);
890 } else {
891 // No backtrace available, print current backtrace
892 log_warn("No backtrace available. Current backtrace:");
893 platform_print_backtrace(2); // Skip 2 frames (this function and debug_process_tracked_unlock)
894 }
895 }
896 }
897
898 HASH_DELETE(hash_handle, g_lock_debug_manager.lock_records, record);
899 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
900
901 free_lock_record(record);
902 atomic_fetch_add(&g_lock_debug_manager.total_locks_released, 1);
903 debug_decrement_lock_counter();
904 return true;
905 }
906
907 rwlock_wrunlock_impl(&g_lock_debug_manager.lock_records_lock);
908 return false;
909}
910
920static void debug_process_untracked_unlock(void *lock_ptr, uint32_t key, const char *lock_type_str,
921 const char *file_name, int line_number, const char *function_name) {
922#ifdef DEBUG_LOCKS
923 uint64_t released = atomic_fetch_add(&g_lock_debug_manager.total_locks_released, 1) + 1;
924#else
925 atomic_fetch_add(&g_lock_debug_manager.total_locks_released, 1);
926#endif
927 uint32_t current_held = atomic_load(&g_lock_debug_manager.current_locks_held);
928#ifdef DEBUG_LOCKS
929 uint32_t held = 0;
930#endif
931 if (current_held > 0) {
932#ifdef DEBUG_LOCKS
933 held = debug_decrement_lock_counter();
934#else
935 debug_decrement_lock_counter();
936#endif
937 } else {
938#ifdef DEBUG_LOCKS
939 SET_ERRNO(ERROR_INVALID_STATE, "Attempting to release %s lock when no locks held!", lock_type_str);
940#endif
941 SET_ERRNO(ERROR_INVALID_STATE, "Attempting to release %s lock when no locks held!", lock_type_str);
942 }
943#ifdef DEBUG_LOCKS
944 SET_ERRNO(ERROR_INVALID_STATE, "%s UNTRACKED RELEASED: %p (key=%u) at %s:%d in %s() - total=%llu, held=%u",
945 lock_type_str, lock_ptr, key, file_name, line_number, function_name, (unsigned long long)released, held);
946#endif
947#ifdef DEBUG_LOCKS
948 SET_ERRNO(ERROR_INVALID_STATE, "*** WARNING: %s lock was acquired and tracked but record was lost! ***",
949 lock_type_str);
950#endif
951
952 // Create an orphaned release record to track this problematic unlock
953 lock_record_t *orphan_record = SAFE_CALLOC(1, sizeof(lock_record_t), lock_record_t *);
954 if (orphan_record) {
955 orphan_record->key = key;
956 orphan_record->lock_address = lock_ptr;
957 if (strcmp(lock_type_str, "MUTEX") == 0) {
958 orphan_record->lock_type = LOCK_TYPE_MUTEX;
959 } else if (strcmp(lock_type_str, "READ") == 0) {
960 orphan_record->lock_type = LOCK_TYPE_RWLOCK_READ;
961 } else if (strcmp(lock_type_str, "WRITE") == 0) {
962 orphan_record->lock_type = LOCK_TYPE_RWLOCK_WRITE;
963 }
964 orphan_record->thread_id = asciichat_thread_current_id();
965 orphan_record->file_name = file_name;
966 orphan_record->line_number = line_number;
967 orphan_record->function_name = function_name;
968 (void)clock_gettime(CLOCK_MONOTONIC, &orphan_record->acquisition_time); // Use release time
969
970 // Capture backtrace for this orphaned release
971#ifdef _WIN32
972 orphan_record->backtrace_size =
973 CaptureStackBackTrace(1, MAX_BACKTRACE_FRAMES, orphan_record->backtrace_buffer, NULL);
974#else
975 orphan_record->backtrace_size = platform_backtrace(orphan_record->backtrace_buffer, MAX_BACKTRACE_FRAMES);
976#endif
977
978 if (orphan_record->backtrace_size > 0) {
979 orphan_record->backtrace_symbols =
980 platform_backtrace_symbols(orphan_record->backtrace_buffer, orphan_record->backtrace_size);
981 }
982
983 // Store in orphaned releases hash table for later analysis
984 rwlock_wrlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
985 HASH_ADD(hash_handle, g_lock_debug_manager.orphaned_releases, key, sizeof(orphan_record->key), orphan_record);
986 rwlock_wrunlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
987 }
988}
989
990// ============================================================================
991// Tracked Lock Functions Implementation
992// ============================================================================
993
994int debug_mutex_lock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name) {
995 if (debug_should_skip_lock_tracking(mutex, file_name, function_name)) {
996 return mutex_lock_impl(mutex);
997 }
998
999 // Acquire the actual lock first (call implementation to avoid recursion)
1000 int result = mutex_lock_impl(mutex);
1001 if (result != 0) {
1002 return result;
1003 }
1004
1005 // Create and add lock record
1006 debug_create_and_insert_lock_record(mutex, LOCK_TYPE_MUTEX, file_name, line_number, function_name);
1007
1008 return 0;
1009}
1010
1011int debug_mutex_trylock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name) {
1012 if (debug_should_skip_lock_tracking(mutex, file_name, function_name)) {
1013 return mutex_trylock_impl(mutex);
1014 }
1015
1016 // Try to acquire the lock (call implementation to avoid recursion)
1017 int result = mutex_trylock_impl(mutex);
1018 if (result != 0) {
1019 // Lock not acquired - no tracking needed
1020 return result;
1021 }
1022
1023 // Lock acquired - create and add lock record
1024 debug_create_and_insert_lock_record(mutex, LOCK_TYPE_MUTEX, file_name, line_number, function_name);
1025
1026 return 0;
1027}
1028
1029int debug_mutex_unlock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name) {
1030 if (debug_should_skip_lock_tracking(mutex, file_name, function_name)) {
1031 return mutex_unlock_impl(mutex);
1032 }
1033
1034 // Look for mutex lock record specifically
1035 uint32_t key = lock_record_key(mutex, LOCK_TYPE_MUTEX);
1036 if (!debug_process_tracked_unlock(mutex, key, "MUTEX", file_name, line_number, function_name)) {
1037 // No record found - check if this is because the lock was filtered or because of a tracking error
1038 uint32_t current_held = atomic_load(&g_lock_debug_manager.current_locks_held);
1039
1040 if (current_held > 0) {
1041 // We have tracked locks but can't find this specific one - this is a tracking error
1042 debug_process_untracked_unlock(mutex, key, "MUTEX", file_name, line_number, function_name);
1043 } else {
1044 // No tracked locks - this means the lock was filtered during lock operation
1045 }
1046 }
1047
1048 // Unlock the actual mutex (call implementation to avoid recursion)
1049 return mutex_unlock_impl(mutex);
1050}
1051
1052int debug_rwlock_rdlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name) {
1053 if (debug_should_skip_lock_tracking(rwlock, file_name, function_name)) {
1054 return rwlock_rdlock_impl(rwlock);
1055 }
1056
1057 // Acquire the actual lock first (call implementation to avoid recursion)
1058 int result = rwlock_rdlock_impl(rwlock);
1059 if (result != 0) {
1060 return result;
1061 }
1062
1063 // Create and add lock record
1064 debug_create_and_insert_lock_record(rwlock, LOCK_TYPE_RWLOCK_READ, file_name, line_number, function_name);
1065
1066 return 0;
1067}
1068
1069int debug_rwlock_wrlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name) {
1070 if (debug_should_skip_lock_tracking(rwlock, file_name, function_name)) {
1071 return rwlock_wrlock_impl(rwlock);
1072 }
1073
1074 // Acquire the actual lock first (call implementation to avoid recursion)
1075 int result = rwlock_wrlock_impl(rwlock);
1076 if (result != 0) {
1077 return result;
1078 }
1079
1080 // Create and add lock record
1081 debug_create_and_insert_lock_record(rwlock, LOCK_TYPE_RWLOCK_WRITE, file_name, line_number, function_name);
1082
1083 return 0;
1084}
1085
1086int debug_rwlock_rdunlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name) {
1087 if (debug_should_skip_lock_tracking(rwlock, file_name, function_name)) {
1089 }
1090
1091 // Look for read lock record specifically
1092 uint32_t read_key = lock_record_key(rwlock, LOCK_TYPE_RWLOCK_READ);
1093
1094 if (!debug_process_tracked_unlock(rwlock, read_key, "READ", file_name, line_number, function_name)) {
1095 debug_process_untracked_unlock(rwlock, read_key, "READ", file_name, line_number, function_name);
1096 }
1097
1099}
1100
1101int debug_rwlock_wrunlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name) {
1102 if (debug_should_skip_lock_tracking(rwlock, file_name, function_name)) {
1104 }
1105
1106 // Look for write lock record specifically
1107 uint32_t write_key = lock_record_key(rwlock, LOCK_TYPE_RWLOCK_WRITE);
1108 if (!debug_process_tracked_unlock(rwlock, write_key, "WRITE", file_name, line_number, function_name)) {
1109 debug_process_untracked_unlock(rwlock, write_key, "WRITE", file_name, line_number, function_name);
1110 }
1111
1113}
1114
1115// Removed debug_rwlock_unlock - generic unlock is ambiguous and problematic
1116// Use debug_rwlock_rdunlock or debug_rwlock_wrunlock instead
1117
1118// ============================================================================
1119// Statistics Functions
1120// ============================================================================
1121
1122void lock_debug_get_stats(uint64_t *total_acquired, uint64_t *total_released, uint32_t *currently_held) {
1123 if (total_acquired) {
1124 *total_acquired = atomic_load(&g_lock_debug_manager.total_locks_acquired);
1125 }
1126 if (total_released) {
1127 *total_released = atomic_load(&g_lock_debug_manager.total_locks_released);
1128 }
1129 if (currently_held) {
1130 *currently_held = atomic_load(&g_lock_debug_manager.current_locks_held);
1131 }
1132}
1133
1134bool lock_debug_is_initialized(void) {
1135 bool initialized = atomic_load(&g_lock_debug_manager.initialized);
1136 bool initializing = atomic_load(&g_initializing);
1137 bool result = initialized && !initializing;
1138
1139 return result;
1140}
1141
1142void lock_debug_print_state(void) {
1143 if (!atomic_load(&g_lock_debug_manager.initialized)) {
1144 log_warn("Lock debug system not initialized.");
1145 return;
1146 }
1147
1148 // Acquire read lock for lock_records
1149 rwlock_rdlock_impl(&g_lock_debug_manager.lock_records_lock);
1150
1151 // Read counters atomically while holding the lock to ensure consistency with lock records
1152 uint64_t total_acquired = atomic_load(&g_lock_debug_manager.total_locks_acquired);
1153 uint64_t total_released = atomic_load(&g_lock_debug_manager.total_locks_released);
1154 uint32_t currently_held = atomic_load(&g_lock_debug_manager.current_locks_held);
1155
1156 // Build comprehensive log message with all information
1157 char log_buffer[32768]; // Large buffer for lock details
1158 int offset = 0;
1159
1160 // Collect lock information during iteration
1161 struct lock_collector {
1162 uint32_t count;
1163 char *buffer;
1164 int *offset;
1165 } lock_collector = {0, log_buffer, &offset};
1166
1167 lock_record_t *entry, *tmp;
1168 HASH_ITER(hash_handle, g_lock_debug_manager.lock_records, entry, tmp) {
1169 collect_lock_record_callback(entry, &lock_collector);
1170 }
1171 uint32_t active_locks = lock_collector.count;
1172
1173 rwlock_rdunlock_impl(&g_lock_debug_manager.lock_records_lock);
1174
1175 // Header
1176 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), "=== LOCK DEBUG STATE ===\n");
1177
1178 // Historical Statistics
1179 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), "Historical Statistics:\n");
1180 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1181 " Total locks acquired: %llu\n", (unsigned long long)total_acquired);
1182 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1183 " Total locks released: %llu\n", (unsigned long long)total_released);
1184 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " Currently held: %u\n",
1185 currently_held);
1186
1187 // Check for underflow before subtraction to avoid UB
1188 if (total_acquired >= total_released) {
1189 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1190 " Net locks (acquired - released): %lld\n", (long long)(total_acquired - total_released));
1191 } else {
1192 // This shouldn't happen - means more releases than acquires
1193 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1194 " *** ERROR: More releases (%llu) than acquires (%llu)! Difference: -%lld ***\n",
1195 (unsigned long long)total_released, (unsigned long long)total_acquired,
1196 (long long)(total_released - total_acquired));
1197 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1198 " *** This indicates lock tracking was not enabled for some acquires ***\n");
1199 }
1200
1201 // Currently Active Locks
1202 offset +=
1203 snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), "\n=== Currently Active Locks ===\n");
1204 if (active_locks == 0) {
1205 offset +=
1206 snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " No locks currently held.\n");
1207 // Check for consistency issues
1208 if (currently_held > 0) {
1209 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1210 " *** CONSISTENCY WARNING: Counter shows %u held locks but no records found! ***\n",
1211 currently_held);
1212 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1213 " *** This may indicate a crash during lock acquisition or hash table corruption. ***\n");
1214
1215 // Additional debug: Check hash table statistics
1216 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1217 " *** DEBUG: Hash table stats for lock_records: ***\n");
1218 rwlock_rdlock_impl(&g_lock_debug_manager.lock_records_lock);
1219 size_t count = HASH_CNT(hash_handle, g_lock_debug_manager.lock_records);
1220 rwlock_rdunlock_impl(&g_lock_debug_manager.lock_records_lock);
1221 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1222 " *** Hash table size: %zu ***\n", count);
1223 if (count > 0) {
1224 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1225 " *** Hash table has entries but iteration didn't find them! ***\n");
1226 }
1227 }
1228 } else {
1229 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " Active locks: %u\n",
1230 active_locks);
1231 // Verify consistency the other way
1232 if (active_locks != currently_held) {
1233 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1234 " *** CONSISTENCY WARNING: Found %u active locks but counter shows %u! ***\n", active_locks,
1235 currently_held);
1236 }
1237
1238 // The lock details are already in the buffer from collect_lock_record_callback
1239 // No need to add them again here
1240 }
1241
1242 // Print usage statistics by code location
1243 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1244 "\n=== Lock Usage Statistics by Code Location ===\n");
1245 rwlock_rdlock_impl(&g_lock_debug_manager.usage_stats_lock);
1246
1247 uint32_t total_usage_locations = 0;
1248 lock_usage_stats_t *stats_entry, *stats_tmp;
1249 HASH_ITER(hash_handle, g_lock_debug_manager.usage_stats, stats_entry, stats_tmp) {
1250 print_usage_stats_callback(stats_entry, &total_usage_locations);
1251 }
1252
1253 rwlock_rdunlock_impl(&g_lock_debug_manager.usage_stats_lock);
1254
1255 if (total_usage_locations == 0) {
1256 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1257 " No lock usage statistics available.\n");
1258 } else {
1259 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1260 " Total code locations with lock usage: %u\n", total_usage_locations);
1261 }
1262
1263 // Print orphaned releases (unlocks without corresponding locks)
1264 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1265 "\n=== Orphaned Releases (unlocks without corresponding locks) ===\n");
1266 rwlock_rdlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
1267
1268 uint32_t orphan_count = 0;
1269 lock_record_t *orphan_entry, *orphan_tmp;
1270 HASH_ITER(hash_handle, g_lock_debug_manager.orphaned_releases, orphan_entry, orphan_tmp) {
1271 orphan_count++;
1272
1273 // Get lock type string
1274 const char *lock_type_str;
1275 switch (orphan_entry->lock_type) {
1276 case LOCK_TYPE_MUTEX:
1277 lock_type_str = "MUTEX";
1278 break;
1279 case LOCK_TYPE_RWLOCK_READ:
1280 lock_type_str = "RWLOCK_READ";
1281 break;
1282 case LOCK_TYPE_RWLOCK_WRITE:
1283 lock_type_str = "RWLOCK_WRITE";
1284 break;
1285 default:
1286 lock_type_str = "UNKNOWN";
1287 break;
1288 }
1289
1290 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1291 "Orphaned Release #%u: %s at %p\n", orphan_count, lock_type_str, orphan_entry->lock_address);
1292 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " Thread ID: %llu\n",
1293 (unsigned long long)orphan_entry->thread_id);
1294 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " Released: %s:%d in %s()\n",
1295 orphan_entry->file_name, orphan_entry->line_number, orphan_entry->function_name);
1296 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1297 " Released at: %lld.%09ld seconds (monotonic)\n",
1298 (long long)orphan_entry->acquisition_time.tv_sec, orphan_entry->acquisition_time.tv_nsec);
1299
1300 // Print backtrace for orphaned release
1301 if (orphan_entry->backtrace_size > 0) {
1302 char **symbols = platform_backtrace_symbols(orphan_entry->backtrace_buffer, orphan_entry->backtrace_size);
1303 offset +=
1304 platform_format_backtrace_symbols(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1305 "Release call stack", symbols, orphan_entry->backtrace_size, 0, 0, NULL);
1306 if (symbols) {
1308 }
1309 } else {
1310 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1311 " Release call stack: <capture failed>\n");
1312 }
1313 }
1314
1315 rwlock_rdunlock_impl(&g_lock_debug_manager.orphaned_releases_lock);
1316
1317 if (orphan_count == 0) {
1318 offset +=
1319 snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), " No orphaned releases found.\n");
1320 } else {
1321 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1322 " Total orphaned releases: %u\n", orphan_count);
1323 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1324 " *** WARNING: %u releases without corresponding locks detected! ***\n", orphan_count);
1325 offset += snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset),
1326 " *** This indicates double unlocks or missing lock acquisitions! ***\n");
1327 }
1328
1329 // End marker
1330 offset +=
1331 snprintf(log_buffer + offset, SAFE_BUFFER_SIZE(sizeof(log_buffer), offset), "\n=== End Lock Debug State ===\n");
1332
1333 // Print all at once
1334 log_info("%s", log_buffer);
1335}
1336
1337#endif // DEBUG_LOCKS - Without DEBUG_LOCKS: no implementation, header provides inline stubs
🔌 Cross-platform abstraction layer umbrella header for ascii-chat
⚠️‼️ Error and/or exit() when things go bad.
#️⃣ FNV-1a Hash Function Implementation
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_CALLOC(count, size, cast)
Definition common.h:218
#define SAFE_BUFFER_SIZE(buffer_size, offset)
Safe buffer size calculation for snprintf.
Definition common.h:421
unsigned long long uint64_t
Definition common.h:59
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
@ ERROR_INVALID_STATE
@ ERROR_PLATFORM_INIT
Definition error_codes.h:57
@ ERROR_THREAD
Definition error_codes.h:95
uint64_t asciichat_thread_current_id(void)
#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.
#define log_info(...)
Log an INFO message.
#define log_debug(...)
Log a DEBUG message.
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
int format_duration_ns(double nanoseconds, char *buffer, size_t buffer_size)
Format nanoseconds as human-readable duration string.
Definition time.c:187
int format_duration_ms(double milliseconds, char *buffer, size_t buffer_size)
Format milliseconds as human-readable duration string.
Definition time.c:269
int mutex_lock_impl(mutex_t *mutex)
Lock a mutex (implementation function)
int debug_rwlock_rdlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name)
int debug_rwlock_wrlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name)
int platform_backtrace(void **buffer, int size)
Get a backtrace of the current call stack.
int rwlock_rdunlock_impl(rwlock_t *lock)
Release a read lock (implementation function)
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe version of snprintf that ensures null termination.
void platform_print_backtrace_symbols(const char *label, char **symbols, int count, int skip_frames, int max_frames, backtrace_frame_filter_t filter)
Print pre-resolved backtrace symbols with consistent formatting.
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
int debug_mutex_unlock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name)
int debug_mutex_lock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name)
pthread_mutex_t mutex_t
Mutex type (POSIX: pthread_mutex_t)
Definition mutex.h:38
int rwlock_rdlock_impl(rwlock_t *lock)
Acquire a read lock (implementation function)
int debug_rwlock_wrunlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name)
int mutex_trylock_impl(mutex_t *mutex)
Try to lock a mutex without blocking (implementation function)
int rwlock_wrunlock_impl(rwlock_t *lock)
Release a write lock (implementation function)
#define UNUSED(x)
Suppress unused parameter warnings.
int rwlock_wrlock_impl(rwlock_t *lock)
Acquire a write lock (implementation function)
void platform_backtrace_symbols_free(char **strings)
Free symbol array returned by platform_backtrace_symbols()
void platform_print_backtrace(int skip_frames)
Print a backtrace of the current call stack.
pthread_rwlock_t rwlock_t
Read-write lock type (POSIX: pthread_rwlock_t)
Definition rwlock.h:40
bool lock_debug_is_initialized(void)
int debug_mutex_trylock(mutex_t *mutex, const char *file_name, int line_number, const char *function_name)
int platform_format_backtrace_symbols(char *buffer, size_t buffer_size, const char *label, char **symbols, int count, int skip_frames, int max_frames, backtrace_frame_filter_t filter)
Format pre-resolved backtrace symbols to a buffer.
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.
char ** platform_backtrace_symbols(void *const *buffer, int size)
Convert backtrace addresses to symbol names.
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Wait for a thread to complete (blocking)
int debug_rwlock_rdunlock(rwlock_t *rwlock, const char *file_name, int line_number, const char *function_name)
int rwlock_init(rwlock_t *lock)
Initialize a read-write lock.
int asciichat_thread_create(asciichat_thread_t *thread, void *(*func)(void *), void *arg)
Create a new thread.
int mutex_unlock_impl(mutex_t *mutex)
Unlock a mutex (implementation function)
🔒 Lock debugging and deadlock detection system for ascii-chat
bool initialized
Definition mmap.c:36
rwlock_t rwlock
Read-write lock for thread-safe access (uthash requires external locking)
Definition time.c:28
⏱️ High-precision timing utilities using sokol_time.h and uthash
#️⃣ Wrapper for uthash.h that ensures common.h is included first