docs/deadlock-detection.md
The deadlock detection system automatically detects, diagnoses, and reports hung tests by:
Hung Test (tests/fl/test_hung.cpp)
Deadlock Detector (ci/util/deadlock_detector.py)
Test Wrapper (tests/test_wrapper.py)
FASTLED_DISABLE_CRASH_HANDLER=1 environment variableCrash Handler Bypass (all tests/crash_handler_*.h files)
FASTLED_DISABLE_CRASH_HANDLER environment variableMeson Integration (tests/meson.build)
timeout: 10 to all test registrations-Dtest_wrapper=truePrevious Problem: Internal crash handlers would block external debugger attachment.
Current Solution: FastLED uses signal handler chaining to provide BOTH internal dumps AND external debugger access:
Result: No configuration needed! Internal dumps work for crashes, external debuggers work for hangs.
If you want ONLY external debugger control (no internal dumps), set:
export FASTLED_DISABLE_CRASH_HANDLER=1
This completely disables internal handlers. However, this is rarely needed since signal chaining allows both to coexist.
Applied to all crash handler implementations:
crash_handler_win.h (Windows SEH)crash_handler_libunwind.h (Linux/Mac with libunwind)crash_handler_execinfo.h (Linux/Mac with execinfo)crash_handler_noop.h (no-op, unchanged)# Run tests normally (Meson handles timeout, no stack dumps)
bash test hung
# Test times out after 10 seconds, Meson reports TIMEOUT
# Enable test wrapper mode for stack dumps
meson setup -Dtest_wrapper=true .build/meson-quick
# Run test - wrapper will dump stacks on timeout
bash test hung
from ci.util.deadlock_detector import handle_hung_test
# Detect hung process and dump stacks
handle_hung_test(pid=12345, test_name="my_test", timeout_seconds=10.0)
runner.exe test.dllrunner.exe test.dllhandle_hung_test(pid, ...):
lldb --batch --attach-pid <pid>thread backtrace allKey difference: No need to disable crash handlers! Signal chaining allows both internal dumps (for crashes) and external debugger access (for hangs).
LLDB script (tmpXXXX.lldb):
settings set target.process.stop-on-exec false
settings set target.process.stop-on-sharedlibrary-events false
settings set target.x86-disassembly-flavor intel
thread backtrace all
thread list
quit
GDB script (tmpXXXX.gdb):
set pagination off
set confirm off
set print pretty on
thread apply all backtrace full
info threads
quit
FASTLED_DISABLE_CRASH_HANDLER=1 - (Optional) Disables internal crash handler completelymeson.options)option('test_wrapper', type: 'boolean', value: false,
description: 'Enable test wrapper with deadlock detection')
tests/meson.build)test_timeout = 10 # seconds per test
test(test_name, runner_exe,
args: [test_dll.full_path()],
timeout: test_timeout,
...
)
// tests/fl/test_hung.cpp
#include "test.h"
#include <cstdio>
__attribute__((noinline))
static void infinite_loop(volatile bool* keep_running) {
volatile int counter = 0;
while (*keep_running) {
counter++;
if (counter % 100000000 == 0) {
printf("Still spinning...\n");
fflush(stdout);
}
}
}
FL_TEST_CASE("test_intentional_hang") {
printf("Entering infinite loop...\n");
fflush(stdout);
volatile bool keep_running = true;
infinite_loop(&keep_running);
FL_CHECK(false); // Never reached
}
# Compile
bash test hung
# Run with wrapper (will timeout and dump stacks)
# Note: FASTLED_DISABLE_CRASH_HANDLER not needed due to signal chaining
uv run python tests/test_wrapper.py \
.build/meson-quick/tests/runner.exe \
.build/meson-quick/tests/fl_test_hung.dll \
5 # 5 second timeout
Running test: fl_test_hung (timeout: 5.0s)
[doctest] doctest version is "2.4.11"
TEST CASE: test_intentional_hang
Entering infinite loop...
Still spinning...
================================================================================
TEST HUNG: fl_test_hung
Exceeded timeout of 5.0s
================================================================================
š Attaching lldb to hung process (PID 12345)...
Note: Crash handlers use signal chaining (internal dump ā external debugger)
Running: lldb --batch --source /tmp/tmpXXX.lldb --attach-pid 12345
THREAD STACK TRACES:
--------------------------------------------------------------------------------
* thread #1, name = 'test_hung', stop reason = signal SIGSTOP
* frame #0: 0x00007ff6b9a41234 test_hung.dll`infinite_loop(...) at test_hung.cpp:12
frame #1: 0x00007ff6b9a41456 test_hung.dll`____C_A_T_C_H____T_E_S_T____0() at test_hung.cpp:23
frame #2: 0x00007ff6b9a50000 runner.exe`main + 123
...
--------------------------------------------------------------------------------
================================================================================
šŖ Killed hung process (PID 12345)
Test was killed due to timeout (5.0s)