diff --git a/tests/lockdeadlocks/CMakeLists.txt b/tests/lockdeadlocks/CMakeLists.txt new file mode 100644 index 00000000..88556f69 --- /dev/null +++ b/tests/lockdeadlocks/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) + +project( + lockdeadlocks-test + + LANGUAGES C CXX +) + +enable_testing() + +file(GENERATE OUTPUT .gitignore CONTENT "*") +set(CMAKE_CXX_STANDARD 20) + +# server headers pull in ppqsort through TracySort.hpp; reuse the CPM checkout when available +if(NOT PPQSORT_INCLUDE_DIR) + file(GLOB PPQSORT_INCLUDE_DIR "$ENV{HOME}/.cache/cpm/ppqsort/*/include") +endif() + +add_executable( + lockdeadlocks-test + test.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../server/TracyLocks.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../server/TracyMemory.cpp +) + +target_include_directories( + lockdeadlocks-test + PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/../../server + ${CMAKE_CURRENT_LIST_DIR}/../../public/common + ${PPQSORT_INCLUDE_DIR} +) + +add_test(NAME lockdeadlocks-test COMMAND lockdeadlocks-test) diff --git a/tests/lockdeadlocks/test.cpp b/tests/lockdeadlocks/test.cpp new file mode 100644 index 00000000..10fb7ac8 --- /dev/null +++ b/tests/lockdeadlocks/test.cpp @@ -0,0 +1,320 @@ +#include +#include +#include + +#include "TracyLocks.hpp" + +using namespace tracy; + +static constexpr uint64_t TA = 100; +static constexpr uint64_t TB = 200; +static constexpr uint64_t TC = 300; +static constexpr uint64_t TD = 400; +static constexpr uint32_t L1 = 1; +static constexpr uint32_t L2 = 2; +static constexpr uint32_t L3 = 3; +static constexpr uint32_t L4 = 4; + +struct Fixture +{ + unordered_flat_map maps; + + LockMap& Make( uint32_t id, LockType type, bool legacy = false ) + { + auto lm = new LockMap(); + InitLockMap( *lm, 0, type, 0 ); + lm->legacyInversions = legacy; + maps.emplace( id, lm ); + return *lm; + } + + ~Fixture() + { + for( auto& v : maps ) delete v.second; + } +}; + +static void Ev( LockMap& lm, int64_t t, uint64_t thread, LockEvent::Type type ) +{ + const auto slot = GetLockSlot( lm, thread ); + AppendLockEvent( lm, t, slot, type ); +} + +struct Result +{ + Vector groups; + Vector members; +}; + +static Result Run( const char* name, Fixture& f ) +{ + printf( "%s... ", name ); + fflush( stdout ); + Result r; + DetectLockDeadlocks( f.maps, r.groups, r.members ); + return r; +} + +static bool HasThread( const Result& r, size_t group, uint64_t thread ) +{ + const auto& g = r.groups[group]; + for( uint32_t i=0; i threads ) +{ + const auto& g = r.groups[group]; + assert( g.cnt == threads.size() ); + for( auto t : threads ) assert( HasThread( r, group, t ) ); +} + +int main() +{ + // Crossed exclusive locks: the classic two-thread deadlock. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l2, 20, TB, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l1, 40, TB, LockEvent::Type::Wait ); + const auto r = Run( "crossed two-lock deadlock", f ); + assert( r.groups.size() == 1 ); + CheckGroup( r, 0, { TA, TB } ); + assert( r.groups[0].time == 40 ); + const auto* ma = Member( r, 0, TA ); + const auto* mb = Member( r, 0, TB ); + assert( ma && ma->lock == L2 && ma->holder == TB && ma->waitTime == 30 ); + assert( mb && mb->lock == L1 && mb->holder == TA && mb->waitTime == 40 ); + printf( "ok\n" ); + } + + // Contention without a cycle is not a deadlock. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l1, 20, TB, LockEvent::Type::Wait ); + assert( Run( "simple contention", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // A recursive acquire window: the thread is its own exclusive holder. Recursion is + // indistinguishable from self-deadlock, so nothing is reported. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l1, 20, TA, LockEvent::Type::Wait ); + assert( Run( "recursive acquire window", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // A shared holder waiting for exclusive can never progress while its own shared hold persists. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::SharedLockable ); + Ev( l1, 10, TA, LockEvent::Type::ObtainShared ); + Ev( l1, 20, TA, LockEvent::Type::Wait ); + const auto r = Run( "upgrade self-deadlock", f ); + assert( r.groups.size() == 1 ); + CheckGroup( r, 0, { TA } ); + const auto* ma = Member( r, 0, TA ); + assert( ma && ma->holder == TA ); + printf( "ok\n" ); + } + + // Two shared holders both requesting exclusive: one group, both members. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::SharedLockable ); + Ev( l1, 10, TA, LockEvent::Type::ObtainShared ); + Ev( l1, 15, TB, LockEvent::Type::ObtainShared ); + Ev( l1, 20, TA, LockEvent::Type::Wait ); + Ev( l1, 25, TB, LockEvent::Type::Wait ); + const auto r = Run( "mutual upgrade", f ); + assert( r.groups.size() == 1 ); + CheckGroup( r, 0, { TA, TB } ); + printf( "ok\n" ); + } + + // A blocked exclusive waiter behind shared holders that keep running is contention, not deadlock. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::SharedLockable ); + Ev( l1, 10, TA, LockEvent::Type::ObtainShared ); + Ev( l1, 20, TB, LockEvent::Type::Wait ); + assert( Run( "exclusive waiter behind live shared holders", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // Exclusive waiter blocked by a shared holder; that holder waits on the first thread's lock. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::SharedLockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::ObtainShared ); + Ev( l2, 20, TB, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l1, 40, TB, LockEvent::Type::Wait ); + const auto r = Run( "cycle through shared hold", f ); + assert( r.groups.size() == 1 ); + CheckGroup( r, 0, { TA, TB } ); + printf( "ok\n" ); + } + + // Chains that terminate at a running thread never close. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l1, 20, TB, LockEvent::Type::Wait ); + Ev( l2, 30, TC, LockEvent::Type::Obtain ); + Ev( l2, 40, TA, LockEvent::Type::Wait ); + assert( Run( "chain to live holder", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // Three-thread cycle forms a single group. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + auto& l3 = f.Make( L3, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l2, 15, TB, LockEvent::Type::Obtain ); + Ev( l3, 20, TC, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l3, 40, TB, LockEvent::Type::Wait ); + Ev( l1, 50, TC, LockEvent::Type::Wait ); + const auto r = Run( "three-thread cycle", f ); + assert( r.groups.size() == 1 ); + CheckGroup( r, 0, { TA, TB, TC } ); + assert( r.groups[0].time == 50 ); + printf( "ok\n" ); + } + + // Independent deadlocks are reported as separate groups. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + auto& l3 = f.Make( L3, LockType::Lockable ); + auto& l4 = f.Make( L4, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l2, 15, TB, LockEvent::Type::Obtain ); + Ev( l3, 20, TC, LockEvent::Type::Obtain ); + Ev( l4, 25, TD, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l1, 35, TB, LockEvent::Type::Wait ); + Ev( l4, 40, TC, LockEvent::Type::Wait ); + Ev( l3, 45, TD, LockEvent::Type::Wait ); + const auto r = Run( "two disjoint cycles", f ); + assert( r.groups.size() == 2 ); + assert( ( HasThread( r, 0, TA ) && HasThread( r, 1, TC ) ) || ( HasThread( r, 0, TC ) && HasThread( r, 1, TA ) ) ); + printf( "ok\n" ); + } + + // Traces with release-ordering inversions have ambiguous holder state; they are excluded. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable, true ); + auto& l2 = f.Make( L2, LockType::Lockable, true ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l2, 20, TB, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l1, 40, TB, LockEvent::Type::Wait ); + assert( Run( "legacy inversion exclusion", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // A wait with no holder and no co-waiters has no edge to follow. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Wait ); + assert( Run( "wait without holder", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // Releasing one leg of a cycle dissolves it; final-state scans reflect the resolution. + { + Fixture f; + auto& l1 = f.Make( L1, LockType::Lockable ); + auto& l2 = f.Make( L2, LockType::Lockable ); + Ev( l1, 10, TA, LockEvent::Type::Obtain ); + Ev( l2, 20, TB, LockEvent::Type::Obtain ); + Ev( l2, 30, TA, LockEvent::Type::Wait ); + Ev( l1, 40, TB, LockEvent::Type::Wait ); + assert( Run( "cycle resolved by release", f ).groups.size() == 1 ); + Ev( l1, 50, TA, LockEvent::Type::Release ); + Result r2; + DetectLockDeadlocks( f.maps, r2.groups, r2.members ); + assert( r2.groups.size() == 0 ); + printf( "ok\n" ); + } + + // Exclusive waiters behind several shared holders: no ring when all holders run free. + { + Fixture f; + auto& rw = f.Make( L1, LockType::SharedLockable ); + Ev( rw, 10, TA, LockEvent::Type::ObtainShared ); + Ev( rw, 11, TB, LockEvent::Type::ObtainShared ); + for( uint64_t w = 0; w < 10; w++ ) Ev( rw, 20 + w, 200 + w, LockEvent::Type::Wait ); + assert( Run( "convoy without cycle", f ).groups.size() == 0 ); + printf( "ok\n" ); + } + + // A convoy closes its ring through one shared holder; free holders and co-waiters are not members. + { + Fixture f; + auto& rw = f.Make( L1, LockType::SharedLockable ); + auto& m = f.Make( L2, LockType::Lockable ); + Ev( rw, 10, TA, LockEvent::Type::ObtainShared ); + Ev( rw, 11, TB, LockEvent::Type::ObtainShared ); + Ev( m, 15, TC, LockEvent::Type::Obtain ); + Ev( rw, 20, TC, LockEvent::Type::Wait ); + Ev( rw, 21, TD, LockEvent::Type::Wait ); + Ev( m, 25, TB, LockEvent::Type::Wait ); + const auto r = Run( "cycle through shared holder", f ); + assert( r.groups.size() == 1 ); + assert( r.groups[0].cnt == 2 ); + assert( HasThread( r, 0, TC ) && HasThread( r, 0, TB ) ); + assert( !HasThread( r, 0, TA ) && !HasThread( r, 0, TD ) ); + const auto* mc = Member( r, 0, TC ); + assert( mc && mc->holder == TB && mc->lock == L1 ); + const auto* mt = Member( r, 0, TB ); + assert( mt && mt->holder == TC && mt->lock == L2 ); + printf( "ok\n" ); + } + + // A shared holder waiting for exclusive is deadlocked by its own hold also when others hold shared. + { + Fixture f; + auto& rw = f.Make( L1, LockType::SharedLockable ); + Ev( rw, 10, TA, LockEvent::Type::ObtainShared ); + Ev( rw, 11, TB, LockEvent::Type::ObtainShared ); + Ev( rw, 12, TA, LockEvent::Type::Wait ); + const auto r = Run( "upgrade with co-holders", f ); + assert( r.groups.size() == 1 ); + assert( r.groups[0].cnt == 1 ); + const auto* ma = Member( r, 0, TA ); + assert( ma && ma->holder == TA && ma->lock == L1 ); + printf( "ok\n" ); + } + + printf( "All deadlock detection tests passed.\n" ); + return 0; +}