From 029deadf198b88a56a82d3eedd17789d0e14c3ec Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:29:25 +0000 Subject: [PATCH 01/11] First draft of Tracy Lock implementation Contains: - Added support for Locks in the TracyTestClient. - Updates to CcpMutex - First draft of tests for low-level Tracy lock functions. - First draft of tests for tests on CcpMutex/CcpAutoMutex classes. --- include/CcpMutex.h | 16 +- tests/CcpTelemetry.cpp | 513 ++++++++++++++++++++++++++++++++++++++ tests/TracyTestClient.cpp | 232 ++++++++++++++++- tests/TracyTestClient.h | 73 ++++++ 4 files changed, 828 insertions(+), 6 deletions(-) diff --git a/include/CcpMutex.h b/include/CcpMutex.h index 5dc60ea..d5d5680 100644 --- a/include/CcpMutex.h +++ b/include/CcpMutex.h @@ -6,6 +6,8 @@ #include "CcpTelemetry.h" #include "CcpAtomic.h" +#include + #ifdef _WIN32 class CcpMutex @@ -22,6 +24,9 @@ class CcpMutex if ( CcpTelemetryIsConnected() ) { TracyCLockAnnounce( m_tracyLockContext ); + // Tracy copies the name, so passing a temporary is fine. + const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); + TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); } #endif @@ -42,14 +47,14 @@ class CcpMutex { #if CCP_TELEMETRY_ENABLED bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif EnterCriticalSection( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( notifyTracy ) + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker { TracyCLockAfterLock( m_tracyLockContext ); } @@ -115,6 +120,9 @@ class CcpMutex if ( CcpTelemetryIsConnected() ) { TracyCLockAnnounce( m_tracyLockContext ); + // Tracy copies the name, so passing a temporary is fine. + const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); + TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); } #endif @@ -135,14 +143,14 @@ class CcpMutex { #if CCP_TELEMETRY_ENABLED bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif pthread_mutex_lock( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( notifyTracy ) + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker { TracyCLockAfterLock( m_tracyLockContext ); } diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 7504ad4..16a28f0 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -2,8 +2,13 @@ #include +#include +#include +#include #include #include +#include +#include #include @@ -128,6 +133,84 @@ class CcpTelemetryTest : public ::testing::Test return tracyZones.end() != std::find_if( tracyZones.begin(), tracyZones.end(), pred ); } + // Finds the lock announced via TracyCLockAnnounce at the given line of this + // file. Locks are identified by their announce call site because absolute + // lock ids and announce counts are not stable across tests: Tracy defers + // LockAnnounce/LockTerminate events and replays them on every new + // connection, so locks from earlier tests reappear here. For the same + // reason, when several locks match the call site (e.g. when a test is + // repeated within one process), the most recently announced one wins. + // The source location is resolved through asynchronous server queries, so + // this returns false until those have completed; call it from a + // TickTelemetry predicate. + bool TryGetLockAtLine( uint32_t line, TracyTestClient::LockInfo& outLock ) + { + bool found = false; + for( const auto& lock : m_tracyClient.GetLocks() ) + { + if( lock.line == line && lock.source == __FILE__ && ( !found || lock.id > outLock.id ) ) + { + outLock = lock; + found = true; + } + } + return found; + } + + // Returns true when the lock was announced from CcpMutex.h. MSVC records + // __FILE__ of included headers in lower case, so compare case-insensitively. + static bool IsAnnouncedFromCcpMutexHeader( const TracyTestClient::LockInfo& lock ) + { + std::string source = lock.source; + std::transform( source.begin(), source.end(), source.begin(), + []( unsigned char c ) { return static_cast( std::tolower( c ) ); } ); + return source.find( "ccpmutex.h" ) != std::string::npos; + } + + // All CcpMutex instances share the same announce call site in CcpMutex.h, so + // they cannot be told apart by source location. This returns all locks + // announced from CcpMutex.h that have not been terminated. Source locations + // resolve through asynchronous server queries, so a just-announced lock + // shows up here only once its source string has arrived; call this from a + // TickTelemetry predicate. + std::vector GetActiveCcpMutexLocks() + { + std::vector result; + for( const auto& lock : m_tracyClient.GetActiveLocks() ) + { + if( IsAnnouncedFromCcpMutexHeader( lock ) ) + result.push_back( lock ); + } + return result; + } + + // Finds the active lock carrying the given custom name. CcpMutex names its + // lock "-" via TracyCLockCustomName in the constructor. The + // name arrives asynchronously shortly after the announce event, so call + // this from a TickTelemetry predicate. When a test is repeated within one + // process, Tracy replays earlier (terminated) locks carrying the same name; + // those are terminated by the time any event of the running test is + // visible, and the most recently announced lock wins by id regardless. + bool TryGetActiveLockNamed( const std::string& name, TracyTestClient::LockInfo& outLock ) + { + bool found = false; + for( const auto& lock : m_tracyClient.GetActiveLocks() ) + { + if( lock.name == name && ( !found || lock.id > outLock.id ) ) + { + outLock = lock; + found = true; + } + } + return found; + } + + // Refreshes a previously identified lock by id. + bool TryGetLock( uint32_t lockId, TracyTestClient::LockInfo& outLock ) + { + return m_tracyClient.TryGetLock( lockId, outLock ); + } + const std::string expectedNoFiber; const std::string expectedFiberName1{ "TestFiber1" }; const std::string expectedFiberName2{ "TestFiber2" }; @@ -135,6 +218,7 @@ class CcpTelemetryTest : public ::testing::Test TracyTestClient m_tracyClient; }; + TEST_F( CcpTelemetryTest, TestFiberSwitching ) { CcpTelemetrySetActiveFiber( expectedFiberName1 ); @@ -242,3 +326,432 @@ TEST_F( CcpTelemetryTest, StartStopStartTelemetryWhileClientIsRunning ) EXPECT_TRUE( m_tracyClient.GetZones().empty() ); EXPECT_EQ( 2, m_tracyClient.GetZoneEndCount() ); } + +TEST_F( CcpTelemetryTest, RawTracyLockCAnnounceAndTerminate ) +{ + TracyCLockCtx lockCtx; + const uint32_t announceLine = __LINE__ + 1; + TracyCLockAnnounce( lockCtx ); + + // Wait for a non-terminated match: when a test is repeated in one process, + // Tracy replays the previous run's (terminated) lock from the same call + // site, and its source location may resolve before that of the new lock. + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); + ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); + EXPECT_GE( m_tracyClient.GetLockAnnounceCount(), 1 ); + EXPECT_FALSE( lockInfo.terminated ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + EXPECT_EQ( 0, lockInfo.waitCount ); + EXPECT_EQ( 0, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + // TracyCLockAnnounce announces with name == nullptr and function == __func__. + EXPECT_TRUE( lockInfo.name.empty() ); + EXPECT_EQ( "TestBody", lockInfo.function ); + + TracyCLockTerminate( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); + EXPECT_GE( m_tracyClient.GetLockTerminateCount(), 1 ); +} + +TEST_F( CcpTelemetryTest, RawTracyUncontendedLock ) +{ + TracyCLockCtx lockCtx; + const uint32_t announceLine = __LINE__ + 1; + TracyCLockAnnounce( lockCtx ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); + ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); + + const auto notifyTracy = TracyCLockBeforeLock( lockCtx ); + EXPECT_TRUE( notifyTracy ); + TracyCLockAfterLock( lockCtx ); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.obtainCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + EXPECT_NE( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ) << "An obtained lock should no longer have its holder in the waiting list"; + + TracyCLockAfterUnlock( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.releaseCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 1, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + + TracyCLockTerminate( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); +} + +// A locks, B waits, A unlocks, B locks, B unlocks. +TEST_F( CcpTelemetryTest, RawTracyContendedLockWithOneWaitingThread ) +{ + TracyCLockCtx lockCtx; + const uint32_t announceLine = __LINE__ + 1; + TracyCLockAnnounce( lockCtx ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); + ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); + + std::mutex mutex; + auto lockMutex = [&] { + const auto notifyTracy = TracyCLockBeforeLock( lockCtx ); + mutex.lock(); + if( notifyTracy ) + { + TracyCLockAfterLock( lockCtx ); + } + }; + auto unlockMutex = [&] { + mutex.unlock(); + TracyCLockAfterUnlock( lockCtx ); + }; + + // Thread A acquires the lock and holds it until we tell it to release. + // The locking must happen on worker threads so that this thread can keep + // ticking the telemetry while the lock is being held / waited on. + std::atomic releaseA{ false }; + std::thread threadA( [&] { + lockMutex(); + while( !releaseA.load() ) + std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) ); + unlockMutex(); + } ); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.holderThread != 0; } ); + const uint32_t threadAId = lockInfo.holderThread; + EXPECT_NE( 0u, threadAId ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + + // Thread B blocks on the lock while A is holding it. + std::thread threadB( [&] { + lockMutex(); + unlockMutex(); + } ); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.waitingThreads.size() == 1; } ); + EXPECT_EQ( 1u, lockInfo.waitingThreads.size() ); + const uint32_t threadBId = lockInfo.waitingThreads.empty() ? 0 : lockInfo.waitingThreads.front(); + EXPECT_NE( 0u, threadBId ); + EXPECT_NE( threadAId, threadBId ); + EXPECT_EQ( threadAId, lockInfo.holderThread ) << "A should still hold the lock while B waits"; + EXPECT_EQ( 2, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + + // Let A unlock; B then obtains and releases the lock. + releaseA.store( true ); + threadA.join(); + threadB.join(); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.releaseCount == 2; } ); + EXPECT_EQ( 2, lockInfo.waitCount ); + EXPECT_EQ( 2, lockInfo.obtainCount ); + EXPECT_EQ( 2, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + + TracyCLockTerminate( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); +} + +// A locks, B waits, C waits, A unlocks, B and C lock/unlock in turn. +TEST_F( CcpTelemetryTest, RawTracyContendedLockWithMultipleWaitingThreads ) +{ + TracyCLockCtx lockCtx; + const uint32_t announceLine = __LINE__ + 1; + TracyCLockAnnounce( lockCtx ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); + ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); + + std::mutex mutex; + auto lockMutex = [&] { + const auto notifyTracy = TracyCLockBeforeLock( lockCtx ); + mutex.lock(); + if( notifyTracy ) + { + TracyCLockAfterLock( lockCtx ); + } + }; + auto unlockMutex = [&] { + mutex.unlock(); + TracyCLockAfterUnlock( lockCtx ); + }; + + std::atomic releaseA{ false }; + std::thread threadA( [&] { + lockMutex(); + while( !releaseA.load() ) + std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) ); + unlockMutex(); + } ); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.holderThread != 0; } ); + const uint32_t threadAId = lockInfo.holderThread; + EXPECT_NE( 0u, threadAId ); + + // B and C block on the lock one after the other while A is holding it. + auto waiterBody = [&] { + lockMutex(); + unlockMutex(); + }; + std::thread threadB( waiterBody ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.waitingThreads.size() == 1; } ); + std::thread threadC( waiterBody ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.waitingThreads.size() == 2; } ); + + EXPECT_EQ( 2u, lockInfo.waitingThreads.size() ); + EXPECT_EQ( threadAId, lockInfo.holderThread ) << "A should still hold the lock while B and C wait"; + EXPECT_EQ( 3, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + if( lockInfo.waitingThreads.size() == 2 ) + { + EXPECT_NE( lockInfo.waitingThreads[0], lockInfo.waitingThreads[1] ); + EXPECT_NE( threadAId, lockInfo.waitingThreads[0] ); + EXPECT_NE( threadAId, lockInfo.waitingThreads[1] ); + } + + // Let A unlock; B and C then obtain and release the lock in whichever + // order the OS wakes them up. + releaseA.store( true ); + threadA.join(); + threadB.join(); + threadC.join(); + + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.releaseCount == 3; } ); + EXPECT_EQ( 3, lockInfo.waitCount ); + EXPECT_EQ( 3, lockInfo.obtainCount ); + EXPECT_EQ( 3, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + + TracyCLockTerminate( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); +} + +// --------------------------------------------------------------------------- +// CcpMutex / CcpAutoMutex +// --------------------------------------------------------------------------- +// CcpMutex announces a Tracy lock in its constructor (when the telemetry is +// connected at that point) and names it "-" via +// TracyCLockCustomName. It reports wait/obtain around EnterCriticalSection in +// Acquire(), reports a release in Release(), and terminates the lock in its +// destructor. The custom name is what identifies a CcpMutex lock; see +// TryGetActiveLockNamed. + +TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruction ) +{ + TracyTestClient::LockInfo lockInfo; + { + CcpMutex mutex( "TelemetryTests", "TestMutex" ); + + // The custom name arrives almost immediately, but the source location + // resolves through extra server-query round trips; wait for both. + TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) && !lockInfo.source.empty(); } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) ); + EXPECT_FALSE( lockInfo.terminated ); + // The owner and name passed to CcpMutex arrive combined as the custom lock name. + EXPECT_EQ( "TelemetryTests-TestMutex", lockInfo.name ); + // The announce site is the TracyCLockAnnounce call in the CcpMutex constructor. + EXPECT_EQ( "CcpMutex", lockInfo.function ); + EXPECT_TRUE( IsAnnouncedFromCcpMutexHeader( lockInfo ) ) << "Unexpected announce site: " << lockInfo.source; + EXPECT_EQ( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + EXPECT_EQ( 0, lockInfo.waitCount ); + EXPECT_EQ( 0, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + } + + // Destroying the mutex terminates its lock. + const uint32_t lockId = lockInfo.id; + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); +} + +TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) +{ + CcpMutex mutex( "TelemetryTests", "TestMutex" ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) ); + const uint32_t lockId = lockInfo.id; + + mutex.Acquire(); + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + EXPECT_NE( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + + mutex.Release(); + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 1, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); +} + +// A acquires the CcpMutex, B waits, A releases, B acquires and releases. +TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) +{ + CcpMutex mutex( "TelemetryTests", "ContendedMutex" ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-ContendedMutex", lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-ContendedMutex", lockInfo ) ); + const uint32_t lockId = lockInfo.id; + + // Thread A acquires the mutex and holds it until we tell it to release. + // The locking must happen on worker threads so that this thread can keep + // ticking the telemetry while the mutex is being held / waited on. + std::atomic releaseA{ false }; + std::thread threadA( [&] { + mutex.Acquire(); + while( !releaseA.load() ) + std::this_thread::sleep_for( std::chrono::milliseconds( 1 ) ); + mutex.Release(); + } ); + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.holderThread != 0; } ); + const uint32_t threadAId = lockInfo.holderThread; + EXPECT_NE( 0u, threadAId ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); + + // Thread B blocks on the mutex while A is holding it. + std::thread threadB( [&] { + mutex.Acquire(); + mutex.Release(); + } ); + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.waitingThreads.size() == 1; } ); + EXPECT_EQ( 1u, lockInfo.waitingThreads.size() ); + const uint32_t threadBId = lockInfo.waitingThreads.empty() ? 0 : lockInfo.waitingThreads.front(); + EXPECT_NE( 0u, threadBId ); + EXPECT_NE( threadAId, threadBId ); + EXPECT_EQ( threadAId, lockInfo.holderThread ) << "A should still hold the mutex while B waits"; + EXPECT_EQ( 2, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + + // Let A release; B then acquires and releases the mutex. + releaseA.store( true ); + threadA.join(); + threadB.join(); + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 2; } ); + EXPECT_EQ( 2, lockInfo.waitCount ); + EXPECT_EQ( 2, lockInfo.obtainCount ); + EXPECT_EQ( 2, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + EXPECT_TRUE( lockInfo.waitingThreads.empty() ); +} + +TEST_F( CcpTelemetryTest, CcpAutoMutexLocksForTheDurationOfItsScope ) +{ + CcpMutex mutex( "TelemetryTests", "AutoMutex" ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-AutoMutex", lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-AutoMutex", lockInfo ) ); + const uint32_t lockId = lockInfo.id; + + { + CcpAutoMutex autoMutex( mutex ); + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + EXPECT_NE( 0u, lockInfo.holderThread ); + } + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 1, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); +} + +TEST_F( CcpTelemetryTest, CcpAutoMutexEarlyReleaseReleasesOnlyOnce ) +{ + CcpMutex mutex( "TelemetryTests", "AutoMutexEarlyRelease" ); + + TracyTestClient::LockInfo lockInfo; + TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-AutoMutexEarlyRelease", lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-AutoMutexEarlyRelease", lockInfo ) ); + const uint32_t lockId = lockInfo.id; + + { + CcpAutoMutex autoMutex( mutex ); + + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_NE( 0u, lockInfo.holderThread ); + + autoMutex.Release(); + TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + EXPECT_EQ( 1, lockInfo.releaseCount ); + EXPECT_EQ( 0u, lockInfo.holderThread ); + } + + // Destroying the CcpAutoMutex after the early release must not release the + // mutex a second time. Tick a little longer to give a (faulty) second + // release event a chance to arrive before asserting it did not. + TickTelemetry( nullptr, std::chrono::milliseconds( 100 ) ); + ASSERT_TRUE( TryGetLock( lockId, lockInfo ) ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 1, lockInfo.releaseCount ); +} + +TEST_F( CcpTelemetryTest, MultipleCcpMutexesAnnounceDistinctLocks ) +{ + // The custom lock names make the two mutexes distinguishable even though + // they share the same announce call site in CcpMutex.h. + CcpMutex firstMutex( "TelemetryTests", "FirstMutex" ); + CcpMutex secondMutex( "TelemetryTests", "SecondMutex" ); + + // GetActiveCcpMutexLocks identifies locks by their (asynchronously + // resolved) announce source location, so wait for that to settle too. + TracyTestClient::LockInfo firstLock; + TracyTestClient::LockInfo secondLock; + TickTelemetry( [&] { + return TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) && + TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ) && + GetActiveCcpMutexLocks().size() == 2; + } ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) ); + ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ) ); + EXPECT_NE( firstLock.id, secondLock.id ); + EXPECT_EQ( 2u, GetActiveCcpMutexLocks().size() ); + + // Each mutex drives its own lock: acquiring the second must not affect the first. + secondMutex.Acquire(); + TickTelemetry( [&] { return TryGetLock( secondLock.id, secondLock ) && secondLock.obtainCount == 1; } ); + EXPECT_EQ( 1, secondLock.obtainCount ); + EXPECT_NE( 0u, secondLock.holderThread ); + ASSERT_TRUE( TryGetLock( firstLock.id, firstLock ) ); + EXPECT_EQ( 0, firstLock.obtainCount ); + EXPECT_EQ( 0u, firstLock.holderThread ); + + secondMutex.Release(); + TickTelemetry( [&] { return TryGetLock( secondLock.id, secondLock ) && secondLock.releaseCount == 1; } ); + EXPECT_EQ( 1, secondLock.releaseCount ); + EXPECT_EQ( 0u, secondLock.holderThread ); +} diff --git a/tests/TracyTestClient.cpp b/tests/TracyTestClient.cpp index e7fa898..c886fab 100644 --- a/tests/TracyTestClient.cpp +++ b/tests/TracyTestClient.cpp @@ -1,6 +1,7 @@ // Copyright © 2025 CCP ehf. #include "TracyTestClient.h" +#include #include #include #include @@ -138,8 +139,10 @@ struct OnDemandPayloadMessage uint64_t currentTime; }; -// Only the server-query value we actually emit. -static constexpr uint8_t kServerQueryFiberName = 7; +// Only the server-query values we actually emit (ServerQuery enum from TracyProtocol.hpp). +static constexpr uint8_t kServerQueryString = 1; +static constexpr uint8_t kServerQuerySourceLocation = 3; +static constexpr uint8_t kServerQueryFiberName = 7; struct ServerQueryPacket { @@ -168,6 +171,57 @@ struct QueueFiberLeave struct QueueStringTransfer { uint64_t ptr; }; +struct QueueLockAnnounce +{ + uint32_t id; + int64_t time; + uint64_t lckloc; // ptr to ___tracy_source_location_data + uint8_t type; // LockType +}; + +struct QueueLockTerminate +{ + uint32_t id; + int64_t time; +}; + +struct QueueLockWait +{ + uint32_t thread; + uint32_t id; + int64_t time; +}; + +struct QueueLockObtain +{ + uint32_t thread; + uint32_t id; + int64_t time; +}; + +struct QueueLockRelease +{ + uint32_t id; + int64_t time; +}; + +// Set via TracyCLockCustomName; the name itself arrives in the +// SingleStringData event immediately preceding this item. +struct QueueLockName +{ + uint32_t id; +}; + +// Reply to a ServerQuerySourceLocation query; name/function/file are string ptrs. +struct QueueSourceLocation +{ + uint64_t name; + uint64_t function; + uint64_t file; + uint32_t line; + uint8_t r, g, b; +}; + // QueueItem matches Tracy's 32-byte packed union layout. struct QueueItem { @@ -177,6 +231,13 @@ struct QueueItem QueueFiberEnter fiberEnter; QueueFiberLeave fiberLeave; QueueStringTransfer stringTransfer; + QueueLockAnnounce lockAnnounce; + QueueLockTerminate lockTerminate; + QueueLockWait lockWait; + QueueLockObtain lockObtain; + QueueLockRelease lockRelease; + QueueLockName lockName; + QueueSourceLocation srcloc; char _pad[31]; }; }; @@ -190,13 +251,21 @@ static constexpr uint8_t kQueueZoneBeginAllocSrcLocCallstack = 8; static constexpr uint8_t kQueueZoneBegin = 15; static constexpr uint8_t kQueueZoneBeginCallstack = 16; static constexpr uint8_t kQueueZoneEnd = 17; +static constexpr uint8_t kQueueLockWait = 18; +static constexpr uint8_t kQueueLockObtain = 19; +static constexpr uint8_t kQueueLockRelease = 20; +static constexpr uint8_t kQueueLockName = 24; static constexpr uint8_t kQueueFiberEnter = 58; static constexpr uint8_t kQueueFiberLeave = 59; static constexpr uint8_t kQueueTerminate = 60; static constexpr uint8_t kQueueThreadContext = 62; +static constexpr uint8_t kQueueSourceLocation = 74; +static constexpr uint8_t kQueueLockAnnounce = 75; +static constexpr uint8_t kQueueLockTerminate = 76; static constexpr uint8_t kQueueSingleStringData = 99; static constexpr uint8_t kQueueSecondStringData = 100; static constexpr uint8_t kQueueStringDataFirst = 104; // indices >= this carry QueueStringTransfer +static constexpr uint8_t kQueueStringData = 104; static constexpr uint8_t kQueueSourceLocationPayload = 107; static constexpr uint8_t kQueueFrameImageData = 111; static constexpr uint8_t kQueueSymbolCode = 114; @@ -473,6 +542,38 @@ std::vector TracyTestClient::GetFiberNames() const return names; } +std::vector TracyTestClient::GetLocks() const +{ + std::lock_guard lock( m_dataMutex ); + std::vector result; + result.reserve( m_locks.size() ); + for( const auto& [id, info] : m_locks ) + result.push_back( info ); + return result; +} + +std::vector TracyTestClient::GetActiveLocks() const +{ + std::lock_guard lock( m_dataMutex ); + std::vector result; + for( const auto& [id, info] : m_locks ) + { + if( !info.terminated ) + result.push_back( info ); + } + return result; +} + +bool TracyTestClient::TryGetLock( uint32_t id, LockInfo& outLock ) const +{ + std::lock_guard lock( m_dataMutex ); + auto it = m_locks.find( id ); + if( it == m_locks.end() ) + return false; + outLock = it->second; + return true; +} + // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- @@ -545,6 +646,24 @@ TracyTestClient::ZoneStack& TracyTestClient::CurrentStack( uint32_t thread ) return m_threadZoneStacks[thread]; } +TracyTestClient::LockInfo& TracyTestClient::LockById( uint32_t id ) +{ + auto& info = m_locks[id]; + info.id = id; + return info; +} + +void TracyTestClient::RequestLockString( uint64_t ptr, uint32_t lockId, int field ) +{ + auto& pending = m_pendingLockStrings[ptr]; + // Several locks can share a string pointer (e.g. the source file); query the + // profiler only once per pointer while a reply is outstanding. + const bool alreadyQueried = !pending.empty(); + pending.push_back( { lockId, field } ); + if( !alreadyQueried ) + SendQueryLocked( kServerQueryString, ptr ); +} + // Parse the decompressed byte stream and update internal state. void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) { @@ -619,6 +738,29 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) std::lock_guard lock( m_dataMutex ); m_fiberNames[strPtr] = std::move( name ); } + else if( idx == kQueueStringData ) + { + // Reply to a ServerQueryString we sent while resolving a + // lock source location; strPtr echoes the queried pointer. + std::lock_guard lock( m_dataMutex ); + auto pendingIt = m_pendingLockStrings.find( strPtr ); + if( pendingIt != m_pendingLockStrings.end() ) + { + const std::string value( ptr, strSz ); + for( const auto& target : pendingIt->second ) + { + auto& info = LockById( target.lockId ); + switch( target.field ) + { + case 0: info.name = value; break; + case 1: info.function = value; break; + case 2: info.source = value; break; + default: break; + } + } + m_pendingLockStrings.erase( pendingIt ); + } + } ptr += strSz; } @@ -634,6 +776,10 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) std::memcpy( &strSz, ptr, sizeof( strSz ) ); ptr += sizeof( strSz ); if( ptr + strSz > end ) return; + // Remember the payload: fat-pointer items (e.g. LockName) are + // preceded by a SingleStringData event carrying their string. + if( idx == kQueueSingleStringData ) + m_pendingSingleString.assign( ptr, strSz ); ptr += strSz; } else @@ -678,6 +824,88 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } + case kQueueLockAnnounce: + { + m_lockAnnounceCount.fetch_add( 1, std::memory_order_relaxed ); + const uint32_t lockId = item->lockAnnounce.id; + const uint64_t srcloc = item->lockAnnounce.lckloc; + std::lock_guard lock( m_dataMutex ); + LockById( lockId ); + // Resolve the announce call site. The reply carries no request + // pointer, so remember which lock the next reply belongs to. + m_pendingLockSrcLocs.push_back( lockId ); + SendQueryLocked( kServerQuerySourceLocation, srcloc ); + break; + } + + case kQueueLockTerminate: + { + m_lockTerminateCount.fetch_add( 1, std::memory_order_relaxed ); + std::lock_guard lock( m_dataMutex ); + LockById( item->lockTerminate.id ).terminated = true; + break; + } + + case kQueueLockWait: + { + m_lockWaitCount.fetch_add( 1, std::memory_order_relaxed ); + std::lock_guard lock( m_dataMutex ); + auto& info = LockById( item->lockWait.id ); + ++info.waitCount; + info.waitingThreads.push_back( item->lockWait.thread ); + break; + } + + case kQueueLockObtain: + { + m_lockObtainCount.fetch_add( 1, std::memory_order_relaxed ); + const uint32_t thread = item->lockObtain.thread; + std::lock_guard lock( m_dataMutex ); + auto& info = LockById( item->lockObtain.id ); + ++info.obtainCount; + info.holderThread = thread; + auto& waiting = info.waitingThreads; + auto waitIt = std::find( waiting.begin(), waiting.end(), thread ); + if( waitIt != waiting.end() ) + waiting.erase( waitIt ); + break; + } + + case kQueueLockRelease: + { + m_lockReleaseCount.fetch_add( 1, std::memory_order_relaxed ); + std::lock_guard lock( m_dataMutex ); + auto& info = LockById( item->lockRelease.id ); + ++info.releaseCount; + info.holderThread = 0; + break; + } + + case kQueueLockName: + { + std::lock_guard lock( m_dataMutex ); + LockById( item->lockName.id ).name = m_pendingSingleString; + break; + } + + case kQueueSourceLocation: + { + std::lock_guard lock( m_dataMutex ); + if( !m_pendingLockSrcLocs.empty() ) + { + const uint32_t lockId = m_pendingLockSrcLocs.front(); + m_pendingLockSrcLocs.pop_front(); + LockById( lockId ).line = item->srcloc.line; + if( item->srcloc.name != 0 ) + RequestLockString( item->srcloc.name, lockId, 0 ); + if( item->srcloc.function != 0 ) + RequestLockString( item->srcloc.function, lockId, 1 ); + if( item->srcloc.file != 0 ) + RequestLockString( item->srcloc.file, lockId, 2 ); + } + break; + } + case kQueueFiberEnter: { const uint64_t fiberPtr = item->fiberEnter.fiber; diff --git a/tests/TracyTestClient.h b/tests/TracyTestClient.h index 410b378..d6da4a8 100644 --- a/tests/TracyTestClient.h +++ b/tests/TracyTestClient.h @@ -2,6 +2,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,28 @@ class TracyTestClient using ZoneStack = std::vector; + // State of a lockable announced via TracyCLockAnnounce, accumulated from + // LockAnnounce / LockTerminate / LockWait / LockObtain / LockRelease events. + // The source location (function/source/line) identifies the TracyCLockAnnounce + // call site and is resolved asynchronously through server queries, so it may + // be empty briefly after the announce event arrives. + struct LockInfo + { + uint32_t id = 0; + // Custom name set via TracyCLockCustomName (LockName event); falls back + // to the srcloc name, which is empty for locks announced via the C API. + std::string name; + std::string function; + std::string source; + uint32_t line = 0; + bool terminated = false; // a LockTerminate event was received + uint32_t holderThread = 0; // thread currently holding the lock, 0 = none + std::vector waitingThreads; // threads between LockWait and LockObtain + int waitCount = 0; // LockWait events (TracyCLockBeforeLock) + int obtainCount = 0; // LockObtain events (TracyCLockAfterLock) + int releaseCount = 0; // LockRelease events (TracyCLockAfterUnlock) + }; + TracyTestClient(); ~TracyTestClient(); @@ -47,6 +70,24 @@ class TracyTestClient std::vector GetFiberNames() const; + // Global event counters for each lock state transition. + // Note: LockAnnounce/LockTerminate are deferred items in Tracy, so they are + // replayed for previously announced locks on every new connection. Within a + // process that runs several tests, these two counters therefore also include + // locks announced before this client connected. + int GetLockAnnounceCount() const { return m_lockAnnounceCount.load( std::memory_order_relaxed ); } + int GetLockTerminateCount() const { return m_lockTerminateCount.load( std::memory_order_relaxed ); } + int GetLockWaitCount() const { return m_lockWaitCount.load( std::memory_order_relaxed ); } + int GetLockObtainCount() const { return m_lockObtainCount.load( std::memory_order_relaxed ); } + int GetLockReleaseCount() const { return m_lockReleaseCount.load( std::memory_order_relaxed ); } + + // Returns all locks this client has seen (including terminated ones). + std::vector GetLocks() const; + // Returns all announced locks that have not been terminated yet. + std::vector GetActiveLocks() const; + // Looks up a single lock by its Tracy lock id. + bool TryGetLock( uint32_t id, LockInfo& outLock ) const; + TracyTestClient( const TracyTestClient& ) = delete; TracyTestClient& operator=( const TracyTestClient& ) = delete; @@ -59,6 +100,14 @@ class TracyTestClient // Must be called with m_dataMutex held. ZoneStack& CurrentStack( uint32_t thread ); + // Returns the LockInfo for the given lock id, creating it if necessary. + // Must be called with m_dataMutex held. + LockInfo& LockById( uint32_t id ); + + // Queries the string behind ptr from the profiler and routes the reply into + // the given LockInfo field. Must be called with m_dataMutex held. + void RequestLockString( uint64_t ptr, uint32_t lockId, int field ); + // Opaque handles to Tracy types, allocated on heap to keep Tracy headers out of this header. void* m_socket = nullptr; // tracy::Socket* void* m_lz4Stream = nullptr; // LZ4_streamDecode_t* @@ -73,6 +122,11 @@ class TracyTestClient std::atomic m_shutdown{ false }; std::atomic m_zoneBeginCount{ 0 }; std::atomic m_zoneEndCount{ 0 }; + std::atomic m_lockAnnounceCount{ 0 }; + std::atomic m_lockTerminateCount{ 0 }; + std::atomic m_lockWaitCount{ 0 }; + std::atomic m_lockObtainCount{ 0 }; + std::atomic m_lockReleaseCount{ 0 }; // Current thread established by ThreadContext events (recv thread only, no mutex needed). uint32_t m_currentThread = 0; @@ -85,9 +139,28 @@ class TracyTestClient ZoneInfo m_pendingZone; bool m_hasPendingZone = false; + // Payload of the most recent SingleStringData event, to be consumed by the + // following fat-pointer item (e.g. LockName). Recv thread only. + std::string m_pendingSingleString; + std::unordered_map m_threadCurrentFiber; // thread id → active fiber ptr (0 = none) std::unordered_map m_threadZoneStacks; // thread id → zone stack std::unordered_map m_fiberZoneStacks; // fiber ptr → zone stack std::unordered_map m_fiberNames; // fiber ptr → name std::unordered_set m_queriedFibers; // ptrs already queried + + std::unordered_map m_locks; // lock id → state + + // SourceLocation replies carry no request pointer; the profiler answers + // queries in order, so match replies FIFO against the announcing lock ids. + std::deque m_pendingLockSrcLocs; + + // String replies do echo the queried pointer. Several locks may share a + // string (e.g. the source file), so each pointer maps to all destinations. + struct PendingLockString + { + uint32_t lockId; + int field; // 0 = name, 1 = function, 2 = source + }; + std::unordered_map> m_pendingLockStrings; }; From 3c0643bc251244d9078fbc70c4bd671b31d58f5d Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:11:15 +0000 Subject: [PATCH 02/11] Cleanup CcpMutex.h and Submodule update --- include/CcpMutex.h | 20 +++++++++---------- vendor/github.com/carbonengine/vcpkg-registry | 2 +- vendor/github.com/microsoft/vcpkg | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/include/CcpMutex.h b/include/CcpMutex.h index d5d5680..6039334 100644 --- a/include/CcpMutex.h +++ b/include/CcpMutex.h @@ -3,11 +3,11 @@ #ifndef CCPMUTEX_H #define CCPMUTEX_H -#include "CcpTelemetry.h" -#include "CcpAtomic.h" - #include +#include "CcpAtomic.h" +#include "CcpTelemetry.h" + #ifdef _WIN32 class CcpMutex @@ -23,9 +23,8 @@ class CcpMutex #if CCP_TELEMETRY_ENABLED if ( CcpTelemetryIsConnected() ) { + const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); TracyCLockAnnounce( m_tracyLockContext ); - // Tracy copies the name, so passing a temporary is fine. - const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); } #endif @@ -47,14 +46,14 @@ class CcpMutex { #if CCP_TELEMETRY_ENABLED bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif EnterCriticalSection( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker + if ( notifyTracy && CcpTelemetryIsConnected() && m_tracyLockContext ) { TracyCLockAfterLock( m_tracyLockContext ); } @@ -119,9 +118,8 @@ class CcpMutex #if CCP_TELEMETRY_ENABLED if ( CcpTelemetryIsConnected() ) { + const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); TracyCLockAnnounce( m_tracyLockContext ); - // Tracy copies the name, so passing a temporary is fine. - const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); } #endif @@ -143,14 +141,14 @@ class CcpMutex { #if CCP_TELEMETRY_ENABLED bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker + if ( CcpTelemetryIsConnected() && m_tracyLockContext ) { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif pthread_mutex_lock( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) // TODO: Can this be changed to CcpTelemetryIsStarted(), should be quicker + if ( notifyTracy && CcpTelemetryIsConnected() && m_tracyLockContext ) { TracyCLockAfterLock( m_tracyLockContext ); } diff --git a/vendor/github.com/carbonengine/vcpkg-registry b/vendor/github.com/carbonengine/vcpkg-registry index de86dca..a578f47 160000 --- a/vendor/github.com/carbonengine/vcpkg-registry +++ b/vendor/github.com/carbonengine/vcpkg-registry @@ -1 +1 @@ -Subproject commit de86dcad60458ef170911adb5c42a053fc5d9117 +Subproject commit a578f475b76a204b85f777692adc26bde7cdf01c diff --git a/vendor/github.com/microsoft/vcpkg b/vendor/github.com/microsoft/vcpkg index b2c7468..44819aa 160000 --- a/vendor/github.com/microsoft/vcpkg +++ b/vendor/github.com/microsoft/vcpkg @@ -1 +1 @@ -Subproject commit b2c74683ecfd6a8e7d27ffb0df077f66a9339509 +Subproject commit 44819aa2a6c10e56065e2b0330e7d6c89d1d2574 From 78789af0deaeb2942b278552ad3c38ac02a06d22 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:36:23 +0000 Subject: [PATCH 03/11] First pass of cleanup of Lock test files. Review of classes TracyTestClient and test/CcpTelemetry --- tests/CcpTelemetry.cpp | 105 ++++++++++++-------------------------- tests/TracyTestClient.cpp | 20 ++++---- tests/TracyTestClient.h | 29 ++++------- 3 files changed, 55 insertions(+), 99 deletions(-) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 16a28f0..a2ce242 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -133,20 +133,17 @@ class CcpTelemetryTest : public ::testing::Test return tracyZones.end() != std::find_if( tracyZones.begin(), tracyZones.end(), pred ); } - // Finds the lock announced via TracyCLockAnnounce at the given line of this - // file. Locks are identified by their announce call site because absolute - // lock ids and announce counts are not stable across tests: Tracy defers - // LockAnnounce/LockTerminate events and replays them on every new - // connection, so locks from earlier tests reappear here. For the same - // reason, when several locks match the call site (e.g. when a test is - // repeated within one process), the most recently announced one wins. - // The source location is resolved through asynchronous server queries, so - // this returns false until those have completed; call it from a - // TickTelemetry predicate. + // Helper for Raw lock tests, finding lock announced via TracyCLockAnnounce at a given line. + // Locks are identified by their announce call site because absolute lock ids and announce counts + // are not stable across tests: Tracy defers LockAnnounce/LockTerminate events and replays them + // on every new connection, so locks from earlier tests reappear here. + // For the same reason, when several locks match the call site (e.g. when a test is repeated + // within one process), the most recently announced one wins. + // Call it as a TickTelemetry predicate. bool TryGetLockAtLine( uint32_t line, TracyTestClient::LockInfo& outLock ) { bool found = false; - for( const auto& lock : m_tracyClient.GetLocks() ) + for( const auto& lock : m_tracyClient.GetAllLocks() ) { if( lock.line == line && lock.source == __FILE__ && ( !found || lock.id > outLock.id ) ) { @@ -157,40 +154,11 @@ class CcpTelemetryTest : public ::testing::Test return found; } - // Returns true when the lock was announced from CcpMutex.h. MSVC records - // __FILE__ of included headers in lower case, so compare case-insensitively. - static bool IsAnnouncedFromCcpMutexHeader( const TracyTestClient::LockInfo& lock ) - { - std::string source = lock.source; - std::transform( source.begin(), source.end(), source.begin(), - []( unsigned char c ) { return static_cast( std::tolower( c ) ); } ); - return source.find( "ccpmutex.h" ) != std::string::npos; - } - - // All CcpMutex instances share the same announce call site in CcpMutex.h, so - // they cannot be told apart by source location. This returns all locks - // announced from CcpMutex.h that have not been terminated. Source locations - // resolve through asynchronous server queries, so a just-announced lock - // shows up here only once its source string has arrived; call this from a - // TickTelemetry predicate. - std::vector GetActiveCcpMutexLocks() - { - std::vector result; - for( const auto& lock : m_tracyClient.GetActiveLocks() ) - { - if( IsAnnouncedFromCcpMutexHeader( lock ) ) - result.push_back( lock ); - } - return result; - } - - // Finds the active lock carrying the given custom name. CcpMutex names its - // lock "-" via TracyCLockCustomName in the constructor. The - // name arrives asynchronously shortly after the announce event, so call - // this from a TickTelemetry predicate. When a test is repeated within one - // process, Tracy replays earlier (terminated) locks carrying the same name; - // those are terminated by the time any event of the running test is - // visible, and the most recently announced lock wins by id regardless. + // Helper for CcpMutex tests, finding active locks by the given custom name. + // CcpMutex names its lock "-" via TracyCLockCustomName in the constructor. + // Name arrives async shortly after announce event, call function from a TickTelemetry predicate. + // When a test is repeated within one process, Tracy replays earlier (terminated) locks having + // the same name, where the most recently announced lock wins by id. bool TryGetActiveLockNamed( const std::string& name, TracyTestClient::LockInfo& outLock ) { bool found = false; @@ -206,7 +174,7 @@ class CcpTelemetryTest : public ::testing::Test } // Refreshes a previously identified lock by id. - bool TryGetLock( uint32_t lockId, TracyTestClient::LockInfo& outLock ) + bool TryGetLockById( uint32_t lockId, TracyTestClient::LockInfo& outLock ) { return m_tracyClient.TryGetLock( lockId, outLock ); } @@ -547,11 +515,10 @@ TEST_F( CcpTelemetryTest, RawTracyContendedLockWithMultipleWaitingThreads ) // CcpMutex / CcpAutoMutex // --------------------------------------------------------------------------- // CcpMutex announces a Tracy lock in its constructor (when the telemetry is -// connected at that point) and names it "-" via -// TracyCLockCustomName. It reports wait/obtain around EnterCriticalSection in -// Acquire(), reports a release in Release(), and terminates the lock in its -// destructor. The custom name is what identifies a CcpMutex lock; see -// TryGetActiveLockNamed. +// connected at that point) and names it "-" via TracyCLockCustomName. +// It reports wait/obtain around EnterCriticalSection in Acquire(), release in +// Release() and terminates in destructor. +// The custom name is what currently identifies a CcpMutex lock; see TryGetActiveLockNamed. TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruction ) { @@ -566,9 +533,8 @@ TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruct EXPECT_FALSE( lockInfo.terminated ); // The owner and name passed to CcpMutex arrive combined as the custom lock name. EXPECT_EQ( "TelemetryTests-TestMutex", lockInfo.name ); - // The announce site is the TracyCLockAnnounce call in the CcpMutex constructor. + // The announce site is the TracyCLockAnnounce call in the CcpMutex constructor (function). EXPECT_EQ( "CcpMutex", lockInfo.function ); - EXPECT_TRUE( IsAnnouncedFromCcpMutexHeader( lockInfo ) ) << "Unexpected announce site: " << lockInfo.source; EXPECT_EQ( 0u, lockInfo.holderThread ); EXPECT_TRUE( lockInfo.waitingThreads.empty() ); EXPECT_EQ( 0, lockInfo.waitCount ); @@ -578,7 +544,7 @@ TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruct // Destroying the mutex terminates its lock. const uint32_t lockId = lockInfo.id; - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.terminated; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.terminated; } ); EXPECT_TRUE( lockInfo.terminated ); } @@ -592,7 +558,7 @@ TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) const uint32_t lockId = lockInfo.id; mutex.Acquire(); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 0, lockInfo.releaseCount ); @@ -600,7 +566,7 @@ TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) EXPECT_TRUE( lockInfo.waitingThreads.empty() ); mutex.Release(); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 1, lockInfo.releaseCount ); @@ -628,7 +594,7 @@ TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) mutex.Release(); } ); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.holderThread != 0; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.holderThread != 0; } ); const uint32_t threadAId = lockInfo.holderThread; EXPECT_NE( 0u, threadAId ); EXPECT_EQ( 1, lockInfo.waitCount ); @@ -641,7 +607,7 @@ TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) mutex.Release(); } ); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.waitingThreads.size() == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.waitingThreads.size() == 1; } ); EXPECT_EQ( 1u, lockInfo.waitingThreads.size() ); const uint32_t threadBId = lockInfo.waitingThreads.empty() ? 0 : lockInfo.waitingThreads.front(); EXPECT_NE( 0u, threadBId ); @@ -656,7 +622,7 @@ TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) threadA.join(); threadB.join(); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 2; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.releaseCount == 2; } ); EXPECT_EQ( 2, lockInfo.waitCount ); EXPECT_EQ( 2, lockInfo.obtainCount ); EXPECT_EQ( 2, lockInfo.releaseCount ); @@ -676,14 +642,14 @@ TEST_F( CcpTelemetryTest, CcpAutoMutexLocksForTheDurationOfItsScope ) { CcpAutoMutex autoMutex( mutex ); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); EXPECT_EQ( 1, lockInfo.waitCount ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 0, lockInfo.releaseCount ); EXPECT_NE( 0u, lockInfo.holderThread ); } - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 1, lockInfo.releaseCount ); EXPECT_EQ( 0u, lockInfo.holderThread ); @@ -701,12 +667,12 @@ TEST_F( CcpTelemetryTest, CcpAutoMutexEarlyReleaseReleasesOnlyOnce ) { CcpAutoMutex autoMutex( mutex ); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.obtainCount == 1; } ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_NE( 0u, lockInfo.holderThread ); autoMutex.Release(); - TickTelemetry( [&] { return TryGetLock( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( lockId, lockInfo ) && lockInfo.releaseCount == 1; } ); EXPECT_EQ( 1, lockInfo.releaseCount ); EXPECT_EQ( 0u, lockInfo.holderThread ); } @@ -715,7 +681,7 @@ TEST_F( CcpTelemetryTest, CcpAutoMutexEarlyReleaseReleasesOnlyOnce ) // mutex a second time. Tick a little longer to give a (faulty) second // release event a chance to arrive before asserting it did not. TickTelemetry( nullptr, std::chrono::milliseconds( 100 ) ); - ASSERT_TRUE( TryGetLock( lockId, lockInfo ) ); + ASSERT_TRUE( TryGetLockById( lockId, lockInfo ) ); EXPECT_EQ( 1, lockInfo.obtainCount ); EXPECT_EQ( 1, lockInfo.releaseCount ); } @@ -727,31 +693,28 @@ TEST_F( CcpTelemetryTest, MultipleCcpMutexesAnnounceDistinctLocks ) CcpMutex firstMutex( "TelemetryTests", "FirstMutex" ); CcpMutex secondMutex( "TelemetryTests", "SecondMutex" ); - // GetActiveCcpMutexLocks identifies locks by their (asynchronously // resolved) announce source location, so wait for that to settle too. TracyTestClient::LockInfo firstLock; TracyTestClient::LockInfo secondLock; TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) && - TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ) && - GetActiveCcpMutexLocks().size() == 2; + TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ); } ); ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) ); ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ) ); EXPECT_NE( firstLock.id, secondLock.id ); - EXPECT_EQ( 2u, GetActiveCcpMutexLocks().size() ); // Each mutex drives its own lock: acquiring the second must not affect the first. secondMutex.Acquire(); - TickTelemetry( [&] { return TryGetLock( secondLock.id, secondLock ) && secondLock.obtainCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( secondLock.id, secondLock ) && secondLock.obtainCount == 1; } ); EXPECT_EQ( 1, secondLock.obtainCount ); EXPECT_NE( 0u, secondLock.holderThread ); - ASSERT_TRUE( TryGetLock( firstLock.id, firstLock ) ); + ASSERT_TRUE( TryGetLockById( firstLock.id, firstLock ) ); EXPECT_EQ( 0, firstLock.obtainCount ); EXPECT_EQ( 0u, firstLock.holderThread ); secondMutex.Release(); - TickTelemetry( [&] { return TryGetLock( secondLock.id, secondLock ) && secondLock.releaseCount == 1; } ); + TickTelemetry( [&] { return TryGetLockById( secondLock.id, secondLock ) && secondLock.releaseCount == 1; } ); EXPECT_EQ( 1, secondLock.releaseCount ); EXPECT_EQ( 0u, secondLock.holderThread ); } diff --git a/tests/TracyTestClient.cpp b/tests/TracyTestClient.cpp index c886fab..d83dff4 100644 --- a/tests/TracyTestClient.cpp +++ b/tests/TracyTestClient.cpp @@ -542,7 +542,7 @@ std::vector TracyTestClient::GetFiberNames() const return names; } -std::vector TracyTestClient::GetLocks() const +std::vector TracyTestClient::GetAllLocks() const { std::lock_guard lock( m_dataMutex ); std::vector result; @@ -646,7 +646,7 @@ TracyTestClient::ZoneStack& TracyTestClient::CurrentStack( uint32_t thread ) return m_threadZoneStacks[thread]; } -TracyTestClient::LockInfo& TracyTestClient::LockById( uint32_t id ) +TracyTestClient::LockInfo& TracyTestClient::GetOrCreateLockById( uint32_t id ) { auto& info = m_locks[id]; info.id = id; @@ -749,7 +749,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) const std::string value( ptr, strSz ); for( const auto& target : pendingIt->second ) { - auto& info = LockById( target.lockId ); + auto& info = GetOrCreateLockById( target.lockId ); switch( target.field ) { case 0: info.name = value; break; @@ -830,7 +830,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) const uint32_t lockId = item->lockAnnounce.id; const uint64_t srcloc = item->lockAnnounce.lckloc; std::lock_guard lock( m_dataMutex ); - LockById( lockId ); + GetOrCreateLockById( lockId ); // Resolve the announce call site. The reply carries no request // pointer, so remember which lock the next reply belongs to. m_pendingLockSrcLocs.push_back( lockId ); @@ -842,7 +842,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) { m_lockTerminateCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); - LockById( item->lockTerminate.id ).terminated = true; + GetOrCreateLockById( item->lockTerminate.id ).terminated = true; break; } @@ -850,7 +850,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) { m_lockWaitCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); - auto& info = LockById( item->lockWait.id ); + auto& info = GetOrCreateLockById( item->lockWait.id ); ++info.waitCount; info.waitingThreads.push_back( item->lockWait.thread ); break; @@ -861,7 +861,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) m_lockObtainCount.fetch_add( 1, std::memory_order_relaxed ); const uint32_t thread = item->lockObtain.thread; std::lock_guard lock( m_dataMutex ); - auto& info = LockById( item->lockObtain.id ); + auto& info = GetOrCreateLockById( item->lockObtain.id ); ++info.obtainCount; info.holderThread = thread; auto& waiting = info.waitingThreads; @@ -875,7 +875,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) { m_lockReleaseCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); - auto& info = LockById( item->lockRelease.id ); + auto& info = GetOrCreateLockById( item->lockRelease.id ); ++info.releaseCount; info.holderThread = 0; break; @@ -884,7 +884,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) case kQueueLockName: { std::lock_guard lock( m_dataMutex ); - LockById( item->lockName.id ).name = m_pendingSingleString; + GetOrCreateLockById( item->lockName.id ).name = m_pendingSingleString; break; } @@ -895,7 +895,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) { const uint32_t lockId = m_pendingLockSrcLocs.front(); m_pendingLockSrcLocs.pop_front(); - LockById( lockId ).line = item->srcloc.line; + GetOrCreateLockById( lockId ).line = item->srcloc.line; if( item->srcloc.name != 0 ) RequestLockString( item->srcloc.name, lockId, 0 ); if( item->srcloc.function != 0 ) diff --git a/tests/TracyTestClient.h b/tests/TracyTestClient.h index d6da4a8..203507e 100644 --- a/tests/TracyTestClient.h +++ b/tests/TracyTestClient.h @@ -27,22 +27,15 @@ class TracyTestClient using ZoneStack = std::vector; - // State of a lockable announced via TracyCLockAnnounce, accumulated from - // LockAnnounce / LockTerminate / LockWait / LockObtain / LockRelease events. - // The source location (function/source/line) identifies the TracyCLockAnnounce - // call site and is resolved asynchronously through server queries, so it may - // be empty briefly after the announce event arrives. struct LockInfo { uint32_t id = 0; - // Custom name set via TracyCLockCustomName (LockName event); falls back - // to the srcloc name, which is empty for locks announced via the C API. - std::string name; + std::string name; // TracyCLockCustomName() or source location (function/source/line) from TracyCLockAnnounce() std::string function; std::string source; uint32_t line = 0; - bool terminated = false; // a LockTerminate event was received - uint32_t holderThread = 0; // thread currently holding the lock, 0 = none + bool terminated = false; // LockTerminate event was received + uint32_t holderThread = 0; // thread currently holding the lock, 0 = none std::vector waitingThreads; // threads between LockWait and LockObtain int waitCount = 0; // LockWait events (TracyCLockBeforeLock) int obtainCount = 0; // LockObtain events (TracyCLockAfterLock) @@ -58,6 +51,7 @@ class TracyTestClient void Disconnect(); bool IsConnected() const; + // Global counters for ZoneBegin/End states int GetZoneBeginCount() const { return m_zoneBeginCount.load( std::memory_order_relaxed ); } int GetZoneEndCount() const { return m_zoneEndCount.load( std::memory_order_relaxed ); } @@ -70,11 +64,10 @@ class TracyTestClient std::vector GetFiberNames() const; - // Global event counters for each lock state transition. - // Note: LockAnnounce/LockTerminate are deferred items in Tracy, so they are - // replayed for previously announced locks on every new connection. Within a - // process that runs several tests, these two counters therefore also include - // locks announced before this client connected. + // Global counters for different internal Lock states (Announce, Wait, Obtain, Release, Terminate). + // Note: LockAnnounce/LockTerminate are deferred items in Tracy, so they are replayed for previously + // announced locks on every new connection. Within a process that runs several tests, these two counters + // therefore also include locks announced before this client connected. int GetLockAnnounceCount() const { return m_lockAnnounceCount.load( std::memory_order_relaxed ); } int GetLockTerminateCount() const { return m_lockTerminateCount.load( std::memory_order_relaxed ); } int GetLockWaitCount() const { return m_lockWaitCount.load( std::memory_order_relaxed ); } @@ -82,7 +75,7 @@ class TracyTestClient int GetLockReleaseCount() const { return m_lockReleaseCount.load( std::memory_order_relaxed ); } // Returns all locks this client has seen (including terminated ones). - std::vector GetLocks() const; + std::vector GetAllLocks() const; // Returns all announced locks that have not been terminated yet. std::vector GetActiveLocks() const; // Looks up a single lock by its Tracy lock id. @@ -102,7 +95,7 @@ class TracyTestClient // Returns the LockInfo for the given lock id, creating it if necessary. // Must be called with m_dataMutex held. - LockInfo& LockById( uint32_t id ); + LockInfo& GetOrCreateLockById( uint32_t id ); // Queries the string behind ptr from the profiler and routes the reply into // the given LockInfo field. Must be called with m_dataMutex held. @@ -149,7 +142,7 @@ class TracyTestClient std::unordered_map m_fiberNames; // fiber ptr → name std::unordered_set m_queriedFibers; // ptrs already queried - std::unordered_map m_locks; // lock id → state + std::unordered_map m_locks; // lock id → LockInfo state // SourceLocation replies carry no request pointer; the profiler answers // queries in order, so match replies FIFO against the announcing lock ids. From ee54828141173489953ba62f9291fcb6efd306e7 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:19:43 +0000 Subject: [PATCH 04/11] Create a vanilla RawTracyLockCounters test This tests the vanilla behavior of the raw underlying TracyCLockXXX() functions. --- tests/CcpTelemetry.cpp | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index a2ce242..dca40de 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -145,6 +145,7 @@ class CcpTelemetryTest : public ::testing::Test bool found = false; for( const auto& lock : m_tracyClient.GetAllLocks() ) { + // All tests are in this file, hence lock.source == __FILE__ if( lock.line == line && lock.source == __FILE__ && ( !found || lock.id > outLock.id ) ) { outLock = lock; @@ -295,6 +296,63 @@ TEST_F( CcpTelemetryTest, StartStopStartTelemetryWhileClientIsRunning ) EXPECT_EQ( 2, m_tracyClient.GetZoneEndCount() ); } +TEST_F( CcpTelemetryTest, RawTracyLockCounters ) +{ + TracyCLockCtx lockCtx; + TracyTestClient::LockInfo lockInfo; + std::string lockName = "CcpTelemetryTest-RawTracyLockCounters"; + EXPECT_EQ( 0, m_tracyClient.GetLockAnnounceCount() ); + EXPECT_EQ( 0, m_tracyClient.GetLockWaitCount() ); + EXPECT_EQ( 0, m_tracyClient.GetLockObtainCount() ); + EXPECT_EQ( 0, m_tracyClient.GetLockReleaseCount() ); + EXPECT_EQ( 0, m_tracyClient.GetLockTerminateCount() ); + + // Announce Lock: + const uint32_t announceLine = __LINE__ + 1; // The line where we call TracyCLockAnnounce() + TracyCLockAnnounce( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ); } ); + EXPECT_EQ( "", lockInfo.name ) << "Name is only set in TracyCLockCustomName"; + EXPECT_EQ( announceLine, lockInfo.line ); + EXPECT_EQ( 1, m_tracyClient.GetLockAnnounceCount() ); + + // Give the Lock a name: + TracyCLockCustomName( lockCtx, lockName.c_str(), lockName.size() ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.name == lockName; } ); + EXPECT_EQ( lockName, lockInfo.name ); + EXPECT_FALSE( lockInfo.terminated ); + EXPECT_EQ( 0, size(lockInfo.waitingThreads) ); + EXPECT_EQ( 0, lockInfo.waitCount ); + EXPECT_EQ( 0, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + + // Before Lock Acquire: + const auto notifyTracy = TracyCLockBeforeLock( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && (lockInfo.obtainCount == 1 || lockInfo.waitCount == 1); } ); + EXPECT_TRUE( notifyTracy ); + EXPECT_EQ( 1, lockInfo.waitCount + lockInfo.obtainCount ) << "Sum of Wait+Obtain needs to match"; + EXPECT_EQ( 1, m_tracyClient.GetLockObtainCount() + m_tracyClient.GetLockWaitCount() ) << "Sum of Wait+Obtain needs to match"; + + // After Lock Acquire: + TracyCLockAfterLock( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.obtainCount == 1; } ); + EXPECT_EQ( 1, lockInfo.waitCount ); + EXPECT_EQ( 1, lockInfo.obtainCount ); + EXPECT_EQ( 0, lockInfo.releaseCount ); + EXPECT_EQ( 1, m_tracyClient.GetLockObtainCount() ); + EXPECT_EQ( 1, m_tracyClient.GetLockWaitCount() ); + + // After Lock Release: + TracyCLockAfterUnlock( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.releaseCount == 1; } ); + EXPECT_EQ( 1, lockInfo.releaseCount ); + EXPECT_EQ( 1, m_tracyClient.GetLockReleaseCount() ); + + // Remove the Lock: + TracyCLockTerminate( lockCtx ); + TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); + EXPECT_TRUE( lockInfo.terminated ); +} + TEST_F( CcpTelemetryTest, RawTracyLockCAnnounceAndTerminate ) { TracyCLockCtx lockCtx; From 63328299225eb5e5ad7fdd8a3366a9e7ab1d6397 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 16:23:05 +0000 Subject: [PATCH 05/11] Remove unnecessary Raw Tracy function tests Following tests removed: - RawTracyLockCAnnounceAndTerminate - RawTracyUncontendedLock --- tests/CcpTelemetry.cpp | 62 ------------------------------------------ 1 file changed, 62 deletions(-) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index dca40de..72cfb8b 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -353,68 +353,6 @@ TEST_F( CcpTelemetryTest, RawTracyLockCounters ) EXPECT_TRUE( lockInfo.terminated ); } -TEST_F( CcpTelemetryTest, RawTracyLockCAnnounceAndTerminate ) -{ - TracyCLockCtx lockCtx; - const uint32_t announceLine = __LINE__ + 1; - TracyCLockAnnounce( lockCtx ); - - // Wait for a non-terminated match: when a test is repeated in one process, - // Tracy replays the previous run's (terminated) lock from the same call - // site, and its source location may resolve before that of the new lock. - TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); - ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); - EXPECT_GE( m_tracyClient.GetLockAnnounceCount(), 1 ); - EXPECT_FALSE( lockInfo.terminated ); - EXPECT_EQ( 0u, lockInfo.holderThread ); - EXPECT_TRUE( lockInfo.waitingThreads.empty() ); - EXPECT_EQ( 0, lockInfo.waitCount ); - EXPECT_EQ( 0, lockInfo.obtainCount ); - EXPECT_EQ( 0, lockInfo.releaseCount ); - // TracyCLockAnnounce announces with name == nullptr and function == __func__. - EXPECT_TRUE( lockInfo.name.empty() ); - EXPECT_EQ( "TestBody", lockInfo.function ); - - TracyCLockTerminate( lockCtx ); - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); - EXPECT_TRUE( lockInfo.terminated ); - EXPECT_GE( m_tracyClient.GetLockTerminateCount(), 1 ); -} - -TEST_F( CcpTelemetryTest, RawTracyUncontendedLock ) -{ - TracyCLockCtx lockCtx; - const uint32_t announceLine = __LINE__ + 1; - TracyCLockAnnounce( lockCtx ); - - TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); - ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); - - const auto notifyTracy = TracyCLockBeforeLock( lockCtx ); - EXPECT_TRUE( notifyTracy ); - TracyCLockAfterLock( lockCtx ); - - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.obtainCount == 1; } ); - EXPECT_EQ( 1, lockInfo.waitCount ); - EXPECT_EQ( 1, lockInfo.obtainCount ); - EXPECT_EQ( 0, lockInfo.releaseCount ); - EXPECT_NE( 0u, lockInfo.holderThread ); - EXPECT_TRUE( lockInfo.waitingThreads.empty() ) << "An obtained lock should no longer have its holder in the waiting list"; - - TracyCLockAfterUnlock( lockCtx ); - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.releaseCount == 1; } ); - EXPECT_EQ( 1, lockInfo.waitCount ); - EXPECT_EQ( 1, lockInfo.obtainCount ); - EXPECT_EQ( 1, lockInfo.releaseCount ); - EXPECT_EQ( 0u, lockInfo.holderThread ); - - TracyCLockTerminate( lockCtx ); - TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && lockInfo.terminated; } ); - EXPECT_TRUE( lockInfo.terminated ); -} - // A locks, B waits, A unlocks, B locks, B unlocks. TEST_F( CcpTelemetryTest, RawTracyContendedLockWithOneWaitingThread ) { From bbe6d6e168eef6a15da43d26afeea45de2df77a5 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:17:16 +0000 Subject: [PATCH 06/11] Rename multi thread Raw TracyLock tests --- tests/CcpTelemetry.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 72cfb8b..5540926 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -354,13 +354,15 @@ TEST_F( CcpTelemetryTest, RawTracyLockCounters ) } // A locks, B waits, A unlocks, B locks, B unlocks. -TEST_F( CcpTelemetryTest, RawTracyContendedLockWithOneWaitingThread ) +TEST_F( CcpTelemetryTest, RawTracyLockOneWaitingThread ) { TracyCLockCtx lockCtx; + TracyTestClient::LockInfo lockInfo; + std::string lockName = "CcpTelemetryTest-RawTracyLockOneWaitingThread"; + const uint32_t announceLine = __LINE__ + 1; TracyCLockAnnounce( lockCtx ); - - TracyTestClient::LockInfo lockInfo; + TracyCLockCustomName( lockCtx, lockName.c_str(), lockName.size() ); TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); @@ -430,13 +432,15 @@ TEST_F( CcpTelemetryTest, RawTracyContendedLockWithOneWaitingThread ) } // A locks, B waits, C waits, A unlocks, B and C lock/unlock in turn. -TEST_F( CcpTelemetryTest, RawTracyContendedLockWithMultipleWaitingThreads ) +TEST_F( CcpTelemetryTest, RawTracyLockMultipleWaitingThreads ) { TracyCLockCtx lockCtx; + TracyTestClient::LockInfo lockInfo; + std::string lockName = "CcpTelemetryTest-RawTracyLockMultipleWaitingThreads"; + const uint32_t announceLine = __LINE__ + 1; TracyCLockAnnounce( lockCtx ); - - TracyTestClient::LockInfo lockInfo; + TracyCLockCustomName( lockCtx, lockName.c_str(), lockName.size() ); TickTelemetry( [&] { return TryGetLockAtLine( announceLine, lockInfo ) && !lockInfo.terminated; } ); ASSERT_TRUE( TryGetLockAtLine( announceLine, lockInfo ) ); From 98870a02fb078fc33f4572e831a78a9c1f22db89 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:39:43 +0000 Subject: [PATCH 07/11] Rename and cleanup CcpMutex/CcpAutoMutex tests --- tests/CcpTelemetry.cpp | 61 ++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 5540926..e4ecc19 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -520,21 +520,24 @@ TEST_F( CcpTelemetryTest, RawTracyLockMultipleWaitingThreads ) // Release() and terminates in destructor. // The custom name is what currently identifies a CcpMutex lock; see TryGetActiveLockNamed. -TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruction ) +TEST_F( CcpTelemetryTest, CcpMutexAnnounceAndTerminate ) { TracyTestClient::LockInfo lockInfo; + + // Scope the CcpMutex so we can see what happens on destruction. { - CcpMutex mutex( "TelemetryTests", "TestMutex" ); + std::string lockName = "CcpTelemetryTest-CcpMutexAnnounceAndTerminate"; + CcpMutex mutex( "CcpTelemetryTest", "CcpMutexAnnounceAndTerminate" ); // The custom name arrives almost immediately, but the source location // resolves through extra server-query round trips; wait for both. - TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) && !lockInfo.source.empty(); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) ); + TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ) && !lockInfo.source.empty(); } ); + ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); EXPECT_FALSE( lockInfo.terminated ); // The owner and name passed to CcpMutex arrive combined as the custom lock name. - EXPECT_EQ( "TelemetryTests-TestMutex", lockInfo.name ); + EXPECT_EQ( lockName, lockInfo.name ); // The announce site is the TracyCLockAnnounce call in the CcpMutex constructor (function). - EXPECT_EQ( "CcpMutex", lockInfo.function ); + EXPECT_EQ( "CcpMutex", lockInfo.function ) << "Function name is the name of the CcpMutex constructor"; EXPECT_EQ( 0u, lockInfo.holderThread ); EXPECT_TRUE( lockInfo.waitingThreads.empty() ); EXPECT_EQ( 0, lockInfo.waitCount ); @@ -550,11 +553,12 @@ TEST_F( CcpTelemetryTest, CcpMutexAnnouncesOnConstructionAndTerminatesOnDestruct TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) { - CcpMutex mutex( "TelemetryTests", "TestMutex" ); + std::string lockName = "CcpTelemetryTest-CcpMutexAcquireAndRelease"; + CcpMutex mutex( "CcpTelemetryTest", "CcpMutexAcquireAndRelease" ); TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-TestMutex", lockInfo ) ); + TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); const uint32_t lockId = lockInfo.id; mutex.Acquire(); @@ -576,11 +580,12 @@ TEST_F( CcpTelemetryTest, CcpMutexAcquireAndRelease ) // A acquires the CcpMutex, B waits, A releases, B acquires and releases. TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) { - CcpMutex mutex( "TelemetryTests", "ContendedMutex" ); + std::string lockName = "CcpTelemetryTest-CcpMutexContentionAcrossThreads"; + CcpMutex mutex( "CcpTelemetryTest", "CcpMutexContentionAcrossThreads" ); TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-ContendedMutex", lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-ContendedMutex", lockInfo ) ); + TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); const uint32_t lockId = lockInfo.id; // Thread A acquires the mutex and holds it until we tell it to release. @@ -630,13 +635,14 @@ TEST_F( CcpTelemetryTest, CcpMutexContentionAcrossThreads ) EXPECT_TRUE( lockInfo.waitingThreads.empty() ); } -TEST_F( CcpTelemetryTest, CcpAutoMutexLocksForTheDurationOfItsScope ) +TEST_F( CcpTelemetryTest, CcpAutoMutexScopeLocking ) { - CcpMutex mutex( "TelemetryTests", "AutoMutex" ); + std::string lockName = "CcpTelemetryTest-CcpAutoMutexScopeLocking"; + CcpMutex mutex( "CcpTelemetryTest", "CcpAutoMutexScopeLocking" ); TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-AutoMutex", lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-AutoMutex", lockInfo ) ); + TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); const uint32_t lockId = lockInfo.id; { @@ -655,13 +661,14 @@ TEST_F( CcpTelemetryTest, CcpAutoMutexLocksForTheDurationOfItsScope ) EXPECT_EQ( 0u, lockInfo.holderThread ); } -TEST_F( CcpTelemetryTest, CcpAutoMutexEarlyReleaseReleasesOnlyOnce ) +TEST_F( CcpTelemetryTest, CcpAutoMutexEarlyRelease ) { - CcpMutex mutex( "TelemetryTests", "AutoMutexEarlyRelease" ); + std::string lockName = "CcpTelemetryTest-CcpAutoMutexEarlyRelease"; + CcpMutex mutex( "CcpTelemetryTest", "CcpAutoMutexEarlyRelease" ); TracyTestClient::LockInfo lockInfo; - TickTelemetry( [&] { return TryGetActiveLockNamed( "TelemetryTests-AutoMutexEarlyRelease", lockInfo ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-AutoMutexEarlyRelease", lockInfo ) ); + TickTelemetry( [&] { return TryGetActiveLockNamed( lockName, lockInfo ); } ); + ASSERT_TRUE( TryGetActiveLockNamed( lockName, lockInfo ) ); const uint32_t lockId = lockInfo.id; { @@ -690,18 +697,20 @@ TEST_F( CcpTelemetryTest, MultipleCcpMutexesAnnounceDistinctLocks ) { // The custom lock names make the two mutexes distinguishable even though // they share the same announce call site in CcpMutex.h. - CcpMutex firstMutex( "TelemetryTests", "FirstMutex" ); - CcpMutex secondMutex( "TelemetryTests", "SecondMutex" ); + CcpMutex firstMutex( "CcpTelemetryTest", "MultiTestFirstMutex" ); + CcpMutex secondMutex( "CcpTelemetryTest", "MultiTestSecondMutex" ); + std::string firstLockName = "CcpTelemetryTest-MultiTestFirstMutex"; + std::string secondLockName = "CcpTelemetryTest-MultiTestSecondMutex"; // resolved) announce source location, so wait for that to settle too. TracyTestClient::LockInfo firstLock; TracyTestClient::LockInfo secondLock; TickTelemetry( [&] { - return TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) && - TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ); + return TryGetActiveLockNamed( firstLockName, firstLock ) && + TryGetActiveLockNamed( secondLockName, secondLock ); } ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-FirstMutex", firstLock ) ); - ASSERT_TRUE( TryGetActiveLockNamed( "TelemetryTests-SecondMutex", secondLock ) ); + ASSERT_TRUE( TryGetActiveLockNamed( firstLockName, firstLock ) ); + ASSERT_TRUE( TryGetActiveLockNamed( secondLockName, secondLock ) ); EXPECT_NE( firstLock.id, secondLock.id ); // Each mutex drives its own lock: acquiring the second must not affect the first. From 84ed4bdd5fdd263f0ec3a3812b23148335f84e86 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Thu, 18 Jun 2026 13:03:20 +0000 Subject: [PATCH 08/11] Add support for Lazy Announce of mutexes in Tracy This is needed because the CcpMutex objects may have been created long before Tracy on-demand telemetry session started. --- include/CcpMutex.h | 91 ++++++++++++++++++++++++++++++++---------- tests/CcpTelemetry.cpp | 10 ++--- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/include/CcpMutex.h b/include/CcpMutex.h index 6039334..b5d83af 100644 --- a/include/CcpMutex.h +++ b/include/CcpMutex.h @@ -21,12 +21,9 @@ class CcpMutex m_name = name; #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() ) - { - const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); - TracyCLockAnnounce( m_tracyLockContext ); - TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); - } + // Lazily announce on first Acquire/Release; this also handles the case where + // the mutex is created before telemetry is connected. + EnsureTracyLockState(); #endif CcpRegisterMutex( *this, owner, name ); @@ -36,24 +33,29 @@ class CcpMutex { ::DeleteCriticalSection( &m_mutex ); #if CCP_TELEMETRY_ENABLED - if ( m_tracyLockContext ) { + // Only terminate if we still have a live context AND telemetry is still connected. + // If telemetry has been disconnected meanwhile, the context is already stale. + if ( m_tracyLockContext && CcpTelemetryIsConnected() ) + { TracyCLockTerminate( m_tracyLockContext ); } + m_tracyLockContext = nullptr; #endif } void Acquire() { #if CCP_TELEMETRY_ENABLED + EnsureTracyLockState(); bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( m_tracyLockContext ) { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif EnterCriticalSection( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( notifyTracy && CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( notifyTracy && m_tracyLockContext ) { TracyCLockAfterLock( m_tracyLockContext ); } @@ -64,7 +66,8 @@ class CcpMutex { LeaveCriticalSection( &m_mutex ); #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + EnsureTracyLockState(); + if ( m_tracyLockContext ) { TracyCLockAfterUnlock( m_tracyLockContext ); } @@ -86,6 +89,32 @@ class CcpMutex private: #if CCP_TELEMETRY_ENABLED + // Synchronizes m_tracyLockContext with the current telemetry connection state. + // - If telemetry is connected and we don't yet have a context, announce one. + // - If telemetry is disconnected but we still have a (now stale) context, drop it + // so that a future reconnect will produce a fresh, valid context. + // After this returns, all other Tracy calls in Acquire/Release can rely on a + // single, fast null-check of m_tracyLockContext. + void EnsureTracyLockState() + { + const bool connected = CcpTelemetryIsConnected(); + if ( m_tracyLockContext ) + { + if ( !connected ) + { + // Telemetry disconnected; drop the stale context quickly so the next + // connect produces a fresh announce/name. + m_tracyLockContext = nullptr; + } + } + else if ( connected ) + { + const std::string tracyLockName = std::string( m_owner ? m_owner : "" ) + "-" + ( m_name ? m_name : "" ); + TracyCLockAnnounce( m_tracyLockContext ); + TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); + } + } + TracyCLockCtx m_tracyLockContext{nullptr}; #endif CRITICAL_SECTION m_mutex; @@ -116,12 +145,9 @@ class CcpMutex m_name = name; #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() ) - { - const std::string tracyLockName = std::string( owner ? owner : "" ) + "-" + ( name ? name : "" ); - TracyCLockAnnounce( m_tracyLockContext ); - TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); - } + // Lazily announce on first Acquire/Release; this also handles the case where + // the mutex is created before telemetry is connected. + EnsureTracyLockState(); #endif CcpRegisterMutex( *this, owner, name ); @@ -131,24 +157,29 @@ class CcpMutex { pthread_mutex_destroy( &m_mutex ); #if CCP_TELEMETRY_ENABLED - if ( m_tracyLockContext ) { + // Only terminate if we still have a live context AND telemetry is still connected. + // If telemetry has been disconnected meanwhile, the context is already stale. + if ( m_tracyLockContext && CcpTelemetryIsConnected() ) + { TracyCLockTerminate( m_tracyLockContext ); } + m_tracyLockContext = nullptr; #endif } void Acquire() { #if CCP_TELEMETRY_ENABLED + EnsureTracyLockState(); bool notifyTracy{false}; - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( m_tracyLockContext ) { notifyTracy = TracyCLockBeforeLock( m_tracyLockContext ); } #endif pthread_mutex_lock( &m_mutex); #if CCP_TELEMETRY_ENABLED - if ( notifyTracy && CcpTelemetryIsConnected() && m_tracyLockContext ) + if ( notifyTracy && m_tracyLockContext ) { TracyCLockAfterLock( m_tracyLockContext ); } @@ -159,7 +190,8 @@ class CcpMutex { pthread_mutex_unlock( &m_mutex ); #if CCP_TELEMETRY_ENABLED - if ( CcpTelemetryIsConnected() && m_tracyLockContext ) + EnsureTracyLockState(); + if ( m_tracyLockContext ) { TracyCLockAfterUnlock( m_tracyLockContext ); } @@ -181,6 +213,25 @@ class CcpMutex private: #if CCP_TELEMETRY_ENABLED + // See the Windows variant above for documentation. + void EnsureTracyLockState() + { + const bool connected = CcpTelemetryIsConnected(); + if ( m_tracyLockContext ) + { + if ( !connected ) + { + m_tracyLockContext = nullptr; + } + } + else if ( connected ) + { + const std::string tracyLockName = std::string( m_owner ? m_owner : "" ) + "-" + ( m_name ? m_name : "" ); + TracyCLockAnnounce( m_tracyLockContext ); + TracyCLockCustomName( m_tracyLockContext, tracyLockName.c_str(), tracyLockName.size() ); + } + } + TracyCLockCtx m_tracyLockContext{nullptr}; #endif pthread_mutex_t m_mutex; diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index e4ecc19..21affb5 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -156,7 +156,7 @@ class CcpTelemetryTest : public ::testing::Test } // Helper for CcpMutex tests, finding active locks by the given custom name. - // CcpMutex names its lock "-" via TracyCLockCustomName in the constructor. + // CcpMutex names its lock "-" via EnsureTracyLockState helper function. // Name arrives async shortly after announce event, call function from a TickTelemetry predicate. // When a test is repeated within one process, Tracy replays earlier (terminated) locks having // the same name, where the most recently announced lock wins by id. @@ -514,8 +514,8 @@ TEST_F( CcpTelemetryTest, RawTracyLockMultipleWaitingThreads ) // --------------------------------------------------------------------------- // CcpMutex / CcpAutoMutex // --------------------------------------------------------------------------- -// CcpMutex announces a Tracy lock in its constructor (when the telemetry is -// connected at that point) and names it "-" via TracyCLockCustomName. +// CcpMutex announces a Tracy lock in the EnsureTracyLockState helper function +// and names it "-" via TracyCLockCustomName. // It reports wait/obtain around EnterCriticalSection in Acquire(), release in // Release() and terminates in destructor. // The custom name is what currently identifies a CcpMutex lock; see TryGetActiveLockNamed. @@ -536,8 +536,8 @@ TEST_F( CcpTelemetryTest, CcpMutexAnnounceAndTerminate ) EXPECT_FALSE( lockInfo.terminated ); // The owner and name passed to CcpMutex arrive combined as the custom lock name. EXPECT_EQ( lockName, lockInfo.name ); - // The announce site is the TracyCLockAnnounce call in the CcpMutex constructor (function). - EXPECT_EQ( "CcpMutex", lockInfo.function ) << "Function name is the name of the CcpMutex constructor"; + // The announce site is the EnsureTracyLockState helper function + EXPECT_EQ( "EnsureTracyLockState", lockInfo.function ); EXPECT_EQ( 0u, lockInfo.holderThread ); EXPECT_TRUE( lockInfo.waitingThreads.empty() ); EXPECT_EQ( 0, lockInfo.waitCount ); From a9e95255624d075b6cc13abfcf90c5fe4d6fd283 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:14:57 +0000 Subject: [PATCH 09/11] Add support for tracking locks in CcpTelemetryConfig --- CcpTelemetry.cpp | 10 ++++++++++ include/CcpMutex.h | 21 +++++++++++---------- include/CcpTelemetry.h | 2 ++ tests/CcpTelemetry.cpp | 6 +++++- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/CcpTelemetry.cpp b/CcpTelemetry.cpp index 86a0d01..6cfa1c8 100644 --- a/CcpTelemetry.cpp +++ b/CcpTelemetry.cpp @@ -95,6 +95,11 @@ bool CcpTelemetryMemoryTrackingIsEnabled() return s_config.trackMemoryAllocations; } +bool CcpTelemetryLockTrackingIsEnabled() +{ + return s_config.trackLocks; +} + void CcpRegisterMutex( class CcpMutex& m, const char* owner, const char* name ) { // Store the name for future Telemetry sessions, even if we're already connected. @@ -450,6 +455,11 @@ bool CcpTelemetryMemoryTrackingIsEnabled() return false; } +bool CcpTelemetryLockTrackingIsEnabled() +{ + return false; +} + void CcpRegisterThread( CcpThreadId_t threadId, const char* name ) { } diff --git a/include/CcpMutex.h b/include/CcpMutex.h index b5d83af..10083c8 100644 --- a/include/CcpMutex.h +++ b/include/CcpMutex.h @@ -33,9 +33,9 @@ class CcpMutex { ::DeleteCriticalSection( &m_mutex ); #if CCP_TELEMETRY_ENABLED - // Only terminate if we still have a live context AND telemetry is still connected. + // Only terminate if Lock tracking is turned on, we still have a live context AND telemetry is still connected. // If telemetry has been disconnected meanwhile, the context is already stale. - if ( m_tracyLockContext && CcpTelemetryIsConnected() ) + if ( CcpTelemetryLockTrackingIsEnabled() && m_tracyLockContext && CcpTelemetryIsConnected() ) { TracyCLockTerminate( m_tracyLockContext ); } @@ -90,20 +90,21 @@ class CcpMutex private: #if CCP_TELEMETRY_ENABLED // Synchronizes m_tracyLockContext with the current telemetry connection state. - // - If telemetry is connected and we don't yet have a context, announce one. - // - If telemetry is disconnected but we still have a (now stale) context, drop it + // - If Lock tracking is disabled, behave as if telemetry were disconnected. + // - If telemetry is connected, and we don't yet have a context, announce one. + // - If telemetry is disconnected, but we still have a (now stale) context, drop it // so that a future reconnect will produce a fresh, valid context. // After this returns, all other Tracy calls in Acquire/Release can rely on a // single, fast null-check of m_tracyLockContext. void EnsureTracyLockState() { - const bool connected = CcpTelemetryIsConnected(); + const bool connected = CcpTelemetryLockTrackingIsEnabled() && CcpTelemetryIsConnected(); if ( m_tracyLockContext ) { if ( !connected ) { - // Telemetry disconnected; drop the stale context quickly so the next - // connect produces a fresh announce/name. + // Telemetry disconnected (or lock tracking disabled); drop the stale + // context quickly so that the next connect produces a fresh announce/name. m_tracyLockContext = nullptr; } } @@ -157,9 +158,9 @@ class CcpMutex { pthread_mutex_destroy( &m_mutex ); #if CCP_TELEMETRY_ENABLED - // Only terminate if we still have a live context AND telemetry is still connected. + // Only terminate if Lock tracking is turned on, we still have a live context AND telemetry is still connected. // If telemetry has been disconnected meanwhile, the context is already stale. - if ( m_tracyLockContext && CcpTelemetryIsConnected() ) + if ( CcpTelemetryLockTrackingIsEnabled() && m_tracyLockContext && CcpTelemetryIsConnected() ) { TracyCLockTerminate( m_tracyLockContext ); } @@ -216,7 +217,7 @@ class CcpMutex // See the Windows variant above for documentation. void EnsureTracyLockState() { - const bool connected = CcpTelemetryIsConnected(); + const bool connected = CcpTelemetryLockTrackingIsEnabled() && CcpTelemetryIsConnected(); if ( m_tracyLockContext ) { if ( !connected ) diff --git a/include/CcpTelemetry.h b/include/CcpTelemetry.h index 670bfcf..c3b3150 100644 --- a/include/CcpTelemetry.h +++ b/include/CcpTelemetry.h @@ -45,6 +45,7 @@ struct CcpTelemetryConfig std::string applicationName; std::chrono::milliseconds captureDuration{}; bool trackMemoryAllocations{false}; + bool trackLocks{false}; }; [[deprecated( "Use `CcpStartTelemetry( const CcpTelemetryConfig& config ) instead" )]] CARBON_CORE_API bool CcpStartTelemetry( const char* server, int connectionType, uint32_t maxThreadCount ); @@ -69,6 +70,7 @@ CARBON_CORE_API bool CcpTelemetryIsConnected(); CARBON_CORE_API bool CcpTelemetryIsStarted(); CARBON_CORE_API bool CcpTelemetryIsStopped(); CARBON_CORE_API bool CcpTelemetryMemoryTrackingIsEnabled(); +CARBON_CORE_API bool CcpTelemetryLockTrackingIsEnabled(); CARBON_CORE_API void CcpTelemetrySetActiveFiber( const std::string& name ); CARBON_CORE_API const std::string& CcpTelemetryGetActiveFiber(); diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 21affb5..921c374 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -67,11 +67,15 @@ class CcpTelemetryTest : public ::testing::Test void StartTelemetry( std::string appName = "Telemetry Tests", std::chrono::milliseconds duration = std::chrono::milliseconds::zero(), - bool trackMemory = false ) + bool trackMemory = true, + // Default to true so CcpMutex tests get lock announcements; CcpMutex now + // gates all TracyCLockXxx calls on CcpTelemetryLockTrackingIsEnabled(). + bool trackLocks = true ) { CcpTelemetryConfig conf{ appName }; conf.captureDuration = duration; conf.trackMemoryAllocations = trackMemory; + conf.trackLocks = trackLocks; CcpStartTelemetry( conf ); // It may appear weird that this checks `TracyIsStarted`, but the reason is that the internal state machine // in CcpTelemetry only advances to `CcpTelemetryIsStarted` once it _also_ has established a connection to From 984579bf37911820def580cd175875dc15334ec8 Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:13:46 +0000 Subject: [PATCH 10/11] Remove unnecessary comment in tests/CcpTelemetry.cpp --- tests/CcpTelemetry.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/CcpTelemetry.cpp b/tests/CcpTelemetry.cpp index 921c374..b105e57 100644 --- a/tests/CcpTelemetry.cpp +++ b/tests/CcpTelemetry.cpp @@ -68,8 +68,6 @@ class CcpTelemetryTest : public ::testing::Test void StartTelemetry( std::string appName = "Telemetry Tests", std::chrono::milliseconds duration = std::chrono::milliseconds::zero(), bool trackMemory = true, - // Default to true so CcpMutex tests get lock announcements; CcpMutex now - // gates all TracyCLockXxx calls on CcpTelemetryLockTrackingIsEnabled(). bool trackLocks = true ) { CcpTelemetryConfig conf{ appName }; From 2bf0028c74e1fbce7461d6fcb68f6c0791a86fed Mon Sep 17 00:00:00 2001 From: CCP ChargeBack <35330827+ccp-chargeback@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:09:33 +0000 Subject: [PATCH 11/11] Fix errors after merging in changes from main Make use of types from the tracy header files: - tracy::ServerQueryString - tracy::ServerQuerySourceLocation - tracy::QueueType::StringData - tracy::QueueType::SingleStringData - tracy::QueueType::LockAnnounce - tracy::QueueType::LockTerminate - tracy::QueueType::LockWait - tracy::QueueType::LockObtain - tracy::QueueType::LockRelease - tracy::QueueType::LockName - tracy::QueueType::SourceLocation --- tests/TracyTestClient.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/TracyTestClient.cpp b/tests/TracyTestClient.cpp index 1d5606c..a2941b6 100644 --- a/tests/TracyTestClient.cpp +++ b/tests/TracyTestClient.cpp @@ -374,7 +374,7 @@ void TracyTestClient::RequestLockString( uint64_t ptr, uint32_t lockId, int fiel const bool alreadyQueried = !pending.empty(); pending.push_back( { lockId, field } ); if( !alreadyQueried ) - SendQueryLocked( kServerQueryString, ptr ); + SendQueryLocked( tracy::ServerQueryString, ptr ); } // Parse the decompressed byte stream and update internal state. @@ -451,7 +451,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) std::lock_guard lock( m_dataMutex ); m_fiberNames[strPtr] = std::move( name ); } - else if( idx == kQueueStringData ) + else if( idx == QueueIdx( tracy::QueueType::StringData ) ) { // Reply to a ServerQueryString we sent while resolving a // lock source location; strPtr echoes the queried pointer. @@ -492,7 +492,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) if( ptr + strSz > end ) return; // Remember the payload: fat-pointer items (e.g. LockName) are // preceded by a SingleStringData event carrying their string. - if( idx == kQueueSingleStringData ) + if( idx == QueueIdx( tracy::QueueType::SingleStringData ) ) m_pendingSingleString.assign( ptr, strSz ); ptr += strSz; } @@ -538,7 +538,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } - case kQueueLockAnnounce: + case tracy::QueueType::LockAnnounce: { m_lockAnnounceCount.fetch_add( 1, std::memory_order_relaxed ); const uint32_t lockId = item->lockAnnounce.id; @@ -548,11 +548,11 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) // Resolve the announce call site. The reply carries no request // pointer, so remember which lock the next reply belongs to. m_pendingLockSrcLocs.push_back( lockId ); - SendQueryLocked( kServerQuerySourceLocation, srcloc ); + SendQueryLocked( tracy::ServerQuerySourceLocation, srcloc ); break; } - case kQueueLockTerminate: + case tracy::QueueType::LockTerminate: { m_lockTerminateCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); @@ -560,7 +560,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } - case kQueueLockWait: + case tracy::QueueType::LockWait: { m_lockWaitCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); @@ -570,7 +570,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } - case kQueueLockObtain: + case tracy::QueueType::LockObtain: { m_lockObtainCount.fetch_add( 1, std::memory_order_relaxed ); const uint32_t thread = item->lockObtain.thread; @@ -585,7 +585,7 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } - case kQueueLockRelease: + case tracy::QueueType::LockRelease: { m_lockReleaseCount.fetch_add( 1, std::memory_order_relaxed ); std::lock_guard lock( m_dataMutex ); @@ -595,14 +595,14 @@ void TracyTestClient::ProcessDecompressedData( const char* data, int sz ) break; } - case kQueueLockName: + case tracy::QueueType::LockName: { std::lock_guard lock( m_dataMutex ); GetOrCreateLockById( item->lockName.id ).name = m_pendingSingleString; break; } - case kQueueSourceLocation: + case tracy::QueueType::SourceLocation: { std::lock_guard lock( m_dataMutex ); if( !m_pendingLockSrcLocs.empty() )