ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
debug/mutex.c
Go to the documentation of this file.
1
13#include <ascii-chat/log/log.h>
21#include <string.h>
22
23// ============================================================================
24// Per-thread Lock Stack Storage
25// ============================================================================
26
27#define MUTEX_STACK_MAX_DEPTH 64
28
34
35static void stack_lock(thread_lock_stack_t *stack) {
36 int expected = 0;
37 while (!atomic_cas_int(&stack->guard, &expected, 1))
38 expected = 0;
39}
40
41static void stack_unlock(thread_lock_stack_t *stack) {
42 atomic_store_int(&stack->guard, 0);
43}
44
45// Global registry of all threads that have used mutexes
46#define MAX_THREADS 256
47typedef struct {
49 thread_lock_stack_t *stack; // Heap-allocated stack per thread
51
52static thread_registry_entry_t g_thread_registry[MAX_THREADS] = {0};
53static atomic_t g_thread_registry_count = {0};
54static atomic_t g_registry_guard = {0};
55
56static void registry_lock(void) {
57 int expected = 0;
58 while (!atomic_cas_int(&g_registry_guard, &expected, 1))
59 expected = 0;
60}
61
62static void registry_unlock(void) {
63 atomic_store_int(&g_registry_guard, 0);
64}
65
66// Thread-local storage key for per-thread lock stack
67// Destructor automatically frees memory when thread exits
68static tls_key_t g_tls_mutex_stack = 0;
69static atomic_t g_tls_initialized = {0};
70
71// Flag set during shutdown to prevent new stack allocations
72// (threads still running at shutdown time won't leak new stacks)
73static atomic_t g_shutting_down = {0};
74
75// Track the mutexes involved in the last detected deadlock for throttling
76#define MAX_CYCLE_MUTEXES 16
77typedef struct {
78 uintptr_t mutexes[MAX_CYCLE_MUTEXES];
79 int count;
81static deadlock_state_t g_last_deadlock = {0};
82
83// ============================================================================
84// Thread Registry Management
85// ============================================================================
86
97static void tls_mutex_stack_destructor(void *arg) {
98 if (!arg) {
99 return;
100 }
101
102 // Check if this stack was already freed by cleanup()
103 // This happens if: (1) thread is in registry AND (2) registry entry matches this stack
104 bool already_freed_by_cleanup = false;
105 thread_id_t current_thread = asciichat_thread_self();
106 registry_lock();
107 int count = atomic_load_int(&g_thread_registry_count);
108 for (int i = 0; i < count; i++) {
109 if (asciichat_thread_equal(g_thread_registry[i].thread_id, current_thread)) {
110 // Found this thread's entry in registry
111 if (g_thread_registry[i].stack == arg) {
112 // Registry entry matches this stack - cleanup() already freed it
113 already_freed_by_cleanup = true;
114 // Clear the entry (optional, cleanup might have done this already)
115 g_thread_registry[i].stack = NULL;
116 }
117 break;
118 }
119 }
120
121 // Free if cleanup() didn't already do it
122 // This includes "orphaned" stacks that were allocated after cleanup() ran
123 // but before the thread's destructor was called
124 if (!already_freed_by_cleanup) {
125 // Use raw free() - stacks are allocated with raw malloc(), not SAFE_CALLOC()
126 // This avoids recursive mutex allocation during destructor execution
127 free(arg);
128 }
129 registry_unlock();
130}
131
136static void ensure_tls_initialized(void) {
137 if (!atomic_load_bool(&g_tls_initialized)) {
138 // Try to initialize TLS key with destructor
139 if (ascii_tls_key_create(&g_tls_mutex_stack, tls_mutex_stack_destructor) == 0) {
140 atomic_store_bool(&g_tls_initialized, true);
141 }
142 }
143}
144
145// Forward declaration
146static void register_thread_if_needed(void);
147
153static thread_lock_stack_t *get_thread_local_stack(void) {
154 // Skip allocation during shutdown (prevents leaks from threads killed mid-shutdown)
155 if (atomic_load_bool(&g_shutting_down)) {
156 return NULL;
157 }
158
159 // Ensure TLS key is initialized
160 ensure_tls_initialized();
161
162 // Get current thread's stack from TLS
163 thread_lock_stack_t *stack = (thread_lock_stack_t *)ascii_tls_get(g_tls_mutex_stack);
164
165 if (stack == NULL) {
166 // Allocate stack on heap for this thread
167 // Use raw malloc() to avoid recursive mutex locking in debug subsystem
168 stack = (thread_lock_stack_t *)malloc(sizeof(thread_lock_stack_t));
169 if (stack) {
170 memset(stack, 0, sizeof(thread_lock_stack_t));
171 // Store in TLS (destructor will free it when thread exits)
172 ascii_tls_set(g_tls_mutex_stack, stack);
173 register_thread_if_needed();
174 }
175 }
176 return stack;
177}
178
187static void register_thread_if_needed(void) {
188 static __thread bool registered = false;
189
190 // Once registered, skip immediately
191 if (registered) {
192 return;
193 }
194
195 thread_lock_stack_t *local_stack = get_thread_local_stack();
196 if (!local_stack) {
197 return;
198 }
199
200 thread_id_t current_thread = asciichat_thread_self();
201
202 // Check if thread is already in registry (without lock)
203 int current_count = atomic_load_int(&g_thread_registry_count);
204 for (int i = 0; i < current_count; i++) {
205 if (asciichat_thread_equal(g_thread_registry[i].thread_id, current_thread)) {
206 registered = true;
207 return; // Already registered
208 }
209 }
210
211 // Try to claim a slot in the registry atomically
212 int slot;
213 while (1) {
214 int old_count = atomic_load_int(&g_thread_registry_count);
215 if (old_count >= MAX_THREADS) {
216 registered = true; // Registry is full, give up
217 return;
218 }
219
220 // Try to atomically increment the count and claim a slot
221 if (atomic_cas_int(&g_thread_registry_count, &old_count, old_count + 1)) {
222 slot = old_count;
223 break; // Successfully claimed slot
224 }
225 // If compare-exchange failed, loop and try again
226 }
227
228 // Write thread data to claimed slot (release semantics for visibility)
229 registry_lock();
230 g_thread_registry[slot].thread_id = current_thread;
231 g_thread_registry[slot].stack = local_stack;
232 registry_unlock();
233
234 // Memory barrier to ensure writes are visible to other threads
235 // Memory ordering is implicit in atomic operations
236
237 registered = true;
238}
239
240// ============================================================================
241// Public Stack Operations (Debug builds only)
242// ============================================================================
243
244#ifndef NDEBUG
245
246void mutex_stack_push_pending(uintptr_t mutex_key, const char *mutex_name) {
247 thread_lock_stack_t *stack = get_thread_local_stack();
248 if (!stack) {
249 return;
250 }
251
252 // Register this thread in the global registry on first mutex use
253 register_thread_if_needed();
254
255 stack_lock(stack);
256 if (stack->depth >= MUTEX_STACK_MAX_DEPTH) {
257 stack_unlock(stack);
258 return;
259 }
260 stack->stack[stack->depth].mutex_key = mutex_key;
261 stack->stack[stack->depth].mutex_name = mutex_name;
263 stack->stack[stack->depth].timestamp_ns = time_get_ns();
264 stack->depth++;
265 stack_unlock(stack);
266}
267
268void mutex_stack_mark_locked(uintptr_t mutex_key) {
269 thread_lock_stack_t *stack = get_thread_local_stack();
270 if (!stack) {
271 return;
272 }
273
274 // Mark the top of the stack as locked
275 stack_lock(stack);
276 if (stack->depth == 0) {
277 stack_unlock(stack);
278 return;
279 }
280 int top = stack->depth - 1;
281 if (stack->stack[top].mutex_key == mutex_key) {
283 stack->stack[top].timestamp_ns = time_get_ns();
284 }
285 stack_unlock(stack);
286
287 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
288}
289
290void mutex_stack_pop(uintptr_t mutex_key) {
291 thread_lock_stack_t *stack = get_thread_local_stack();
292 if (!stack) {
293 return;
294 }
295
296 // Validate top matches
297 stack_lock(stack);
298 if (stack->depth == 0) {
299 stack_unlock(stack);
300 return;
301 }
302 int top = stack->depth - 1;
303 if (stack->stack[top].mutex_key == mutex_key) {
304 stack->depth--;
305 }
306 stack_unlock(stack);
307
308 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
309}
310
311#endif
312
313int mutex_stack_get_current(mutex_stack_entry_t *out_entries, int max_entries) {
314 thread_lock_stack_t *stack = get_thread_local_stack();
315 if (!stack || !out_entries) {
316 return 0;
317 }
318
319 stack_lock(stack);
320 int depth = stack->depth;
321 int count = (depth < max_entries) ? depth : max_entries;
322 memcpy(out_entries, stack->stack, count * sizeof(mutex_stack_entry_t));
323 stack_unlock(stack);
324 return depth; // Return actual depth even if truncated
325}
326
327// ============================================================================
328// Global Thread Stack Access
329// ============================================================================
330
331int mutex_stack_get_all_threads(mutex_stack_entry_t ***out_stacks, int **out_stack_counts, int *out_thread_count) {
332
333 if (!out_stacks || !out_stack_counts || !out_thread_count) {
334 return -1;
335 }
336
337 // Read the thread count with acquire semantics (lock-free, no mutex needed)
338 // Use memory_order_seq_cst to ensure we get a consistent snapshot
339 int thread_count = atomic_load_int(&g_thread_registry_count);
340
341 // Allocate arrays for threads in the registry
342 // Note: SAFE_MALLOC takes bytes as first parameter, not count
343 *out_stacks = SAFE_CALLOC(thread_count, sizeof(mutex_stack_entry_t *), mutex_stack_entry_t **);
344 *out_stack_counts = SAFE_CALLOC(thread_count, sizeof(int), int *);
345
346 if (!*out_stacks || !*out_stack_counts) {
347 SAFE_FREE(*out_stacks);
348 SAFE_FREE(*out_stack_counts);
349 return -1;
350 }
351
352 // Copy each thread's stack from the registry
353 // Note: the thread count can change concurrently, so we re-read it in the loop
354 // to avoid accessing out-of-bounds memory if threads exit during iteration
355 for (int i = 0; i < thread_count; i++) {
356 // Re-check thread count in case registry shrank
357 int current_registry_count = atomic_load_int(&g_thread_registry_count);
358 if (i >= current_registry_count) {
359 break;
360 }
361
362 // Allocate before locking because tracked allocation updates the lock stack.
364 (*out_stack_counts)[i] = 0;
365 registry_lock();
366 thread_lock_stack_t *src = g_thread_registry[i].stack;
367 if (src) {
368 stack_lock(src);
369 int depth = src->depth;
370 (*out_stack_counts)[i] = depth;
371 memcpy((*out_stacks)[i], src->stack, depth * sizeof(mutex_stack_entry_t));
372 stack_unlock(src);
373 }
374 registry_unlock();
375 }
376
377 *out_thread_count = thread_count;
378 return 0;
379}
380
381void mutex_stack_free_all_threads(mutex_stack_entry_t **stacks, int *stack_counts, int thread_count) {
382
383 if (!stacks || !stack_counts)
384 return;
385
386 for (int i = 0; i < thread_count; i++) {
387 SAFE_FREE(stacks[i]);
388 }
389
390 SAFE_FREE(stacks);
391 SAFE_FREE(stack_counts);
392}
393
394// ============================================================================
395// Deadlock Detection
396// ============================================================================
397
401static bool thread_holds_mutex(thread_lock_stack_t *stack, uintptr_t mutex_key) {
402 if (!stack) {
403 return false;
404 }
405 for (int i = 0; i < stack->depth; i++) {
406 if (stack->stack[i].mutex_key == mutex_key && stack->stack[i].state == MUTEX_STACK_STATE_LOCKED) {
407 return true;
408 }
409 }
410 return false;
411}
412
416static uintptr_t thread_waiting_for_mutex(thread_lock_stack_t *stack) {
417 if (!stack) {
418 return 0;
419 }
420 if (stack->depth > 0) {
421 int top = stack->depth - 1;
422 if (stack->stack[top].state == MUTEX_STACK_STATE_PENDING) {
423 return stack->stack[top].mutex_key;
424 }
425 }
426 return 0;
427}
428
432static int find_thread_holding_mutex(thread_lock_stack_t *snapshots, int thread_count, uintptr_t mutex_key) {
433 for (int i = 0; i < thread_count; i++) {
434 thread_lock_stack_t *stack = &snapshots[i];
435 if (thread_holds_mutex(stack, mutex_key)) {
436 return i;
437 }
438 }
439 return -1;
440}
441
447#define MAX_CYCLE_LEN 64
448static int detect_cycle_dfs(thread_lock_stack_t *snapshots, int thread_count, int start_thread, int *cycle_path,
449 int *cycle_len) {
450 int visited[MAX_THREADS];
451 int path[MAX_CYCLE_LEN];
452 int path_len = 0;
453
454 // Initialize visited array
455 for (int i = 0; i < MAX_THREADS; i++) {
456 visited[i] = -1; // -1 = not visited, >= 0 = index in path
457 }
458
459 // DFS starting from start_thread
460 int current = start_thread;
461 while (path_len < MAX_CYCLE_LEN && path_len < thread_count) {
462 if (current < 0 || current >= thread_count) {
463 break; // Invalid thread
464 }
465
466 // Check if current is already in path (cycle found!)
467 for (int i = 0; i < path_len; i++) {
468 if (path[i] == current) {
469 // Found a cycle! Extract the cycle portion
470 *cycle_len = path_len - i;
471 for (int j = 0; j < *cycle_len; j++) {
472 cycle_path[j] = path[i + j];
473 }
474 return i; // Return where the cycle starts
475 }
476 }
477
478 // Add current to path
479 path[path_len++] = current;
480
481 // Find next thread in the waits-for graph
482 thread_lock_stack_t *stack = &snapshots[current];
483 uintptr_t waiting_for = thread_waiting_for_mutex(stack);
484
485 if (waiting_for == 0) {
486 break; // No waiting, path ends
487 }
488
489 // Find who holds the mutex we're waiting for
490 current = find_thread_holding_mutex(snapshots, thread_count, waiting_for);
491 }
492
493 *cycle_len = 0;
494 return -1; // No cycle found
495}
496
500static int compare_uintptr(const void *a, const void *b) {
501 uintptr_t ua = *(const uintptr_t *)a;
502 uintptr_t ub = *(const uintptr_t *)b;
503 if (ua < ub)
504 return -1;
505 if (ua > ub)
506 return 1;
507 return 0;
508}
509
514static bool deadlock_mutexes_changed(const uintptr_t *current_mutexes, int count) {
515 if (count != g_last_deadlock.count)
516 return true;
517
518 // Sort both arrays for order-independent comparison
519 uintptr_t current_sorted[MAX_CYCLE_MUTEXES];
520 uintptr_t last_sorted[MAX_CYCLE_MUTEXES];
521
522 for (int i = 0; i < count; i++) {
523 current_sorted[i] = current_mutexes[i];
524 last_sorted[i] = g_last_deadlock.mutexes[i];
525 }
526
527 qsort(current_sorted, count, sizeof(uintptr_t), compare_uintptr);
528 qsort(last_sorted, count, sizeof(uintptr_t), compare_uintptr);
529
530 for (int i = 0; i < count; i++) {
531 if (current_sorted[i] != last_sorted[i])
532 return true;
533 }
534 return false;
535}
536
540static void update_deadlock_state(const uintptr_t *mutexes, int count) {
541 g_last_deadlock.count = (count < MAX_CYCLE_MUTEXES) ? count : MAX_CYCLE_MUTEXES;
542 for (int i = 0; i < g_last_deadlock.count; i++) {
543 g_last_deadlock.mutexes[i] = mutexes[i];
544 }
545}
546
560 mutex_stack_entry_t **all_stacks = NULL;
561 int *stack_counts = NULL;
562 int thread_count = 0;
563
564 if (mutex_stack_get_all_threads(&all_stacks, &stack_counts, &thread_count) != 0) {
565 return;
566 }
567
568 // Analyze copies instead of stacks changing concurrently on other threads.
569 thread_lock_stack_t *snapshots = SAFE_CALLOC(thread_count, sizeof(thread_lock_stack_t), thread_lock_stack_t *);
570 for (int i = 0; i < thread_count; i++) {
571 snapshots[i].depth = stack_counts[i];
572 if (stack_counts[i] > 0)
573 memcpy(snapshots[i].stack, all_stacks[i], stack_counts[i] * sizeof(mutex_stack_entry_t));
574 }
575
576 // Check each thread for deadlock conditions
577 for (int i = 0; i < thread_count; i++) {
578 thread_lock_stack_t *stack_a = &snapshots[i];
579 uintptr_t waiting_for = thread_waiting_for_mutex(stack_a);
580
581 if (waiting_for == 0)
582 continue; // Thread not waiting
583
584 // Same-thread deadlock: thread trying to acquire a mutex it already holds
585 if (thread_holds_mutex(stack_a, waiting_for)) {
586 log_error("%s", colored_string(LOG_COLOR_ERROR, "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"));
587 log_error("%s", colored_string(LOG_COLOR_ERROR, "โ•‘ โš ๏ธ DEADLOCK DETECTED: Same-thread Recursive Lock โš ๏ธ โ•‘"));
588 log_error("%s", colored_string(LOG_COLOR_ERROR, "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"));
589 log_error(" Thread Address: 0x%lx", (unsigned long)g_thread_registry[i].thread_id);
590 log_error(" Mutex: 0x%lx", waiting_for);
591 log_error(" Issue: Thread attempts recursive lock on non-recursive mutex");
592 continue;
593 }
594
595 // Multi-thread circular wait: use DFS to detect cycles
596 int cycle_path[MAX_CYCLE_LEN];
597 int cycle_len = 0;
598 int cycle_start = detect_cycle_dfs(snapshots, thread_count, i, cycle_path, &cycle_len);
599
600 if (cycle_start >= 0 && cycle_len > 1) {
601 // Collect mutexes involved in this deadlock
602 uintptr_t cycle_mutexes[MAX_CYCLE_MUTEXES];
603 int mutex_count = 0;
604 for (int k = 0; k < cycle_len && mutex_count < MAX_CYCLE_MUTEXES; k++) {
605 int thread_idx = cycle_path[k];
606 thread_lock_stack_t *stack = &snapshots[thread_idx];
607 uintptr_t waiting_for = thread_waiting_for_mutex(stack);
608 if (waiting_for != 0) {
609 cycle_mutexes[mutex_count++] = waiting_for;
610 }
611 }
612
613 // Check if mutexes are different from last deadlock
614 bool is_new_deadlock = deadlock_mutexes_changed(cycle_mutexes, mutex_count);
615 if (is_new_deadlock) {
616 update_deadlock_state(cycle_mutexes, mutex_count);
617 }
618
619 // Cycle detected! Build complete message in one string
620 char cycle_msg[4096];
621 int msg_len = 0;
622
623 // Leading newline and header box
624 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "\n%s\n",
625 colored_string(LOG_COLOR_ERROR, "โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—"));
626 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
627 colored_string(LOG_COLOR_ERROR, "โ•‘ DEADLOCK: Circular Wait Cycle โ•‘"));
628 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
629 colored_string(LOG_COLOR_ERROR, "โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•"));
630
631 // Print each thread in the cycle
632 for (int k = 0; k < cycle_len; k++) {
633 int thread_idx = cycle_path[k];
634 int next_thread_idx = cycle_path[(k + 1) % cycle_len];
635
636 thread_lock_stack_t *current_stack = &snapshots[thread_idx];
637 uintptr_t current_waiting = thread_waiting_for_mutex(current_stack);
638
639 char thread_name[256], mutex_name[256], held_by_name[256];
640 NAMED_GET_BY_PTR((uintptr_t)g_thread_registry[thread_idx].thread_id, thread_name, sizeof(thread_name));
641 NAMED_GET_BY_PTR((uintptr_t)current_waiting, mutex_name, sizeof(mutex_name));
642 NAMED_GET_BY_PTR((uintptr_t)g_thread_registry[next_thread_idx].thread_id, held_by_name, sizeof(held_by_name));
643
644 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, " T%d: %s waits for %s (held by %s)%s",
645 k + 1, thread_name, mutex_name, held_by_name, k < cycle_len - 1 ? "\n" : "");
646 }
647
648 // Log repeated deadlock detections (skip first call, throttle subsequent ones)
649 if (!is_new_deadlock) {
650 log_error_every(1000000, "%s", cycle_msg); // 1000000 ยตs = 1 second
651 }
652 }
653 }
654
655 SAFE_FREE(snapshots);
656 mutex_stack_free_all_threads(all_stacks, stack_counts, thread_count);
657}
658
659// ============================================================================
660// Condition Variable Deadlock Detection (Debug builds only)
661// ============================================================================
662
663#ifndef NDEBUG
664
665#define COND_DEADLOCK_THRESHOLD_NS (5ULL * 1000000000ULL) // 5 seconds
666
673static void cond_deadlock_check_callback(uintptr_t key, const char *name, void *user_data) {
674 (void)user_data; // Unused
675
676 const char *type = named_get_type(key);
677 if (!type || strcmp(type, "cond") != 0) {
678 return;
679 }
680
681 const cond_t *cond = (const cond_t *)key;
682 if (atomic_load_u64(&cond->waiting_count) == 0) {
683 return; // No threads waiting, nothing to check
684 }
685
686 // Skip deadlock checks for thread pool work queues - it's normal for worker threads
687 // to wait idly when there's no work to process
688 if (name && strstr(name, "task_available") != NULL) {
689 return;
690 }
691
692 uint64_t now = time_get_ns();
693 uint64_t stuck_ns = now - cond->last_wait_time_ns;
694 bool no_signal_since_wait = (cond->last_signal_time_ns == 0 || cond->last_signal_time_ns < cond->last_wait_time_ns);
695
696 if (stuck_ns < COND_DEADLOCK_THRESHOLD_NS || !no_signal_since_wait) {
697 return; // Not stuck yet or was signaled recently
698 }
699
700 // Condition variable appears to be stuck - log detailed diagnostic info
701 char stuck_str[64];
702 time_pretty(stuck_ns, -1, stuck_str, sizeof(stuck_str));
703
704 char cond_buf[1024] = {0};
705 int cond_written =
706 safe_snprintf(cond_buf, sizeof(cond_buf),
707 "Stuck cond '%s': %lu thread(s) waiting %s with no signal (most recent waiter: 0x%lx)\n", name,
708 cond->waiting_count, stuck_str, (unsigned long)cond->last_waiting_key);
709
710 cond_written +=
711 safe_snprintf(cond_buf + cond_written, sizeof(cond_buf) - cond_written, " wait entered at %s:%d %s()\n",
713
714 if (cond_written >= 0 && cond->last_wait_mutex) {
715 safe_snprintf(cond_buf + cond_written, sizeof(cond_buf) - cond_written,
716 " associated mutex: %p (cannot safely inspect without lock ownership)",
717 (void *)cond->last_wait_mutex);
718 }
719
720 log_warn_every(500 * NS_PER_MS_INT, "%s", cond_buf);
721}
722
723// Forward declare function from sync.c to check cleanup status
724extern bool debug_sync_is_cleanup_in_progress(void);
725
739 return;
740 }
741 named_registry_for_each(cond_deadlock_check_callback, NULL);
742}
743
744#endif // NDEBUG
745
746// ============================================================================
747// Initialization
748// ============================================================================
749
751 // No initialization needed - registry uses lock-free atomic operations
752 return 0;
753}
754
756 // Explicitly free the current thread's TLS stack
757 // This is used to prevent leaks when TLS destructors might not run reliably
758 // (e.g., debug threads exiting before mutex_stack_cleanup() deletes the TLS key)
759
760 if (!atomic_load_bool(&g_tls_initialized)) {
761 return; // TLS not initialized, nothing to clean up
762 }
763
764 thread_lock_stack_t *stack = (thread_lock_stack_t *)ascii_tls_get(g_tls_mutex_stack);
765 if (!stack) {
766 return; // No stack allocated for this thread
767 }
768
769 // Clear from TLS
770 ascii_tls_set(g_tls_mutex_stack, NULL);
771
772 // Update registry if this thread is registered
773 thread_id_t current_thread = asciichat_thread_self();
774 registry_lock();
775 int count = atomic_load_int(&g_thread_registry_count);
776 for (int i = 0; i < count; i++) {
777 if (asciichat_thread_equal(g_thread_registry[i].thread_id, current_thread) && g_thread_registry[i].stack == stack) {
778 g_thread_registry[i].stack = NULL; // Mark as freed in registry
779 break;
780 }
781 }
782
783 // Free the stack (raw free to match raw malloc above)
784 free(stack);
785 registry_unlock();
786}
787
789 // Signal shutdown to prevent new allocations from threads still running
790 atomic_store_bool(&g_shutting_down, true);
791
792 // Manually free all stacks in the registry
793 // This must happen BEFORE deleting the TLS key to avoid double-free
794 // (destructor won't run on still-running threads until they exit, by which time
795 // these stacks will already be freed)
796 int count = atomic_load_int(&g_thread_registry_count);
797 for (int i = 0; i < count; i++) {
798 if (g_thread_registry[i].stack) {
799 // Use raw free() - stacks are allocated with raw malloc(), not SAFE_CALLOC()
800 free(g_thread_registry[i].stack);
801 g_thread_registry[i].stack = NULL;
802 }
803 }
804
805 // Now delete the TLS key - destructors will see NULL in registry and skip freeing
806 // For any threads that haven't been registered yet, destructor will free directly
807 if (atomic_load_bool(&g_tls_initialized)) {
808 ascii_tls_key_delete(g_tls_mutex_stack);
809 atomic_store_bool(&g_tls_initialized, false);
810 }
811
812 // Clear registry on cleanup using atomic operations
813 atomic_store_int(&g_thread_registry_count, 0);
814}
815
816// ============================================================================
817// Release Build Stubs (NDEBUG)
818// ============================================================================
819// These no-op implementations replace the debug implementations in release builds
820// (when debug/mutex.c is not compiled into the library)
821
822#ifdef NDEBUG
823
824void mutex_stack_push_pending(uintptr_t mutex_key, const char *mutex_name) {
825 (void)mutex_key;
826 (void)mutex_name;
827 // No-op in release builds
828}
829
830void mutex_stack_mark_locked(uintptr_t mutex_key) {
831 (void)mutex_key;
832 // No-op in release builds
833}
834
835void mutex_stack_pop(uintptr_t mutex_key) {
836 (void)mutex_key;
837 // No-op in release builds
838}
839
840#endif
DLL export/import macros for cross-platform symbol visibility.
int thread_id
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_int(atomic_t *a, int value)
Atomically store an int value.
Definition atomic.c:202
bool atomic_cas_int(atomic_t *a, int *expected, int new_value)
Atomically compare-and-swap an int.
Definition atomic.c:225
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
int atomic_load_int(atomic_t *a)
Atomically load an int value.
Definition atomic.c:194
Cross-platform condition variable interface for ascii-chat.
bool debug_sync_is_cleanup_in_progress(void)
Definition sync.c:653
#define MAX_CYCLE_MUTEXES
Definition debug/mutex.c:76
void mutex_stack_cleanup_current_thread(void)
Cleanup TLS stack for current thread Explicitly frees the thread-local mutex stack....
int mutex_stack_get_all_threads(mutex_stack_entry_t ***out_stacks, int **out_stack_counts, int *out_thread_count)
Get all threads' lock stacks for deadlock analysis.
void mutex_stack_free_all_threads(mutex_stack_entry_t **stacks, int *stack_counts, int thread_count)
Free memory allocated by mutex_stack_get_all_threads()
#define COND_DEADLOCK_THRESHOLD_NS
#define MUTEX_STACK_MAX_DEPTH
Definition debug/mutex.c:27
void mutex_stack_cleanup(void)
Cleanup mutex stack system.
int mutex_stack_get_current(mutex_stack_entry_t *out_entries, int max_entries)
Get the current thread's lock stack.
void mutex_stack_pop(uintptr_t mutex_key)
Pop the top mutex from the current thread's lock stack (unlock)
#define MAX_CYCLE_LEN
DFS-based cycle detection in the waits-for graph Returns cycle start index if found,...
#define MAX_THREADS
Definition debug/mutex.c:46
void mutex_stack_push_pending(uintptr_t mutex_key, const char *mutex_name)
Push a mutex onto the current thread's lock stack (PENDING state)
void mutex_stack_mark_locked(uintptr_t mutex_key)
Mark the top of the current thread's lock stack as LOCKED (transitions PENDING -> LOCKED)
int mutex_stack_init(void)
Initialize mutex stack system.
void mutex_stack_detect_deadlocks(void)
Detect circular wait deadlocks using DFS-based cycle detection.
Per-thread mutex lock stack for deadlock detection.
@ MUTEX_STACK_STATE_LOCKED
Definition debug/mutex.h:24
@ MUTEX_STACK_STATE_PENDING
Definition debug/mutex.h:23
Named object registry for debugging โ€” log identifiable resource names.
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
void named_registry_for_each(named_iter_callback_t callback, void *user_data)
Iterate through all registered entries.
#define NAMED_GET_BY_PTR(key, buffer, size)
Get name of a pointer/key or format as address fallback.
const char * named_get_type(uintptr_t key)
Look up the registered type for a resource.
void debug_sync_check_cond_deadlocks(void)
Check all condition variables for deadlocks.
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
@ LOG_COLOR_ERROR
Definition log/log.h:135
uint64_t time_get_ns(void)
Get current monotonic time in nanoseconds.
Definition util/time.c:108
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as pretty duration with spaces and configurable precision.
Definition util/time.c:424
#define NS_PER_MS_INT
Definition time.h:156
int ascii_tls_key_create(tls_key_t *key, void(*destructor)(void *))
Create a thread-local storage key.
Definition threading.c:97
int ascii_tls_set(tls_key_t key, void *value)
Set thread-local value for a key.
Definition threading.c:109
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
void * ascii_tls_get(tls_key_t key)
Get thread-local value for a key.
Definition threading.c:105
int ascii_tls_key_delete(tls_key_t key)
Delete a thread-local storage key.
Definition threading.c:101
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:456
๐Ÿงต Cross-platform thread interface for ascii-chat
#define asciichat_thread_self()
#define asciichat_thread_equal(t1, t2)
๐Ÿ“ Logging API with multiple log levels and terminal output control
#define log_error_every(interval_us, fmt,...)
Rate-limited ERROR logging.
Definition log/log.h:711
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:708
๐Ÿ“‚ Path Manipulation Utilities
Cross-platform mutex interface for ascii-chat.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63
uint64_t last_wait_time_ns
Timestamp of last wait (nanoseconds)
Definition cond.h:70
mutex_t * last_wait_mutex
Associated mutex at most recent wait (for deadlock detection)
Definition cond.h:72
uint64_t last_signal_time_ns
Timestamp of last signal (nanoseconds)
Definition cond.h:68
uintptr_t last_waiting_key
Registry key of most recent waiter.
Definition cond.h:71
atomic_t waiting_count
Number of threads currently waiting (functional - needed for all builds)
Definition cond.h:66
const char * last_wait_file
Callsite file of most recent cond_wait (for deadlock detection)
Definition cond.h:73
int last_wait_line
Callsite line of most recent cond_wait (for deadlock detection)
Definition cond.h:74
const char * last_wait_func
Callsite function of most recent cond_wait (for deadlock detection)
Definition cond.h:75
uintptr_t mutexes[16]
Definition debug/mutex.c:78
Definition debug/mutex.h:30
const char * mutex_name
Definition debug/mutex.h:32
mutex_stack_state_t state
Definition debug/mutex.h:33
uint64_t timestamp_ns
Definition debug/mutex.h:34
uintptr_t mutex_key
Definition debug/mutex.h:31
mutex_stack_entry_t stack[64]
Definition debug/mutex.c:30
Definition debug/mutex.c:47
thread_lock_stack_t * stack
Definition debug/mutex.c:49
thread_id_t thread_id
Definition debug/mutex.c:48
โฑ๏ธ High-precision timing utilities using sokol_time.h and uthash
๐Ÿ”ค String Manipulation and Shell Escaping Utilities