ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
mutex.c
Go to the documentation of this file.
1
2/* Storage capacities in elements; text buffers include the terminating NUL. */
3#define DEBUG_MUTEX_CYCLE_MSG_BUFFER_SIZE 4096
4#define DEBUG_MUTEX_THREAD_NAME_BUFFER_SIZE 256
5#define DEBUG_MUTEX_MUTEX_NAME_BUFFER_SIZE 256
6#define DEBUG_MUTEX_HELD_BY_NAME_BUFFER_SIZE 256
7#define DEBUG_MUTEX_STUCK_BUFFER_SIZE 64
8#define DEBUG_MUTEX_COND_BUFFER_SIZE 1024
9#define DEBUG_MUTEX_NAME_BUFFER_SIZE 256
10
21
24#include <ascii-chat/log/log.h>
28#include <ascii-chat/source_root.h>
29#include <ascii-chat/os/api.h>
30#include <libplatform/mutex.h>
31#include <libplatform/cond.h>
32#include <libplatform/thread.h>
33#include <string.h>
34#include <ascii-chat/otel.h>
35
36// ============================================================================
37// Per-thread Lock Stack Storage
38// ============================================================================
39
40#define MUTEX_STACK_MAX_DEPTH 64
41
50
51static void stack_lock(thread_lock_stack_t *stack) {
52 int expected = 0;
53 while (!atomic_cas_int(&stack->guard, &expected, 1))
54 expected = 0;
55}
56
57static void stack_unlock(thread_lock_stack_t *stack) {
58 atomic_store_int(&stack->guard, 0);
59}
60
61// Global registry of all threads that have used mutexes
62#define MAX_THREADS 256
68typedef struct {
69 thread_id_t thread_id;
70 thread_lock_stack_t *stack; // Heap-allocated stack per thread
74
75static thread_registry_entry_t g_thread_registry[MAX_THREADS] = {0};
76static atomic_t g_thread_registry_count = {0};
77static atomic_t g_registry_guard = {0};
78
79static void registry_lock(void) {
80 int expected = 0;
81 while (!atomic_cas_int(&g_registry_guard, &expected, 1))
82 expected = 0;
83}
84
85static void registry_unlock(void) {
86 atomic_store_int(&g_registry_guard, 0);
87}
88
89// Thread-local storage key for per-thread lock stack
90// Destructor automatically frees memory when thread exits
91static tls_key_t g_tls_mutex_stack = 0;
92static atomic_t g_tls_initialized = {0};
93
94// Flag set during shutdown to prevent new stack allocations
95// (threads still running at shutdown time won't leak new stacks)
96static atomic_t g_shutting_down = {0};
97
98// Track the mutexes involved in the last detected deadlock for throttling
99#define MAX_CYCLE_MUTEXES 16
100typedef struct {
102 int count;
108static deadlock_state_t g_last_deadlock = {0};
109
110// ============================================================================
111// Thread Registry Management
112// ============================================================================
113
115static void tls_mutex_stack_destructor(void *arg) {
116 if (!arg)
117 return;
118 registry_lock();
119 int count = atomic_load_int(&g_thread_registry_count);
120 for (int i = 0; i < count; ++i) {
121 // Windows FLS deletion can invoke this on the thread deleting the key.
122 // Identify the allocation, rather than assuming the current thread owns it.
123 if (g_thread_registry[i].stack == arg) {
124 g_thread_registry[i].stack = NULL;
125 break;
126 }
127 }
128 registry_unlock();
129 // Matches the untracked allocation used to avoid instrumenting this tracker.
130 free(arg);
131}
132
137static void ensure_tls_initialized(void) {
138 if (!atomic_load_bool(&g_tls_initialized)) {
139 registry_lock();
140 // Try to initialize TLS key with destructor
141 if (!atomic_load_bool(&g_tls_initialized) &&
142 platform_tls_key_create(&g_tls_mutex_stack, tls_mutex_stack_destructor) == 0) {
143 atomic_store_bool(&g_tls_initialized, true);
144 }
145 registry_unlock();
146 }
147}
148
149// Forward declaration
150static void register_thread_if_needed(void);
151
157static thread_lock_stack_t *get_thread_local_stack(void) {
158 // Skip allocation during shutdown (prevents leaks from threads killed mid-shutdown)
159 if (atomic_load_bool(&g_shutting_down)) {
160 return NULL;
161 }
162
163 // Ensure TLS key is initialized
164 ensure_tls_initialized();
165
166 // Get current thread's stack from TLS
167 thread_lock_stack_t *stack = (thread_lock_stack_t *)platform_tls_get(g_tls_mutex_stack);
168
169 if (stack == NULL) {
170 // Allocate stack on heap for this thread
171 // Use raw malloc() to avoid recursive mutex locking in debug subsystem
172 stack = (thread_lock_stack_t *)malloc(sizeof(thread_lock_stack_t));
173 if (stack) {
174 memset(stack, 0, sizeof(thread_lock_stack_t));
175 // Store in TLS (destructor will free it when thread exits)
176 platform_tls_set(g_tls_mutex_stack, stack);
177 register_thread_if_needed();
178 }
179 }
180 return stack;
181}
182
184static void register_thread_if_needed(void) {
185 static _Thread_local bool registered = false;
186 if (registered)
187 return;
188 thread_lock_stack_t *local_stack = get_thread_local_stack();
189 if (!local_stack)
190 return;
191 thread_id_t current_thread = thread_self();
192 registry_lock();
193 int count = atomic_load_int(&g_thread_registry_count);
194 int slot = -1;
195 for (int i = 0; i < count; ++i) {
196 if (thread_equal(g_thread_registry[i].thread_id, current_thread)) {
197 slot = i;
198 break;
199 }
200 if (!g_thread_registry[i].stack && slot < 0)
201 slot = i;
202 }
203 if (slot < 0 && count < MAX_THREADS)
204 slot = count++;
205 if (slot >= 0) {
206 g_thread_registry[slot].thread_id = current_thread;
207 g_thread_registry[slot].stack = local_stack;
208 atomic_store_int(&g_thread_registry_count, count);
209 }
210 registry_unlock();
211 registered = true;
212}
213
214// ============================================================================
215// Public Stack Operations (Debug builds only)
216// ============================================================================
217
218#ifndef NDEBUG
219
220static void mutex_stack_push(uintptr_t mutex_key, const char *mutex_name, mutex_stack_state_t state) {
221 thread_lock_stack_t *stack = get_thread_local_stack();
222 if (!stack) {
223 return;
224 }
225
226 // Register this thread in the global registry on first mutex use
227 register_thread_if_needed();
228
229 mutex_wait_snapshot_t telemetry = {0};
230 asciichat_error_context_t error = {0};
232 SAFE_STRNCPY(telemetry.span_name, ascii_otel_current_operation(), sizeof(telemetry.span_name));
233#ifndef __EMSCRIPTEN__
234 gotel_context_t context = ascii_otel_context();
235 memcpy(telemetry.trace_id, context.trace_id, sizeof(context.trace_id));
236 memcpy(telemetry.span_id, context.span_id, sizeof(context.span_id));
237 telemetry.context_valid = context.valid;
238 telemetry.context_sampled = context.sampled;
239 gotel_span_snapshot_t snapshot = gotel_current_span_snapshot();
240 memcpy(telemetry.parent_span_id, snapshot.parent_span_id, sizeof(telemetry.parent_span_id));
241 telemetry.span_start_unix_ns = snapshot.start_unix_ns;
242#endif
243 (void)HAS_ERRNO(&error);
244 }
245 stack_lock(stack);
246 if (state == MUTEX_STACK_STATE_PENDING) {
247 telemetry.error_code = error.code;
248 memcpy(telemetry.error_message, error.context_message, sizeof(telemetry.error_message));
249 stack->telemetry = telemetry;
250 }
251 if (stack->depth >= MUTEX_STACK_MAX_DEPTH) {
252 stack_unlock(stack);
253 return;
254 }
255 stack->stack[stack->depth].mutex_key = mutex_key;
256 stack->stack[stack->depth].mutex_name = mutex_name;
257 stack->stack[stack->depth].state = state;
258 stack->stack[stack->depth].timestamp_ns = time_get_ns();
259 stack->depth++;
260 stack_unlock(stack);
261}
262
263void mutex_stack_push_pending(uintptr_t mutex_key, const char *mutex_name) {
264 mutex_stack_push(mutex_key, mutex_name, MUTEX_STACK_STATE_PENDING);
265}
266
267void mutex_stack_push_locked(uintptr_t mutex_key, const char *mutex_name) {
268 mutex_stack_push(mutex_key, mutex_name, MUTEX_STACK_STATE_LOCKED);
269}
270
271void mutex_stack_mark_locked(uintptr_t mutex_key) {
272 thread_lock_stack_t *stack = get_thread_local_stack();
273 if (!stack) {
274 return;
275 }
276
277 // Mark the top of the stack as locked
278 stack_lock(stack);
279 if (stack->depth == 0) {
280 stack_unlock(stack);
281 return;
282 }
283 int top = stack->depth - 1;
284 if (stack->stack[top].mutex_key == mutex_key) {
286 stack->stack[top].timestamp_ns = time_get_ns();
287 }
288 stack_unlock(stack);
289
290 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
291}
292
293void mutex_stack_pop(uintptr_t mutex_key) {
294 thread_lock_stack_t *stack = get_thread_local_stack();
295 if (!stack) {
296 return;
297 }
298
299 // Validate top matches
300 stack_lock(stack);
301 if (stack->depth == 0) {
302 stack_unlock(stack);
303 return;
304 }
305 // Locks may be released out of acquisition order. Remove the most recent
306 // matching acquisition, preserving any recursive acquisitions below it.
307 for (int i = stack->depth - 1; i >= 0; --i) {
308 if (stack->stack[i].mutex_key == mutex_key) {
309 memmove(&stack->stack[i], &stack->stack[i + 1], (size_t)(stack->depth - i - 1) * sizeof(stack->stack[0]));
310 --stack->depth;
311 break;
312 }
313 }
314 stack_unlock(stack);
315
316 // Thread-local only. Registry is populated on-demand by mutex_stack_get_all_threads()
317}
318
319#endif
320
321int mutex_stack_get_current(mutex_stack_entry_t *out_entries, int max_entries) {
322 thread_lock_stack_t *stack = get_thread_local_stack();
323 if (!stack || !out_entries) {
324 return 0;
325 }
326
327 stack_lock(stack);
328 int depth = stack->depth;
329 int count = (depth < max_entries) ? depth : max_entries;
330 memcpy(out_entries, stack->stack, count * sizeof(mutex_stack_entry_t));
331 stack_unlock(stack);
332 return depth; // Return actual depth even if truncated
333}
334
335// ============================================================================
336// Global Thread Stack Access
337// ============================================================================
338
339int mutex_stack_get_all_threads(mutex_stack_entry_t ***out_stacks, int **out_stack_counts, int *out_thread_count) {
340
341 if (!out_stacks || !out_stack_counts || !out_thread_count) {
342 return -1;
343 }
344
345 // Read the thread count with acquire semantics (lock-free, no mutex needed)
346 // Use memory_order_seq_cst to ensure we get a consistent snapshot
347 int thread_count = atomic_load_int(&g_thread_registry_count);
348
349 // Allocate arrays for threads in the registry
350 // Note: SAFE_MALLOC takes bytes as first parameter, not count
351 *out_stacks = SAFE_CALLOC(thread_count, sizeof(mutex_stack_entry_t *), mutex_stack_entry_t **);
352 *out_stack_counts = SAFE_CALLOC(thread_count, sizeof(int), int *);
353
354 if (!*out_stacks || !*out_stack_counts) {
355 SAFE_FREE(*out_stacks);
356 SAFE_FREE(*out_stack_counts);
357 return -1;
358 }
359
360 // Copy each thread's stack from the registry
361 // Note: the thread count can change concurrently, so we re-read it in the loop
362 // to avoid accessing out-of-bounds memory if threads exit during iteration
363 for (int i = 0; i < thread_count; i++) {
364 // Re-check thread count in case registry shrank
365 int current_registry_count = atomic_load_int(&g_thread_registry_count);
366 if (i >= current_registry_count) {
367 break;
368 }
369
370 // Allocate before locking because tracked allocation updates the lock stack.
372 (*out_stack_counts)[i] = 0;
373 registry_lock();
374 thread_lock_stack_t *src = g_thread_registry[i].stack;
375 if (src) {
376 stack_lock(src);
377 int depth = src->depth;
378 (*out_stack_counts)[i] = depth;
379 memcpy((*out_stacks)[i], src->stack, depth * sizeof(mutex_stack_entry_t));
380 stack_unlock(src);
381 }
382 registry_unlock();
383 }
384
385 *out_thread_count = thread_count;
386 return 0;
387}
388
389void mutex_stack_free_all_threads(mutex_stack_entry_t **stacks, int *stack_counts, int thread_count) {
390
391 if (!stacks || !stack_counts)
392 return;
393
394 for (int i = 0; i < thread_count; i++) {
395 SAFE_FREE(stacks[i]);
396 }
397
398 SAFE_FREE(stacks);
399 SAFE_FREE(stack_counts);
400}
401
402// ============================================================================
403// Deadlock Detection
404// ============================================================================
405
409static bool thread_holds_mutex(thread_lock_stack_t *stack, uintptr_t mutex_key) {
410 if (!stack) {
411 return false;
412 }
413 for (int i = 0; i < stack->depth; i++) {
414 if (stack->stack[i].mutex_key == mutex_key && stack->stack[i].state == MUTEX_STACK_STATE_LOCKED) {
415 return true;
416 }
417 }
418 return false;
419}
420
424static uintptr_t thread_waiting_for_mutex(thread_lock_stack_t *stack) {
425 if (!stack) {
426 return 0;
427 }
428 if (stack->depth > 0) {
429 int top = stack->depth - 1;
430 if (stack->stack[top].state == MUTEX_STACK_STATE_PENDING) {
431 return stack->stack[top].mutex_key;
432 }
433 }
434 return 0;
435}
436
440static int find_thread_holding_mutex(thread_lock_stack_t *snapshots, int thread_count, uintptr_t mutex_key) {
441 for (int i = 0; i < thread_count; i++) {
442 thread_lock_stack_t *stack = &snapshots[i];
443 if (thread_holds_mutex(stack, mutex_key)) {
444 return i;
445 }
446 }
447 return -1;
448}
449
455#define MAX_CYCLE_LEN 64
456static int detect_cycle_dfs(thread_lock_stack_t *snapshots, int thread_count, int start_thread, int *cycle_path,
457 int *cycle_len) {
458 int visited[MAX_THREADS];
459 int path[MAX_CYCLE_LEN];
460 int path_len = 0;
461
462 // Initialize visited array
463 for (int i = 0; i < MAX_THREADS; i++) {
464 visited[i] = -1; // -1 = not visited, >= 0 = index in path
465 }
466
467 // DFS starting from start_thread
468 int current = start_thread;
469 while (path_len <= MAX_CYCLE_LEN && path_len <= thread_count) {
470 if (current < 0 || current >= thread_count) {
471 break; // Invalid thread
472 }
473
474 // Check if current is already in path (cycle found!)
475 for (int i = 0; i < path_len; i++) {
476 if (path[i] == current) {
477 // Found a cycle! Extract the cycle portion
478 *cycle_len = path_len - i;
479 for (int j = 0; j < *cycle_len; j++) {
480 cycle_path[j] = path[i + j];
481 }
482 return i; // Return where the cycle starts
483 }
484 }
485
486 if (path_len == MAX_CYCLE_LEN || path_len == thread_count)
487 break;
488 // Add current to path
489 path[path_len++] = current;
490
491 // Find next thread in the waits-for graph
492 thread_lock_stack_t *stack = &snapshots[current];
493 uintptr_t waiting_for = thread_waiting_for_mutex(stack);
494
495 if (waiting_for == 0) {
496 break; // No waiting, path ends
497 }
498
499 // Find who holds the mutex we're waiting for
500 current = find_thread_holding_mutex(snapshots, thread_count, waiting_for);
501 }
502
503 *cycle_len = 0;
504 return -1; // No cycle found
505}
506
507bool mutex_stack_try_snapshot(mutex_wait_snapshot_t *waits, size_t capacity, size_t *count, bool *limited) {
508 // A single collector owns this scratch space. Never dereference names from
509 // copied stacks: their registry entries may have been removed meanwhile.
510 static thread_lock_stack_t snapshots[MAX_THREADS];
511 static thread_id_t ids[MAX_THREADS];
512 static atomic_flag sampling = ATOMIC_FLAG_INIT;
513 if (atomic_flag_test_and_set(&sampling))
514 return false;
515 uint64_t expected = 0;
516 if (!atomic_compare_exchange_strong(&g_registry_guard.impl, &expected, 1)) {
517 atomic_flag_clear(&sampling);
518 return false;
519 }
520 int threads = atomic_load_int_impl(&g_thread_registry_count);
521 bool complete = true;
522 for (int i = 0; i < threads; ++i) {
523 thread_lock_stack_t *stack = g_thread_registry[i].stack;
524 ids[i] = g_thread_registry[i].thread_id;
525 snapshots[i].depth = 0;
526 if (!stack)
527 continue;
528 expected = 0;
529 if (!atomic_compare_exchange_strong(&stack->guard.impl, &expected, 1)) {
530 complete = false;
531 break;
532 }
533 snapshots[i].depth = stack->depth;
534 memcpy(snapshots[i].stack, stack->stack, sizeof(stack->stack));
535 snapshots[i].telemetry = stack->telemetry;
536 atomic_store(&stack->guard.impl, 0);
537 }
538 atomic_store(&g_registry_guard.impl, 0);
539 if (complete) {
540 *count = 0;
541 *limited = threads >= MAX_CYCLE_LEN;
542 for (int i = 0; i < threads; ++i) {
543 uintptr_t key = thread_waiting_for_mutex(&snapshots[i]);
544 *limited |= snapshots[i].depth == MUTEX_STACK_MAX_DEPTH;
545 if (!key)
546 continue;
547 if (*count == capacity) {
548 *limited = true;
549 continue;
550 }
551 int path[MAX_CYCLE_LEN], length = 0;
552 bool cycle = thread_holds_mutex(&snapshots[i], key);
553 if (detect_cycle_dfs(snapshots, threads, i, path, &length) >= 0)
554 for (int j = 0; j < length; ++j)
555 cycle |= path[j] == i;
556 mutex_wait_snapshot_t *wait = &waits[(*count)++];
557 *wait = snapshots[i].telemetry;
558 wait->mutex_key = key;
559 wait->thread_key = (uintptr_t)ids[i];
560 wait->since_ns = snapshots[i].stack[snapshots[i].depth - 1].timestamp_ns;
561 wait->cycle = cycle;
562 }
563 }
564 atomic_flag_clear(&sampling);
565 return complete;
566}
567
571static int compare_uintptr(const void *a, const void *b) {
572 uintptr_t ua = *(const uintptr_t *)a;
573 uintptr_t ub = *(const uintptr_t *)b;
574 if (ua < ub)
575 return -1;
576 if (ua > ub)
577 return 1;
578 return 0;
579}
580
585static bool deadlock_mutexes_changed(const uintptr_t *current_mutexes, int count) {
586 if (count != g_last_deadlock.count)
587 return true;
588
589 // Sort both arrays for order-independent comparison
590 uintptr_t current_sorted[MAX_CYCLE_MUTEXES];
591 uintptr_t last_sorted[MAX_CYCLE_MUTEXES];
592
593 for (int i = 0; i < count; i++) {
594 current_sorted[i] = current_mutexes[i];
595 last_sorted[i] = g_last_deadlock.mutexes[i];
596 }
597
598 qsort(current_sorted, count, sizeof(uintptr_t), compare_uintptr);
599 qsort(last_sorted, count, sizeof(uintptr_t), compare_uintptr);
600
601 for (int i = 0; i < count; i++) {
602 if (current_sorted[i] != last_sorted[i])
603 return true;
604 }
605 return false;
606}
607
611static void update_deadlock_state(const uintptr_t *mutexes, int count) {
612 g_last_deadlock.count = (count < MAX_CYCLE_MUTEXES) ? count : MAX_CYCLE_MUTEXES;
613 for (int i = 0; i < g_last_deadlock.count; i++) {
614 g_last_deadlock.mutexes[i] = mutexes[i];
615 }
616}
617
631 // Fixed scratch avoids allocating one list per thread on every 100 ms scan.
632 // Skip a busy snapshot rather than interfering with the threads being observed.
633 static thread_lock_stack_t snapshots[MAX_THREADS];
634 static thread_id_t ids[MAX_THREADS];
635 static atomic_flag sampling = ATOMIC_FLAG_INIT;
636 if (atomic_flag_test_and_set(&sampling))
637 return;
638 uint64_t expected = 0;
639 if (!atomic_compare_exchange_strong(&g_registry_guard.impl, &expected, 1)) {
640 atomic_flag_clear(&sampling);
641 return;
642 }
643 int thread_count = atomic_load_int_impl(&g_thread_registry_count);
644 bool complete = true;
645 for (int i = 0; i < thread_count; ++i) {
646 thread_lock_stack_t *src = g_thread_registry[i].stack;
647 ids[i] = g_thread_registry[i].thread_id;
648 snapshots[i].depth = 0;
649 if (!src)
650 continue;
651 expected = 0;
652 if (!atomic_compare_exchange_strong(&src->guard.impl, &expected, 1)) {
653 complete = false;
654 break;
655 }
656 snapshots[i].depth = src->depth;
657 memcpy(snapshots[i].stack, src->stack, (size_t)src->depth * sizeof(mutex_stack_entry_t));
658 atomic_store(&src->guard.impl, 0);
659 }
660 atomic_store(&g_registry_guard.impl, 0);
661 if (!complete) {
662 atomic_flag_clear(&sampling);
663 return;
664 }
665
666 // Check each thread for deadlock conditions
667 for (int i = 0; i < thread_count; i++) {
668 thread_lock_stack_t *stack_a = &snapshots[i];
669 uintptr_t waiting_for = thread_waiting_for_mutex(stack_a);
670
671 if (waiting_for == 0)
672 continue; // Thread not waiting
673
674 // Same-thread deadlock: thread trying to acquire a mutex it already holds
675 if (thread_holds_mutex(stack_a, waiting_for)) {
676 log_error("%s", colored_string(LOG_COLOR_ERROR, "╔═══════════════════════════════════════════════════════════╗"));
677 log_error("%s", colored_string(LOG_COLOR_ERROR, "║ ⚠️ DEADLOCK DETECTED: Same-thread Recursive Lock ⚠️ ║"));
678 log_error("%s", colored_string(LOG_COLOR_ERROR, "╚═══════════════════════════════════════════════════════════╝"));
679 log_error(" Thread Address: 0x%lx", (unsigned long)ids[i]);
680 log_error(" Mutex: 0x%lx", waiting_for);
681 log_error(" Issue: Thread attempts recursive lock on non-recursive mutex");
682 continue;
683 }
684
685 // Multi-thread circular wait: use DFS to detect cycles
686 int cycle_path[MAX_CYCLE_LEN];
687 int cycle_len = 0;
688 int cycle_start = detect_cycle_dfs(snapshots, thread_count, i, cycle_path, &cycle_len);
689
690 if (cycle_start >= 0 && cycle_len > 1) {
691 // Collect mutexes involved in this deadlock
692 uintptr_t cycle_mutexes[MAX_CYCLE_MUTEXES];
693 int mutex_count = 0;
694 for (int k = 0; k < cycle_len && mutex_count < MAX_CYCLE_MUTEXES; k++) {
695 int thread_idx = cycle_path[k];
696 thread_lock_stack_t *stack = &snapshots[thread_idx];
697 uintptr_t waiting_for = thread_waiting_for_mutex(stack);
698 if (waiting_for != 0) {
699 cycle_mutexes[mutex_count++] = waiting_for;
700 }
701 }
702
703 // Check if mutexes are different from last deadlock
704 bool is_new_deadlock = deadlock_mutexes_changed(cycle_mutexes, mutex_count);
705 if (is_new_deadlock) {
706 update_deadlock_state(cycle_mutexes, mutex_count);
707 }
708
709 // Cycle detected! Build complete message in one string
710 char cycle_msg[DEBUG_MUTEX_CYCLE_MSG_BUFFER_SIZE];
711 int msg_len = 0;
712
713 // Leading newline and header box
714 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "\n%s\n",
715 colored_string(LOG_COLOR_ERROR, "╔═════════════════════════════════╗"));
716 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
717 colored_string(LOG_COLOR_ERROR, "║ DEADLOCK: Circular Wait Cycle ║"));
718 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, "%s\n",
719 colored_string(LOG_COLOR_ERROR, "╚═════════════════════════════════╝"));
720
721 // Print each thread in the cycle
722 for (int k = 0; k < cycle_len; k++) {
723 int thread_idx = cycle_path[k];
724 int next_thread_idx = cycle_path[(k + 1) % cycle_len];
725
726 thread_lock_stack_t *current_stack = &snapshots[thread_idx];
727 uintptr_t current_waiting = thread_waiting_for_mutex(current_stack);
728
731 NAMED_GET_BY_PTR((uintptr_t)ids[thread_idx], thread_name, sizeof(thread_name));
732 NAMED_GET_BY_PTR((uintptr_t)current_waiting, mutex_name, sizeof(mutex_name));
733 NAMED_GET_BY_PTR((uintptr_t)ids[next_thread_idx], held_by_name, sizeof(held_by_name));
734
735 msg_len += snprintf(cycle_msg + msg_len, sizeof(cycle_msg) - msg_len, " T%d: %s waits for %s (held by %s)%s",
736 k + 1, thread_name, mutex_name, held_by_name, k < cycle_len - 1 ? "\n" : "");
737 }
738
739 // Log repeated deadlock detections (skip first call, throttle subsequent ones)
740 if (!is_new_deadlock) {
741 log_error_every(1000000, "%s", cycle_msg); // 1000000 µs = 1 second
742 }
743 }
744 }
745
746 atomic_flag_clear(&sampling);
747}
748
749// ============================================================================
750// Condition Variable Deadlock Detection (Debug builds only)
751// ============================================================================
752
753#ifndef NDEBUG
754
755#define COND_DEADLOCK_THRESHOLD_NS (5ULL * 1000000000ULL) // 5 seconds
756static _Atomic uint64_t g_next_cond_check_ns;
757
775
776static void copy_cond_deadlock_state(uintptr_t key, void *data) {
777 const cond_t *cond = (const cond_t *)key;
778 cond_deadlock_snapshot_t *snapshot = data;
779 snapshot->waiting_count = atomic_load(&cond->waiting_count);
780 snapshot->last_wait_time_ns = cond->last_wait_time_ns;
781 snapshot->last_signal_time_ns = cond->last_signal_time_ns;
782 snapshot->last_waiting_key = cond->last_waiting_key;
783 snapshot->last_wait_file = cond->last_wait_file;
784 snapshot->last_wait_line = cond->last_wait_line;
785 snapshot->last_wait_func = cond->last_wait_func;
786 snapshot->last_wait_mutex = cond->last_wait_mutex;
787}
788
797static void cond_deadlock_check_snapshot(const cond_deadlock_snapshot_t *cond, const char *name) {
798 if (cond->waiting_count == 0) {
799 return; // No threads waiting, nothing to check
800 }
801
802 // Skip deadlock checks for thread pool work queues - it's normal for worker threads
803 // to wait idly when there's no work to process. Older registrations used
804 // task_available; current pools register work_available.
805 if (name && (strstr(name, "task_available") != NULL || strstr(name, "work_available") != NULL)) {
806 return;
807 }
808
809 uint64_t now = time_get_ns();
810 uint64_t stuck_ns = now - cond->last_wait_time_ns;
811 bool no_signal_since_wait = (cond->last_signal_time_ns == 0 || cond->last_signal_time_ns < cond->last_wait_time_ns);
812
813 if (stuck_ns < COND_DEADLOCK_THRESHOLD_NS || !no_signal_since_wait) {
814 return; // Not stuck yet or was signaled recently
815 }
816
817 // Condition variable appears to be stuck - log detailed diagnostic info
818 char stuck_str[DEBUG_MUTEX_STUCK_BUFFER_SIZE];
819 time_pretty(stuck_ns, -1, stuck_str, sizeof(stuck_str));
820
821 char cond_buf[DEBUG_MUTEX_COND_BUFFER_SIZE] = {0};
822 int cond_written =
823 safe_snprintf(cond_buf, sizeof(cond_buf),
824 "Stuck cond '%s': %lu thread(s) waiting %s with no signal (most recent waiter: 0x%lx)\n", name,
825 cond->waiting_count, stuck_str, (unsigned long)cond->last_waiting_key);
826
827 cond_written +=
828 safe_snprintf(cond_buf + cond_written, sizeof(cond_buf) - cond_written, " wait entered at %s:%d %s()\n",
829 extract_project_relative_path(cond->last_wait_file, ASCIICHAT_OWNED_SOURCE_DIRS), cond->last_wait_line, cond->last_wait_func);
830
831 if (cond_written >= 0 && cond->last_wait_mutex) {
832 safe_snprintf(cond_buf + cond_written, sizeof(cond_buf) - cond_written,
833 " associated mutex: %p (cannot safely inspect without lock ownership)",
834 (void *)cond->last_wait_mutex);
835 }
836
837 log_warn_every(500 * NS_PER_MS_INT, "%s", cond_buf);
838}
839
840// Forward declare function from sync.c to check cleanup status
842
849
861
862static void copy_cond_deadlock_entry(uintptr_t key, uint64_t generation, const char *name, const char *type,
863 const char *file, int line, void *data) {
864 (void)generation;
865 (void)file;
866 (void)line;
867 if (!type || strcmp(type, "cond") != 0)
868 return;
869 cond_deadlock_batch_t *batch = data;
870 cond_deadlock_entry_t *entry = &batch->entries[batch->count++];
871 copy_cond_deadlock_state(key, &entry->state);
872 safe_snprintf(entry->name, sizeof(entry->name), "%s", name ? name : "?");
873}
874
892 return;
893 }
894 uint64_t now = time_get_ns();
895 if (now < atomic_load(&g_next_cond_check_ns))
896 return;
897 bool completed = false;
898 size_t required = 0;
899 cond_deadlock_batch_t batch = {0};
900 if (named_registry_try_snapshot(0, &required, &completed, copy_cond_deadlock_entry, &batch) != ASCIICHAT_OK ||
901 !required || required > SIZE_MAX / sizeof(*batch.entries))
902 return;
903 // Registry metadata must not re-enter allocation tracking. Object state and
904 // names are copied while pinned, and diagnostics run after releasing the lock.
905 batch.entries = UNTRACKED_MALLOC(required * sizeof(*batch.entries), cond_deadlock_entry_t *);
906 if (!batch.entries)
907 return;
908 size_t capacity = required;
909 if (named_registry_try_snapshot(capacity, &required, &completed, copy_cond_deadlock_entry, &batch) == ASCIICHAT_OK &&
910 completed) {
912 for (size_t i = 0; i < batch.count; ++i) {
913 const cond_deadlock_snapshot_t *state = &batch.entries[i].state;
914 const char *name = batch.entries[i].name;
915 cond_deadlock_check_snapshot(state, name);
916 if (state->waiting_count && !strstr(name, "task_available") && !strstr(name, "work_available")) {
917 uint64_t due = state->last_wait_time_ns + COND_DEADLOCK_THRESHOLD_NS;
918 // A recent signal already rules out this wait. Repeated warnings for
919 // an actually stuck condition retain their existing 500 ms cadence.
920 if (state->last_signal_time_ns < state->last_wait_time_ns || !state->last_signal_time_ns) {
921 if (due <= now)
922 due = now + 500 * NS_PER_MS_INT;
923 if (due < next)
924 next = due;
925 }
926 }
927 }
928 atomic_store(&g_next_cond_check_ns, next);
929 }
930 UNTRACKED_FREE(batch.entries);
931}
932
933#endif // NDEBUG
934
935// ============================================================================
936// Initialization
937// ============================================================================
938
940#ifndef NDEBUG
941 atomic_store(&g_next_cond_check_ns, 0);
942#endif
943 // No initialization needed - registry uses lock-free atomic operations
944 return 0;
945}
946
948 // Explicitly free the current thread's TLS stack
949 // This is used to prevent leaks when TLS destructors might not run reliably
950 // (e.g., debug threads exiting before mutex_stack_cleanup() deletes the TLS key)
951
952 if (!atomic_load_bool(&g_tls_initialized)) {
953 return; // TLS not initialized, nothing to clean up
954 }
955
956 thread_lock_stack_t *stack = (thread_lock_stack_t *)platform_tls_get(g_tls_mutex_stack);
957 if (!stack) {
958 return; // No stack allocated for this thread
959 }
960
961 // Clear from TLS
962 platform_tls_set(g_tls_mutex_stack, NULL);
963
964 // Update registry if this thread is registered
965 thread_id_t current_thread = thread_self();
966 registry_lock();
967 int count = atomic_load_int(&g_thread_registry_count);
968 for (int i = 0; i < count; i++) {
969 if (thread_equal(g_thread_registry[i].thread_id, current_thread) && g_thread_registry[i].stack == stack) {
970 g_thread_registry[i].stack = NULL; // Mark as freed in registry
971 break;
972 }
973 }
974
975 // Free the stack (raw free to match raw malloc above)
976 free(stack);
977 registry_unlock();
978}
979
981 // Signal shutdown to prevent new allocations from threads still running
982 atomic_store_bool(&g_shutting_down, true);
983
984 // Call only after tracked workers stop. Windows FLS key deletion invokes
985 // destructors; POSIX key deletion does not. Delete first so those callbacks
986 // retire their entries before we free any remaining allocations.
987 if (atomic_load_bool(&g_tls_initialized)) {
988 platform_tls_key_delete(g_tls_mutex_stack);
989 atomic_store_bool(&g_tls_initialized, false);
990 }
991 registry_lock();
992 int count = atomic_load_int(&g_thread_registry_count);
993 for (int i = 0; i < count; ++i) {
994 free(g_thread_registry[i].stack);
995 g_thread_registry[i].stack = NULL;
996 }
997 registry_unlock();
998
999 // Clear registry on cleanup using atomic operations
1000 atomic_store_int(&g_thread_registry_count, 0);
1001}
1002
1003// ============================================================================
1004// Release Build Stubs (NDEBUG)
1005// ============================================================================
1006// These no-op implementations replace the debug implementations in release builds
1007// (when debug/mutex.c is not compiled into the library)
1008
1009#ifdef NDEBUG
1010
1011void mutex_stack_push_locked(uintptr_t mutex_key, const char *mutex_name) {
1012 (void)mutex_key;
1013 (void)mutex_name;
1014}
1015
1016void mutex_stack_push_pending(uintptr_t mutex_key, const char *mutex_name) {
1017 (void)mutex_key;
1018 (void)mutex_name;
1019 // No-op in release builds
1020}
1021
1022void mutex_stack_mark_locked(uintptr_t mutex_key) {
1023 (void)mutex_key;
1024 // No-op in release builds
1025}
1026
1027void mutex_stack_pop(uintptr_t mutex_key) {
1028 (void)mutex_key;
1029 // No-op in release builds
1030}
1031
1032#endif
#define HAS_ERRNO(context)
Named object registry for debugging — log identifiable resource names.
#define UNTRACKED_FREE(ptr)
Definition common.h:414
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
#define SAFE_CALLOC(count, size, cast)
Definition common.h:275
unsigned long long uint64_t
Definition common.h:63
#define UNTRACKED_MALLOC(size, cast)
Definition common.h:405
void debug_sync_check_cond_deadlocks(void)
The useful part of cond deadlock batch is in entries, count: together they describe what is pending,...
Definition mutex.c:890
#define NAMED_GET_BY_PTR(key, buffer, size)
Get name of a pointer/key or format as address fallback.
asciichat_error_t named_registry_try_snapshot(size_t capacity, size_t *required, bool *completed, named_snapshot_fn copy, void *data)
@ ASCIICHAT_OK
Definition error_codes.h:46
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
@ LOG_COLOR_ERROR
Definition log/log.h:152
int time_pretty(uint64_t nanoseconds, int decimals, char *buffer, size_t buffer_size)
Format nanoseconds as a compact duration with configurable sub-minute precision.
Definition time.c:374
#define NS_PER_MS_INT
Definition time.h:168
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Platform-safe snprintf wrapper.
const char * colored_string(log_color_t color, const char *text)
Build a colored string for terminal output.
Definition string.c:284
const char * extract_project_relative_path(const char *file, const char *const *owned_dirs)
Extract a path relative to the source root embedded by CMake.
Statistics worker for error totals and memory reports. Signal handlers may only request a report.
📝 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:758
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
#define DEBUG_MUTEX_COND_BUFFER_SIZE
Definition mutex.c:8
bool debug_sync_is_cleanup_in_progress(void)
#define MAX_CYCLE_MUTEXES
Definition mutex.c:99
void mutex_stack_cleanup_current_thread(void)
Cleanup TLS stack for current thread Explicitly frees the thread-local mutex stack....
Definition mutex.c:947
#define DEBUG_MUTEX_NAME_BUFFER_SIZE
Definition mutex.c:9
#define DEBUG_MUTEX_THREAD_NAME_BUFFER_SIZE
Definition mutex.c:4
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.
Definition mutex.c:339
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().
Definition mutex.c:389
#define COND_DEADLOCK_THRESHOLD_NS
Definition mutex.c:755
#define MUTEX_STACK_MAX_DEPTH
Definition mutex.c:40
void mutex_stack_cleanup(void)
Cleanup mutex stack system.
Definition mutex.c:980
int mutex_stack_get_current(mutex_stack_entry_t *out_entries, int max_entries)
Get the current thread's lock stack.
Definition mutex.c:321
void mutex_stack_pop(uintptr_t mutex_key)
Pop the top mutex from the current thread's lock stack (unlock).
Definition mutex.c:293
#define DEBUG_MUTEX_MUTEX_NAME_BUFFER_SIZE
Definition mutex.c:5
void mutex_stack_push_locked(uintptr_t mutex_key, const char *mutex_name)
Definition mutex.c:267
bool mutex_stack_try_snapshot(mutex_wait_snapshot_t *waits, size_t capacity, size_t *count, bool *limited)
Definition mutex.c:507
#define MAX_CYCLE_LEN
DFS-based cycle detection in the waits-for graph Returns cycle start index if found,...
Definition mutex.c:455
#define MAX_THREADS
Definition mutex.c:62
#define DEBUG_MUTEX_HELD_BY_NAME_BUFFER_SIZE
Definition mutex.c:6
#define DEBUG_MUTEX_CYCLE_MSG_BUFFER_SIZE
Definition mutex.c:3
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).
Definition mutex.c:263
void mutex_stack_mark_locked(uintptr_t mutex_key)
Mark the top of the current thread's lock stack as LOCKED (transitions PENDING -> LOCKED).
Definition mutex.c:271
int mutex_stack_init(void)
Initialize mutex stack system.
Definition mutex.c:939
void mutex_stack_detect_deadlocks(void)
Detect circular wait deadlocks using DFS-based cycle detection.
Definition mutex.c:630
#define DEBUG_MUTEX_STUCK_BUFFER_SIZE
Definition mutex.c:7
Per-thread mutex lock stack for deadlock detection.
mutex_stack_state_t
mutex stack entry is intentionally a small, concrete record. Fields such as mutex_key,...
Definition mutex.h:32
@ MUTEX_STACK_STATE_LOCKED
Definition mutex.h:34
@ MUTEX_STACK_STATE_PENDING
Definition mutex.h:33
bool ascii_otel_enabled(void)
Definition otel.c:158
gotel_context_t ascii_otel_context(void)
Definition otel.c:725
const char * ascii_otel_current_operation(void)
Definition otel.c:153
📂 Path Manipulation Utilities
🔤 String Manipulation and Shell Escaping Utilities
When code needs to work with cond deadlock entry, it keeps state here instead of scattering it across...
Definition mutex.c:855
cond_deadlock_entry_t * entries
Definition mutex.c:856
Definition mutex.c:843
cond_deadlock_snapshot_t state
Definition mutex.c:844
char name[256]
Definition mutex.c:845
deadlock state is intentionally a small, concrete record. Fields such as count give callers the ident...
Definition mutex.c:763
uint64_t last_signal_time_ns
Definition mutex.c:766
const mutex_t * last_wait_mutex
Definition mutex.c:771
uint64_t last_wait_time_ns
Definition mutex.c:765
const char * last_wait_func
Definition mutex.c:770
const char * last_wait_file
Definition mutex.c:768
uintptr_t last_waiting_key
Definition mutex.c:767
uintptr_t mutexes[16]
Definition mutex.c:101
Definition mutex.h:40
const char * mutex_name
Definition mutex.h:42
mutex_stack_state_t state
Definition mutex.h:43
uint64_t timestamp_ns
Definition mutex.h:44
uintptr_t mutex_key
Definition mutex.h:41
For mutex wait snapshot, this is the live record that ties mutex_key, thread_key, since_ns,...
Definition mutex.h:111
uint64_t since_ns
Definition mutex.h:114
uint8_t trace_id[16]
Definition mutex.h:119
uintptr_t thread_key
Definition mutex.h:113
uint8_t span_id[8]
Definition mutex.h:119
uint64_t span_start_unix_ns
Definition mutex.h:121
char span_name[224]
Definition mutex.h:116
uintptr_t mutex_key
Definition mutex.h:112
uint8_t parent_span_id[8]
Definition mutex.h:120
char error_message[1024]
Definition mutex.h:118
mutex_stack_entry_t stack[64]
Definition mutex.c:43
mutex_wait_snapshot_t telemetry
Definition mutex.c:46
atomic_t guard
Definition mutex.c:45
thread lock stack keeps the values that move through the subsystem: depth, guard, telemetry....
Definition mutex.c:68
thread_lock_stack_t * stack
Definition mutex.c:70
thread_id_t thread_id
Definition mutex.c:69
⏱️ High-precision timing utilities using sokol_time.h and uthash