From 092bfcc2a0d22b206b16286a983157caafcb0bd6 Mon Sep 17 00:00:00 2001 From: Bartosz Taudul Date: Tue, 22 Sep 2026 02:21:19 +0200 Subject: [PATCH] Add deadlock cycle detection to the lock event engine. The scan consumes the final Waiting/SharedHolding flags of each lock map: a thread blocked in a lock acquisition emits no Obtain and no other event, so an unclosed wait at the last known state is a permanent wait per the event contract. A failed try_lock never emits a wait, so it cannot fabricate an edge. Locks recorded by clients with inverted release ordering are skipped entirely, as their holder attribution is ambiguous. A thread that holds a shared lock and waits for exclusive cannot progress while its own shared hold persists; this is reported as a size-1 cycle. --- server/TracyLocks.cpp | 227 ++++++++++++++++++++++++++++++++++++++++++ server/TracyLocks.hpp | 33 ++++++ 2 files changed, 260 insertions(+) diff --git a/server/TracyLocks.cpp b/server/TracyLocks.cpp index 5cdeddf0..ee4b97ba 100644 --- a/server/TracyLocks.cpp +++ b/server/TracyLocks.cpp @@ -1,4 +1,5 @@ #include +#include #include "TracyLocks.hpp" @@ -356,4 +357,230 @@ bool ApplyLockMark( LockMap& map, uint16_t slot, int16_t srcloc ) return true; } +static void DetectLockDeadlocksImpl( const unordered_flat_map& lockMap, + const unordered_flat_set* candidates, + Vector& groups, Vector& members ) +{ + struct AdjEdge + { + uint32_t to; + uint64_t toThread; + uint32_t lock; + int64_t waitTime; + }; + + unordered_flat_map nodeIdx; + Vector nodeThread; + std::vector> adj; + Vector selfLoop; + Vector aux; + + auto addNode = [&] ( uint64_t thread ) -> uint32_t { + auto it = nodeIdx.find( thread ); + if( it != nodeIdx.end() ) return it->second; + const uint32_t idx = (uint32_t)nodeThread.size(); + nodeThread.push_back( thread ); + adj.emplace_back(); + selfLoop.push_back( 0 ); + aux.push_back( 0 ); + nodeIdx.emplace( thread, idx ); + return idx; + }; + + auto buildFor = [&] ( uint32_t lockId, const LockMap& map ) + { + if( !map.valid || map.legacyInversions ) return; + if( map.curWaitCount == 0 && map.curWaitSharedCount == 0 ) return; + uint32_t sharedAux = ~0u; + + for( uint16_t s=0; saux->holders instead of a + // waiters x holders fan-out. The aux node only ever relays those pairs, + // so closed rings over real threads are unchanged. + if( sharedAux == ~0u ) + { + sharedAux = (uint32_t)nodeThread.size(); + nodeThread.push_back( 0 ); + adj.emplace_back(); + selfLoop.push_back( 0 ); + aux.push_back( 1 ); + for( uint16_t h=0; hsecond ); + } + } + else + { + for( auto& mit : lockMap ) buildFor( mit.first, *mit.second ); + } + + const uint32_t n = (uint32_t)nodeThread.size(); + if( nodeThread.empty() ) return; + + Vector disc; + Vector low; + Vector onStack; + disc.reserve_and_use( n ); + low.reserve_and_use( n ); + onStack.reserve_and_use( n ); + memset( disc.begin(), 0xFF, n * sizeof( int32_t ) ); + memset( low.begin(), 0, n * sizeof( int32_t ) ); + memset( onStack.begin(), 0, n ); + Vector stack; + Vector comp; + Vector inComp; + inComp.reserve_and_use( n ); + memset( inComp.begin(), 0, n ); + int32_t clk = 0; + + struct Frame + { + uint32_t v; + size_t ei; + }; + Vector rstack; + + for( uint32_t root=0; root= 0 ) continue; + disc[root] = low[root] = clk++; + stack.push_back( root ); + onStack[root] = 1; + rstack.push_back( { root, 0 } ); + while( !rstack.empty() ) + { + const uint32_t v = rstack.back().v; + if( rstack.back().ei < adj[v].size() ) + { + const auto e = adj[v][rstack.back().ei++]; + if( disc[e.to] < 0 ) + { + disc[e.to] = low[e.to] = clk++; + stack.push_back( e.to ); + onStack[e.to] = 1; + rstack.push_back( { e.to, 0 } ); + } + else if( onStack[e.to] && disc[e.to] < low[v] ) + { + low[v] = disc[e.to]; + } + } + else + { + rstack.pop_back(); + if( !rstack.empty() && low[v] < low[rstack.back().v] ) low[rstack.back().v] = low[v]; + if( low[v] != disc[v] ) continue; + + comp.clear(); + uint32_t w; + do + { + w = stack.back(); + stack.pop_back(); + onStack[w] = 0; + comp.push_back( w ); + } while( w != v ); + + uint32_t real = 0; + bool auxInComp = false; + for( auto c : comp ) + { + if( aux[c] != 0 ) auxInComp = true; + else real++; + } + // Aux is only a relay: one real thread closing its ring through aux is + // waiting on a lock held also by itself - an upgrade self-deadlock. + if( real < 2 && !( real == 1 && ( auxInComp || selfLoop[v] ) ) ) continue; + + for( auto c : comp ) inComp[c] = 1; + + const uint32_t first = (uint32_t)members.size(); + int64_t time = 0; + for( auto c : comp ) + { + if( aux[c] != 0 ) continue; + for( auto& e : adj[c] ) + { + if( !inComp[e.to] ) continue; + uint32_t blocker = e.to; + if( aux[e.to] != 0 ) + { + bool found = false; + for( auto& he : adj[e.to] ) + { + if( inComp[he.to] != 0 ) { blocker = he.to; found = true; break; } + } + if( !found ) continue; + } + members.push_back( { nodeThread[c], nodeThread[blocker], e.lock, e.waitTime } ); + if( e.waitTime > time ) time = e.waitTime; + break; + } + } + const uint32_t cnt = (uint32_t)members.size() - first; + std::sort( members.begin()+first, members.begin()+first+cnt, [] ( const DeadlockMember& lhs, const DeadlockMember& rhs ) { return lhs.thread < rhs.thread; } ); + groups.push_back( { time, first, cnt } ); + + for( auto c : comp ) inComp[c] = 0; + } + } + } +} + +void DetectLockDeadlocks( const unordered_flat_map& lockMap, + Vector& groups, Vector& members ) +{ + DetectLockDeadlocksImpl( lockMap, nullptr, groups, members ); +} + +void DetectLockDeadlocks( const unordered_flat_map& lockMap, + const unordered_flat_set& candidates, + Vector& groups, Vector& members ) +{ + DetectLockDeadlocksImpl( lockMap, &candidates, groups, members ); +} + } diff --git a/server/TracyLocks.hpp b/server/TracyLocks.hpp index 45cae912..969fbc7b 100644 --- a/server/TracyLocks.hpp +++ b/server/TracyLocks.hpp @@ -244,6 +244,39 @@ inline void ForEachLockDrawItem( const LockMap& map, const LockThreadInfo& ti, i } } +struct DeadlockMember +{ + uint64_t thread; + uint64_t holder; // thread blocking `thread`; itself on self-loop (upgrade) edges + uint32_t lock; + int64_t waitTime; +}; + +struct DeadlockGroup +{ + int64_t time; // the wait that closed the cycle + uint32_t first; // range into the members array + uint32_t cnt; +}; + +// Cycles of the wait-for graph built from the final state of every lock: an edge +// from each still-waiting thread to a thread currently holding the lock it waits +// on. Blocking is by current holders only: a shared wait queued behind a pending +// exclusive waiter gets no edge. A closed cycle means no member can ever proceed +// - each holder is blocked itself and can never release. Members of a group are +// sorted by thread id. Invalid and legacy inversion locks are excluded: their +// holder state is ambiguous. +void DetectLockDeadlocks( const unordered_flat_map& lockMap, + Vector& groups, Vector& members ); + +// Candidate-restricted form for live sessions: every edge originates at a waiter, +// so scanning exactly the locks with nonzero wait counts finds the same cycles as +// the full scan, at cost proportional to the candidate set instead of the +// announced locks. Where a thread has several in-cycle wait edges, the recorded +// member edge follows scan order and may differ between the two forms. +void DetectLockDeadlocks( const unordered_flat_map& lockMap, + const unordered_flat_set& candidates, + Vector& groups, Vector& members ); } #endif