mirror of
https://github.com/ValveSoftware/GameNetworkingSockets.git
synced 2026-05-29 16:20:34 +00:00
Break out a few misc things from steamnetworkingsockets_socketthread.cpp
Lock debugging Timer Memory override
This commit is contained in:
@@ -51,6 +51,7 @@ set(GNS_CLIENTLIB_SRCS
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_flat.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_connections.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_socketthread.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_lowlevel_misc.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_spew.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_taskqueue.cpp"
|
||||
"steamnetworkingsockets/clientlib/steamnetworkingsockets_ice_client.cpp"
|
||||
|
||||
@@ -321,16 +321,29 @@ private:
|
||||
|
||||
extern int g_cbUDPSocketBufferSize;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Misc low level service thread stuff
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Called when we know it's safe to actually destroy sockets pending deletion.
|
||||
/// This is when: 1.) We own the lock and 2.) we aren't polling in the service thread.
|
||||
extern void ProcessPendingDestroyClosedRawUDPSockets();
|
||||
|
||||
/// Return true if the service thread is running
|
||||
extern bool IsServiceThreadRunning();
|
||||
|
||||
/// Wake up the service thread ASAP. Intended to be called from other threads,
|
||||
/// but is safe to call from the service thread as well.
|
||||
extern void WakeServiceThread();
|
||||
|
||||
/// Return true if it looks like the address is a local address
|
||||
extern bool IsRouteToAddressProbablyLocal( netadr_t addr );
|
||||
|
||||
extern bool ResolveHostname( const char* pszHostname, CUtlVector< SteamNetworkingIPAddr > *pAddrs );
|
||||
extern bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs );
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Spew
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Last time that we spewed something that was subject to rate limit
|
||||
extern SteamNetworkingMicroseconds g_usecLastRateLimitSpew;
|
||||
extern int g_nRateLimitSpewCount;
|
||||
@@ -628,10 +641,12 @@ using ShortDurationScopeLock = ScopeLock<ShortDurationLock>;
|
||||
#define AssertHeldByCurrentThread( ... ) _AssertHeldByCurrentThread( __FILE__, __LINE__ ,## __VA_ARGS__ )
|
||||
#define AssertLocksHeldByCurrentThread( ... ) _AssertLocksHeldByCurrentThread( __FILE__, __LINE__,## __VA_ARGS__ )
|
||||
#define TakeLockOwnership( pLock, ... ) _TakeLockOwnership( (pLock), __FILE__, __LINE__,## __VA_ARGS__ )
|
||||
extern void AssertGlobalLockHeldExactlyOnce();
|
||||
#else
|
||||
#define AssertHeldByCurrentThread( ... ) _AssertHeldByCurrentThread( nullptr, 0,## __VA_ARGS__ )
|
||||
#define AssertLocksHeldByCurrentThread( ... ) _AssertLocksHeldByCurrentThread( nullptr, 0,## __VA_ARGS__ )
|
||||
#define TakeLockOwnership( pLock, ... ) _TakeLockOwnership( (pLock), nullptr, 0,## __VA_ARGS__ )
|
||||
inline void AssertGlobalLockHeldExactlyOnce() {}
|
||||
#endif
|
||||
|
||||
/// Special utilities for acquiring the global lock
|
||||
@@ -654,16 +669,13 @@ struct SteamNetworkingGlobalLock
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef DBGFLAG_VALIDATE
|
||||
extern void SteamNetworkingSocketsLowLevelValidate( CValidator &validator );
|
||||
#endif
|
||||
extern SteamNetworkingMicroseconds s_usecServiceThreadLockWaitWarning;
|
||||
|
||||
/// Return true if the service thread is running
|
||||
extern bool IsServiceThreadRunning();
|
||||
|
||||
/// Wake up the service thread ASAP. Intended to be called from other threads,
|
||||
/// but is safe to call from the service thread as well.
|
||||
extern void WakeServiceThread();
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Task queue
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class CQueuedTask;
|
||||
|
||||
@@ -826,20 +838,21 @@ extern CTaskList g_taskListRunInBackground;
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Fetch current time
|
||||
extern SteamNetworkingMicroseconds SteamNetworkingSockets_GetLocalTimestamp();
|
||||
#ifdef DBGFLAG_VALIDATE
|
||||
extern void SteamNetworkingSocketsLowLevelValidate( CValidator &validator );
|
||||
#endif
|
||||
|
||||
extern void SeedWeakRandomGenerator();
|
||||
extern void ETW_LongOp( const char *opName, SteamNetworkingMicroseconds usec, const char *pszInfo );
|
||||
|
||||
/// Set debug output hook
|
||||
extern void SteamNetworkingSockets_SetDebugOutputFunction( ESteamNetworkingSocketsDebugOutputType eDetailLevel, FSteamNetworkingSocketsDebugOutput pfnFunc );
|
||||
|
||||
/// Return true if it looks like the address is a local address
|
||||
extern bool IsRouteToAddressProbablyLocal( netadr_t addr );
|
||||
|
||||
extern bool ResolveHostname( const char* pszHostname, CUtlVector< SteamNetworkingIPAddr > *pAddrs );
|
||||
extern bool GetLocalAddresses( CUtlVector< SteamNetworkingIPAddr >* pAddrs );
|
||||
|
||||
} // namespace SteamNetworkingSocketsLib
|
||||
|
||||
/// Fetch current time
|
||||
extern SteamNetworkingMicroseconds SteamNetworkingSockets_GetLocalTimestamp();
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_DefaultPreFormatDebugOutputHandler( ESteamNetworkingSocketsDebugOutputType eType, bool bFmt, const char* pstrFile, int nLine, const char *pMsg, va_list ap );
|
||||
|
||||
#endif // STEAMNETWORKINGSOCKETS_LOWLEVEL_H
|
||||
|
||||
@@ -0,0 +1,604 @@
|
||||
//====== Copyright Valve Corporation, All rights reserved. ====================
|
||||
//
|
||||
// Miscellaneous "low level" functionality for SteamNetworkingSockets.
|
||||
//
|
||||
// - Global lock (mutex) and lock debugging
|
||||
// - Local timestamp clock
|
||||
// - Memory allocator override
|
||||
//
|
||||
#include "steamnetworkingsockets_lowlevel.h"
|
||||
#include "crypto.h"
|
||||
#include <vstdlib/random.h>
|
||||
#include <tier0/valve_tracelogging.h>
|
||||
|
||||
#if IsPosix()
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
#include <tier0/memdbgon.h>
|
||||
|
||||
TRACELOGGING_DECLARE_PROVIDER( HTraceLogging_SteamNetworkingSockets );
|
||||
|
||||
using namespace SteamNetworkingSocketsLib;
|
||||
namespace SteamNetworkingSocketsLib {
|
||||
|
||||
void ETW_LongOp( const char *opName, SteamNetworkingMicroseconds usec, const char *pszInfo )
|
||||
{
|
||||
if ( !pszInfo )
|
||||
pszInfo = "";
|
||||
TraceLoggingWrite(
|
||||
HTraceLogging_SteamNetworkingSockets,
|
||||
"LongOp",
|
||||
TraceLoggingLevel( WINEVENT_LEVEL_WARNING ),
|
||||
TraceLoggingUInt64( usec, "Microseconds" ),
|
||||
TraceLoggingString( pszInfo, "ExtraInfo" )
|
||||
);
|
||||
}
|
||||
|
||||
void SeedWeakRandomGenerator()
|
||||
{
|
||||
|
||||
// Seed cheesy random number generator using true source of entropy
|
||||
int temp;
|
||||
CCrypto::GenerateRandomBlock( &temp, sizeof(temp) );
|
||||
WeakRandomSeed( temp );
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Lock debugging / hygiene
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// Global lock for all local data structures
|
||||
static Lock<RecursiveTimedMutexImpl> s_mutexGlobalLock( "global", 0, LockDebugInfo::k_nOrder_Global );
|
||||
|
||||
// Threshold for the service-thread-specific *assert*
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
SteamNetworkingMicroseconds s_usecServiceThreadLockWaitWarning = 300*1000;
|
||||
#else
|
||||
SteamNetworkingMicroseconds s_usecServiceThreadLockWaitWarning = 50*1000;
|
||||
#endif
|
||||
|
||||
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
|
||||
// By default, complain if we hold the lock for more than this long.
|
||||
// TSan adds 10-20x overhead so the threshold is scaled up accordingly.
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
constexpr SteamNetworkingMicroseconds k_usecDefaultLongLockHeldWarningThreshold = 5*1000*20;
|
||||
#else
|
||||
constexpr SteamNetworkingMicroseconds k_usecDefaultLongLockHeldWarningThreshold = 5*1000;
|
||||
#endif
|
||||
|
||||
// Threshold for warning about waiting on any lock.
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
static SteamNetworkingMicroseconds s_usecLockWaitWarningThreshold = 300*1000;
|
||||
#else
|
||||
static SteamNetworkingMicroseconds s_usecLockWaitWarningThreshold = 2*1000;
|
||||
#endif
|
||||
|
||||
// Debug the locks active on the cu
|
||||
struct ThreadLockDebugInfo
|
||||
{
|
||||
static constexpr int k_nMaxHeldLocks = 8;
|
||||
static constexpr int k_nMaxTags = 32;
|
||||
|
||||
int m_nHeldLocks = 0;
|
||||
int m_nTags = 0;
|
||||
|
||||
SteamNetworkingMicroseconds m_usecLongLockWarningThreshold;
|
||||
SteamNetworkingMicroseconds m_usecIgnoreLongLockWaitTimeUntil;
|
||||
SteamNetworkingMicroseconds m_usecOuterLockStartTime; // Time when we started waiting on outermost lock (if we don't have it yet), or when we aquired the lock (if we have it)
|
||||
|
||||
const LockDebugInfo *m_arHeldLocks[ k_nMaxHeldLocks ];
|
||||
struct Tag_t
|
||||
{
|
||||
const char *m_pszTag;
|
||||
int m_nCount;
|
||||
};
|
||||
Tag_t m_arTags[ k_nMaxTags ];
|
||||
|
||||
inline void AddTag( const char *pszTag );
|
||||
};
|
||||
|
||||
static void (*s_fLockAcquiredCallback)( const char *tags, SteamNetworkingMicroseconds usecWaited );
|
||||
static void (*s_fLockHeldCallback)( const char *tags, SteamNetworkingMicroseconds usecWaited );
|
||||
|
||||
/// Get the per-thread debug info
|
||||
static ThreadLockDebugInfo &GetThreadDebugInfo()
|
||||
{
|
||||
// Apple seems to hate thread_local. Is there some sort of feature
|
||||
// define a can check here? It's a shame because it's really very
|
||||
// efficient on MSVC, gcc, and clang on Windows and linux.
|
||||
//
|
||||
// Apple seems to support thread_local starting with Xcode 8.0
|
||||
#if defined(__APPLE__) && __clang_major__ < 8
|
||||
|
||||
static pthread_key_t key;
|
||||
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
// One time init the TLS key
|
||||
pthread_once( &key_once,
|
||||
[](){ // Initialization code to run once
|
||||
pthread_key_create(
|
||||
&key,
|
||||
[](void *ptr) { free(ptr); } // Destructor function
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Get current object
|
||||
void *result = pthread_getspecific(key);
|
||||
if ( unlikely( result == nullptr ) )
|
||||
{
|
||||
result = malloc( sizeof(ThreadLockDebugInfo) );
|
||||
memset( result, 0, sizeof(ThreadLockDebugInfo) );
|
||||
pthread_setspecific(key, result);
|
||||
}
|
||||
return *static_cast<ThreadLockDebugInfo *>( result );
|
||||
#else
|
||||
|
||||
// Use thread_local
|
||||
thread_local ThreadLockDebugInfo tls_lockDebugInfo;
|
||||
return tls_lockDebugInfo;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// If non-NULL, add a "tag" to the lock journal for the current thread.
|
||||
/// This is useful so that if we hold a lock for a long time, we can get
|
||||
/// an idea what sorts of operations were taking a long time.
|
||||
inline void ThreadLockDebugInfo::AddTag( const char *pszTag )
|
||||
{
|
||||
if ( !pszTag )
|
||||
return;
|
||||
|
||||
Assert( m_nHeldLocks > 0 ); // Can't add a tag unless we are locked!
|
||||
|
||||
for ( int i = 0 ; i < m_nTags ; ++i )
|
||||
{
|
||||
if ( m_arTags[i].m_pszTag == pszTag )
|
||||
{
|
||||
++m_arTags[i].m_nCount;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( m_nTags >= ThreadLockDebugInfo::k_nMaxTags )
|
||||
return;
|
||||
|
||||
m_arTags[ m_nTags ].m_pszTag = pszTag;
|
||||
m_arTags[ m_nTags ].m_nCount = 1;
|
||||
++m_nTags;
|
||||
}
|
||||
|
||||
LockDebugInfo::~LockDebugInfo()
|
||||
{
|
||||
// We should not be locked! If we are, remove us
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
AssertMsg( false, "Lock '%s' being destroyed while it is held!", m_pszName );
|
||||
AboutToUnlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LockDebugInfo::AboutToLock( bool bTry )
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
|
||||
// First lock held by this thread?
|
||||
if ( t.m_nHeldLocks == 0 )
|
||||
{
|
||||
// Remember when we started trying to lock
|
||||
t.m_usecOuterLockStartTime = SteamNetworkingSockets_GetLocalTimestamp();
|
||||
return;
|
||||
}
|
||||
|
||||
// We already hold a lock. Check for taking locks in such a way
|
||||
// that might lead to deadlocks.
|
||||
const LockDebugInfo *pTopLock = t.m_arHeldLocks[ t.m_nHeldLocks-1 ];
|
||||
|
||||
// Taking locks in increasing order is always allowed
|
||||
if ( likely( pTopLock->m_nOrder < m_nOrder ) )
|
||||
return;
|
||||
|
||||
// Global lock *must* always be the outermost lock. (It is legal to take other locks in
|
||||
// between and then lock the global lock recursively.)
|
||||
const bool bHoldGlobalLock = t.m_arHeldLocks[ 0 ] == &s_mutexGlobalLock;
|
||||
AssertMsg(
|
||||
bHoldGlobalLock || this != &s_mutexGlobalLock,
|
||||
"Taking global lock while already holding lock '%s'", t.m_arHeldLocks[ 0 ]->m_pszName
|
||||
);
|
||||
|
||||
// If they are only "trying", we allow out-of-order behaviour.
|
||||
if ( bTry )
|
||||
return;
|
||||
|
||||
// It's always OK to lock recursively.
|
||||
//
|
||||
// (Except for "short duration" locks, which are allowed to
|
||||
// use a mutex implementation that does not support this.)
|
||||
if ( !( m_nFlags & k_nFlag_ShortDuration ) )
|
||||
{
|
||||
for ( int i = 0 ; i < t.m_nHeldLocks ; ++i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Taking multiple object locks? This is allowed under certain circumstances
|
||||
if ( likely( pTopLock->m_nOrder == m_nOrder && m_nOrder == k_nOrder_ObjectOrTable ) )
|
||||
{
|
||||
|
||||
// If we hold the global lock, it's OK
|
||||
if ( bHoldGlobalLock )
|
||||
return;
|
||||
|
||||
// If the global lock isn't held, then no more than one
|
||||
// object lock is allowed, since two different threads
|
||||
// might take them in different order.
|
||||
constexpr int k_nObjectFlags = LockDebugInfo::k_nFlag_Connection | LockDebugInfo::k_nFlag_PollGroup;
|
||||
if (
|
||||
( ( m_nFlags & k_nObjectFlags ) != 0 )
|
||||
//|| ( m_nFlags & k_nFlag_Table ) // We actually do this in one place when we know it's OK. Not worth it right now to get this situation exempted from the checking.
|
||||
) {
|
||||
// We must not already hold any existing object locks (except perhaps this one)
|
||||
for ( int i = 0 ; i < t.m_nHeldLocks ; ++i )
|
||||
{
|
||||
const LockDebugInfo *pOtherLock = t.m_arHeldLocks[ i ];
|
||||
AssertMsg( pOtherLock == this || !( pOtherLock->m_nFlags & k_nObjectFlags ),
|
||||
"Taking lock '%s' and then '%s', while not holding the global lock", pOtherLock->m_pszName, m_pszName );
|
||||
}
|
||||
}
|
||||
|
||||
// Usage is OK if we didn't find any problems above
|
||||
return;
|
||||
}
|
||||
|
||||
AssertMsg( false, "Taking lock '%s' while already holding lock '%s'", m_pszName, pTopLock->m_pszName );
|
||||
}
|
||||
|
||||
void LockDebugInfo::OnLocked( const char *pszTag )
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
|
||||
Assert( t.m_nHeldLocks < ThreadLockDebugInfo::k_nMaxHeldLocks );
|
||||
t.m_arHeldLocks[ t.m_nHeldLocks++ ] = this;
|
||||
|
||||
if ( t.m_nHeldLocks == 1 )
|
||||
{
|
||||
SteamNetworkingMicroseconds usecNow = SteamNetworkingSockets_GetLocalTimestamp();
|
||||
SteamNetworkingMicroseconds usecTimeSpentWaitingOnLock = usecNow - t.m_usecOuterLockStartTime;
|
||||
t.m_usecLongLockWarningThreshold = k_usecDefaultLongLockHeldWarningThreshold;
|
||||
t.m_nTags = 0;
|
||||
|
||||
if ( usecTimeSpentWaitingOnLock > s_usecLockWaitWarningThreshold && usecNow > t.m_usecIgnoreLongLockWaitTimeUntil )
|
||||
{
|
||||
if ( pszTag )
|
||||
SpewWarning( "Waited %.1fms for SteamNetworkingSockets lock [%s]", usecTimeSpentWaitingOnLock*1e-3, pszTag );
|
||||
else
|
||||
SpewWarning( "Waited %.1fms for SteamNetworkingSockets lock", usecTimeSpentWaitingOnLock*1e-3 );
|
||||
ETW_LongOp( "lock wait", usecTimeSpentWaitingOnLock, pszTag );
|
||||
}
|
||||
|
||||
auto callback = s_fLockAcquiredCallback; // save to temp, to prevent very narrow race condition where variable is cleared after we null check it, and we call null
|
||||
if ( callback )
|
||||
callback( pszTag, usecTimeSpentWaitingOnLock );
|
||||
|
||||
t.m_usecOuterLockStartTime = usecNow;
|
||||
}
|
||||
|
||||
t.AddTag( pszTag );
|
||||
}
|
||||
|
||||
void LockDebugInfo::AboutToUnlock()
|
||||
{
|
||||
char tags[ 256 ];
|
||||
|
||||
SteamNetworkingMicroseconds usecElapsed = 0;
|
||||
SteamNetworkingMicroseconds usecElapsedTooLong = 0;
|
||||
auto lockHeldCallback = s_fLockHeldCallback;
|
||||
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
Assert( t.m_nHeldLocks > 0 );
|
||||
|
||||
// Unlocking the last lock?
|
||||
if ( t.m_nHeldLocks == 1 )
|
||||
{
|
||||
|
||||
// We're about to do the final release. How long did we hold the lock?
|
||||
usecElapsed = SteamNetworkingSockets_GetLocalTimestamp() - t.m_usecOuterLockStartTime;
|
||||
|
||||
// Too long? We need to check the threshold here because the threshold could
|
||||
// change by another thread immediately after we release the lock. Also, if
|
||||
// we're debugging, all bets are off. They could have hit a breakpoint, and
|
||||
// we don't want to create a bunch of confusing spew with spurious asserts
|
||||
if ( usecElapsed >= t.m_usecLongLockWarningThreshold && !Plat_IsInDebugSession() )
|
||||
{
|
||||
usecElapsedTooLong = usecElapsed;
|
||||
}
|
||||
|
||||
if ( usecElapsedTooLong > 0 || lockHeldCallback )
|
||||
{
|
||||
char *p = tags;
|
||||
char *end = tags + sizeof(tags) - 1;
|
||||
for ( int i = 0 ; i < t.m_nTags && p+5 < end ; ++i )
|
||||
{
|
||||
if ( p > tags )
|
||||
*(p++) = ',';
|
||||
|
||||
const ThreadLockDebugInfo::Tag_t &tag = t.m_arTags[i];
|
||||
int taglen = std::min( int(end-p), (int)V_strlen( tag.m_pszTag ) );
|
||||
memcpy( p, tag.m_pszTag, taglen );
|
||||
p += taglen;
|
||||
|
||||
if ( tag.m_nCount > 1 )
|
||||
{
|
||||
int l = end-p;
|
||||
if ( l <= 5 )
|
||||
break;
|
||||
p += V_snprintf( p, l, "(x%d)", tag.m_nCount );
|
||||
}
|
||||
}
|
||||
*p = '\0';
|
||||
}
|
||||
|
||||
t.m_nTags = 0;
|
||||
t.m_usecOuterLockStartTime = 0; // Just for grins.
|
||||
}
|
||||
|
||||
if ( usecElapsed > 0 && lockHeldCallback )
|
||||
{
|
||||
lockHeldCallback(tags, usecElapsed);
|
||||
}
|
||||
|
||||
// Yelp if we held the lock for longer than the threshold.
|
||||
if ( usecElapsedTooLong != 0 )
|
||||
{
|
||||
SpewWarning(
|
||||
"SteamNetworkingSockets lock held for %.1fms. (Performance warning.) %s\n"
|
||||
"This is usually a symptom of a general performance problem such as thread starvation.",
|
||||
usecElapsedTooLong*1e-3, tags
|
||||
);
|
||||
ETW_LongOp( "lock held", usecElapsedTooLong, tags );
|
||||
}
|
||||
|
||||
// NOTE: We are allowed to unlock out of order! We specifically
|
||||
// do this with the table lock!
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
--t.m_nHeldLocks;
|
||||
if ( i < t.m_nHeldLocks ) // Don't do the memmove in the common case of stack pop
|
||||
memmove( &t.m_arHeldLocks[i], &t.m_arHeldLocks[i+1], (t.m_nHeldLocks-i) * sizeof(t.m_arHeldLocks[0]) );
|
||||
t.m_arHeldLocks[t.m_nHeldLocks] = nullptr; // Just for grins
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AssertMsg( false, "Unlocked a lock '%s' that wasn't held?", m_pszName );
|
||||
}
|
||||
|
||||
void LockDebugInfo::_AssertHeldByCurrentThread( const char *pszFile, int line, const char *pszTag ) const
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
t.AddTag( pszTag );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AssertMsg( false, "%s(%d): Lock '%s' not held", pszFile, line, m_pszName );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::SetLongLockWarningThresholdMS( const char *pszTag, int msWarningThreshold )
|
||||
{
|
||||
AssertHeldByCurrentThread( pszTag );
|
||||
SteamNetworkingMicroseconds usecWarningThreshold = SteamNetworkingMicroseconds{msWarningThreshold}*1000;
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
if ( t.m_usecLongLockWarningThreshold < usecWarningThreshold )
|
||||
{
|
||||
t.m_usecLongLockWarningThreshold = usecWarningThreshold;
|
||||
t.m_usecIgnoreLongLockWaitTimeUntil = SteamNetworkingSockets_GetLocalTimestamp() + t.m_usecLongLockWarningThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::_AssertHeldByCurrentThread( const char *pszFile, int line )
|
||||
{
|
||||
s_mutexGlobalLock._AssertHeldByCurrentThread( pszFile, line, nullptr );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::_AssertHeldByCurrentThread( const char *pszFile, int line, const char *pszTag )
|
||||
{
|
||||
s_mutexGlobalLock._AssertHeldByCurrentThread( pszFile, line, pszTag );
|
||||
}
|
||||
|
||||
void AssertGlobalLockHeldExactlyOnce()
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
Assert( t.m_nHeldLocks == 1 && t.m_arHeldLocks[0] == &s_mutexGlobalLock );
|
||||
}
|
||||
|
||||
#endif // #if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
|
||||
void SteamNetworkingGlobalLock::Lock( const char *pszTag )
|
||||
{
|
||||
s_mutexGlobalLock.lock( pszTag );
|
||||
}
|
||||
|
||||
bool SteamNetworkingGlobalLock::TryLock( const char *pszTag, int msTimeout )
|
||||
{
|
||||
return s_mutexGlobalLock.try_lock_for( msTimeout, pszTag );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::Unlock()
|
||||
{
|
||||
s_mutexGlobalLock.unlock();
|
||||
}
|
||||
|
||||
} // namespace SteamNetworkingSocketsLib
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockWaitWarningThreshold( SteamNetworkingMicroseconds usecTheshold )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_usecLockWaitWarningThreshold = usecTheshold;
|
||||
#endif
|
||||
s_usecServiceThreadLockWaitWarning = usecTheshold;
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockAcquiredCallback( void (*callback)( const char *tags, SteamNetworkingMicroseconds usecWaited ) )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_fLockAcquiredCallback = callback;
|
||||
#else
|
||||
// Should we assert here?
|
||||
#endif
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockHeldCallback( void (*callback)( const char *tags, SteamNetworkingMicroseconds usecWaited ) )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_fLockHeldCallback = callback;
|
||||
#else
|
||||
// Should we assert here?
|
||||
#endif
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Local timestamp
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
namespace SteamNetworkingSocketsLib {
|
||||
|
||||
static std::atomic<long long> s_usecTimeLastReturned;
|
||||
|
||||
// Start with an offset so that a timestamp of zero is always pretty far in the past.
|
||||
// But round it up to nice round number, so that looking at timestamps in the debugger
|
||||
// is easy to read.
|
||||
const long long k_nInitialTimestampMin = k_nMillion*24*3600*30;
|
||||
const long long k_nInitialTimestamp = 3000000000000ll;
|
||||
COMPILE_TIME_ASSERT( 2000000000000ll < k_nInitialTimestampMin );
|
||||
COMPILE_TIME_ASSERT( k_nInitialTimestampMin < k_nInitialTimestamp );
|
||||
static std::atomic<long long> s_usecTimeOffset( k_nInitialTimestamp );
|
||||
|
||||
// Max delta allowed between GetLocalTimestamp calls before we clamp.
|
||||
// Matches the service thread's max poll interval (k_msMaxPollWait * 1100us).
|
||||
constexpr SteamNetworkingMicroseconds k_usecMaxTimestampDelta = 1100 * 1000;
|
||||
|
||||
// Defined in steamnetworkingsockets_socketthread.cpp
|
||||
extern std::atomic<int> s_nLowLevelSupportRefCount;
|
||||
|
||||
} // namespace SteamNetworkingSocketsLib
|
||||
|
||||
SteamNetworkingMicroseconds SteamNetworkingSockets_GetLocalTimestamp()
|
||||
{
|
||||
SteamNetworkingMicroseconds usecResult;
|
||||
long long usecLastReturned;
|
||||
for (;;)
|
||||
{
|
||||
// Fetch values into locals (probably registers)
|
||||
usecLastReturned = SteamNetworkingSocketsLib::s_usecTimeLastReturned;
|
||||
long long usecOffset = SteamNetworkingSocketsLib::s_usecTimeOffset;
|
||||
|
||||
// Read raw timer
|
||||
uint64 usecRaw = Plat_USTime();
|
||||
|
||||
// Add offset to get value in "SteamNetworkingMicroseconds" time
|
||||
usecResult = usecRaw + usecOffset;
|
||||
|
||||
// How much raw timer time (presumed to be wall clock time) has elapsed since
|
||||
// we read the timer?
|
||||
SteamNetworkingMicroseconds usecElapsed = usecResult - usecLastReturned;
|
||||
Assert( usecElapsed >= 0 ); // Our raw timer function is not monotonic! We assume this never happens!
|
||||
if ( usecElapsed <= SteamNetworkingSocketsLib::k_usecMaxTimestampDelta )
|
||||
{
|
||||
// Should be the common case - only a relatively small of time has elapsed
|
||||
break;
|
||||
}
|
||||
if ( SteamNetworkingSocketsLib::s_nLowLevelSupportRefCount.load(std::memory_order_acquire) <= 0 )
|
||||
{
|
||||
// We don't have any expectation that we should be updating the timer frequently,
|
||||
// so a big jump in the value just means they aren't calling it very often
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE: We should only rarely get here, and probably as a result of running under the debugger
|
||||
|
||||
// Adjust offset so that delta between timestamps is limited
|
||||
long long usecNewOffset = usecOffset - ( usecElapsed - SteamNetworkingSocketsLib::k_usecMaxTimestampDelta );
|
||||
usecResult = usecRaw + usecNewOffset;
|
||||
|
||||
// Save the new offset.
|
||||
if ( SteamNetworkingSocketsLib::s_usecTimeOffset.compare_exchange_strong( usecOffset, usecNewOffset ) )
|
||||
break;
|
||||
|
||||
// Race condition which should be extremely rare. Some other thread changed the offset, in the time
|
||||
// between when we fetched it and now. (So, a really small race window!) Just start all over from
|
||||
// the beginning.
|
||||
}
|
||||
|
||||
// Save the last value returned. Unless another thread snuck in there while we were busy.
|
||||
// If so, that's OK.
|
||||
SteamNetworkingSocketsLib::s_usecTimeLastReturned.compare_exchange_strong( usecLastReturned, usecResult );
|
||||
|
||||
return usecResult;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// memory override
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <tier0/memdbgoff.h>
|
||||
|
||||
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MEM_OVERRIDE
|
||||
|
||||
namespace SteamNetworkingSocketsLib {
|
||||
static bool s_bHasAllocatedMemory = false;
|
||||
|
||||
static void* (*s_pfn_malloc)( size_t s ) = malloc;
|
||||
static void (*s_pfn_free)( void *p ) = free;
|
||||
static void* (*s_pfn_realloc)( void *p, size_t s ) = realloc;
|
||||
}
|
||||
|
||||
void *SteamNetworkingSockets_Malloc( size_t s )
|
||||
{
|
||||
s_bHasAllocatedMemory = true;
|
||||
return (*s_pfn_malloc)( s );
|
||||
}
|
||||
|
||||
void *SteamNetworkingSockets_Realloc( void *p, size_t s )
|
||||
{
|
||||
s_bHasAllocatedMemory = true;
|
||||
return (*s_pfn_realloc)( p, s );
|
||||
}
|
||||
|
||||
void SteamNetworkingSockets_Free( void *p )
|
||||
{
|
||||
(*s_pfn_free)( p );
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetCustomMemoryAllocator(
|
||||
void* (*pfn_malloc)( size_t s ),
|
||||
void (*pfn_free)( void *p ),
|
||||
void* (*pfn_realloc)( void *p, size_t s )
|
||||
) {
|
||||
Assert( !s_bHasAllocatedMemory ); // Too late!
|
||||
|
||||
s_pfn_malloc = pfn_malloc;
|
||||
s_pfn_free = pfn_free;
|
||||
s_pfn_realloc = pfn_realloc;
|
||||
}
|
||||
#endif
|
||||
@@ -1,14 +1,11 @@
|
||||
//====== Copyright Valve Corporation, All rights reserved. ====================
|
||||
//
|
||||
// "Low level" stuff used by the SteamNetworkingSockets client library to
|
||||
// interface with the operating system. Ideally, most OS-specific details are
|
||||
// handled in this file.
|
||||
// Socket and service thread management for SteamNetworkingSockets.
|
||||
//
|
||||
// - Dealing with OS sockets, sending/receiving of UDP packets
|
||||
// - Simulating network conditions such as fake lag/loss/reording/jitter
|
||||
// - Managing the main service thread, polling efficiently
|
||||
// - Dispatching received packets to the registered callbacks.
|
||||
// - Lock (mutex) details, especially hygiene enforcement and debugging
|
||||
// - Support for wifi adapters that can send on both bands simultaneously
|
||||
//
|
||||
#include <thread>
|
||||
@@ -91,21 +88,7 @@ constexpr int k_cbETWEventUDPPacketDataSize = 16;
|
||||
|
||||
namespace SteamNetworkingSocketsLib {
|
||||
|
||||
inline void ETW_LongOp( const char *opName, SteamNetworkingMicroseconds usec, const char *pszInfo )
|
||||
{
|
||||
if ( !pszInfo )
|
||||
pszInfo = "";
|
||||
TraceLoggingWrite(
|
||||
HTraceLogging_SteamNetworkingSockets,
|
||||
"LongOp",
|
||||
TraceLoggingLevel( WINEVENT_LEVEL_WARNING ),
|
||||
TraceLoggingUInt64( usec, "Microseconds" ),
|
||||
TraceLoggingString( pszInfo, "ExtraInfo" )
|
||||
);
|
||||
}
|
||||
|
||||
constexpr int k_msMaxPollWait = 1000;
|
||||
constexpr SteamNetworkingMicroseconds k_usecMaxTimestampDelta = k_msMaxPollWait * 1100;
|
||||
|
||||
int g_cbUDPSocketBufferSize = 256*1024;
|
||||
|
||||
@@ -113,415 +96,7 @@ int g_cbUDPSocketBufferSize = 256*1024;
|
||||
int g_nSendECNAuto = -1;
|
||||
#endif
|
||||
|
||||
/// Global lock for all local data structures
|
||||
static Lock<RecursiveTimedMutexImpl> s_mutexGlobalLock( "global", 0, LockDebugInfo::k_nOrder_Global );
|
||||
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
|
||||
// By default, complain if we hold the lock for more than this long.
|
||||
// TSan adds 10-20x overhead so the threshold is scaled up accordingly.
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
constexpr SteamNetworkingMicroseconds k_usecDefaultLongLockHeldWarningThreshold = 5*1000*20;
|
||||
#else
|
||||
constexpr SteamNetworkingMicroseconds k_usecDefaultLongLockHeldWarningThreshold = 5*1000;
|
||||
#endif
|
||||
|
||||
// Debug the locks active on the cu
|
||||
struct ThreadLockDebugInfo
|
||||
{
|
||||
static constexpr int k_nMaxHeldLocks = 8;
|
||||
static constexpr int k_nMaxTags = 32;
|
||||
|
||||
int m_nHeldLocks = 0;
|
||||
int m_nTags = 0;
|
||||
|
||||
SteamNetworkingMicroseconds m_usecLongLockWarningThreshold;
|
||||
SteamNetworkingMicroseconds m_usecIgnoreLongLockWaitTimeUntil;
|
||||
SteamNetworkingMicroseconds m_usecOuterLockStartTime; // Time when we started waiting on outermost lock (if we don't have it yet), or when we aquired the lock (if we have it)
|
||||
|
||||
const LockDebugInfo *m_arHeldLocks[ k_nMaxHeldLocks ];
|
||||
struct Tag_t
|
||||
{
|
||||
const char *m_pszTag;
|
||||
int m_nCount;
|
||||
};
|
||||
Tag_t m_arTags[ k_nMaxTags ];
|
||||
|
||||
inline void AddTag( const char *pszTag );
|
||||
};
|
||||
|
||||
static void (*s_fLockAcquiredCallback)( const char *tags, SteamNetworkingMicroseconds usecWaited );
|
||||
static void (*s_fLockHeldCallback)( const char *tags, SteamNetworkingMicroseconds usecWaited );
|
||||
|
||||
// Threshold for warning about waiting on any lock.
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
static SteamNetworkingMicroseconds s_usecLockWaitWarningThreshold = 300*1000;
|
||||
#else
|
||||
static SteamNetworkingMicroseconds s_usecLockWaitWarningThreshold = 2*1000;
|
||||
#endif
|
||||
|
||||
// Threshold for the service-thread-specific *assert*
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
static SteamNetworkingMicroseconds s_usecServiceThreadLockWaitWarning = 300*1000;
|
||||
#else
|
||||
static SteamNetworkingMicroseconds s_usecServiceThreadLockWaitWarning = 50*1000;
|
||||
#endif
|
||||
|
||||
/// Get the per-thread debug info
|
||||
static ThreadLockDebugInfo &GetThreadDebugInfo()
|
||||
{
|
||||
// Apple seems to hate thread_local. Is there some sort of feature
|
||||
// define a can check here? It's a shame because it's really very
|
||||
// efficient on MSVC, gcc, and clang on Windows and linux.
|
||||
//
|
||||
// Apple seems to support thread_local starting with Xcode 8.0
|
||||
#if defined(__APPLE__) && __clang_major__ < 8
|
||||
|
||||
static pthread_key_t key;
|
||||
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
// One time init the TLS key
|
||||
pthread_once( &key_once,
|
||||
[](){ // Initialization code to run once
|
||||
pthread_key_create(
|
||||
&key,
|
||||
[](void *ptr) { free(ptr); } // Destructor function
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Get current object
|
||||
void *result = pthread_getspecific(key);
|
||||
if ( unlikely( result == nullptr ) )
|
||||
{
|
||||
result = malloc( sizeof(ThreadLockDebugInfo) );
|
||||
memset( result, 0, sizeof(ThreadLockDebugInfo) );
|
||||
pthread_setspecific(key, result);
|
||||
}
|
||||
return *static_cast<ThreadLockDebugInfo *>( result );
|
||||
#else
|
||||
|
||||
// Use thread_local
|
||||
thread_local ThreadLockDebugInfo tls_lockDebugInfo;
|
||||
return tls_lockDebugInfo;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// If non-NULL, add a "tag" to the lock journal for the current thread.
|
||||
/// This is useful so that if we hold a lock for a long time, we can get
|
||||
/// an idea what sorts of operations were taking a long time.
|
||||
inline void ThreadLockDebugInfo::AddTag( const char *pszTag )
|
||||
{
|
||||
if ( !pszTag )
|
||||
return;
|
||||
|
||||
Assert( m_nHeldLocks > 0 ); // Can't add a tag unless we are locked!
|
||||
|
||||
for ( int i = 0 ; i < m_nTags ; ++i )
|
||||
{
|
||||
if ( m_arTags[i].m_pszTag == pszTag )
|
||||
{
|
||||
++m_arTags[i].m_nCount;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( m_nTags >= ThreadLockDebugInfo::k_nMaxTags )
|
||||
return;
|
||||
|
||||
m_arTags[ m_nTags ].m_pszTag = pszTag;
|
||||
m_arTags[ m_nTags ].m_nCount = 1;
|
||||
++m_nTags;
|
||||
}
|
||||
|
||||
LockDebugInfo::~LockDebugInfo()
|
||||
{
|
||||
// We should not be locked! If we are, remove us
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
AssertMsg( false, "Lock '%s' being destroyed while it is held!", m_pszName );
|
||||
AboutToUnlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LockDebugInfo::AboutToLock( bool bTry )
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
|
||||
// First lock held by this thread?
|
||||
if ( t.m_nHeldLocks == 0 )
|
||||
{
|
||||
// Remember when we started trying to lock
|
||||
t.m_usecOuterLockStartTime = SteamNetworkingSockets_GetLocalTimestamp();
|
||||
return;
|
||||
}
|
||||
|
||||
// We already hold a lock. Check for taking locks in such a way
|
||||
// that might lead to deadlocks.
|
||||
const LockDebugInfo *pTopLock = t.m_arHeldLocks[ t.m_nHeldLocks-1 ];
|
||||
|
||||
// Taking locks in increasing order is always allowed
|
||||
if ( likely( pTopLock->m_nOrder < m_nOrder ) )
|
||||
return;
|
||||
|
||||
// Global lock *must* always be the outermost lock. (It is legal to take other locks in
|
||||
// between and then lock the global lock recursively.)
|
||||
const bool bHoldGlobalLock = t.m_arHeldLocks[ 0 ] == &s_mutexGlobalLock;
|
||||
AssertMsg(
|
||||
bHoldGlobalLock || this != &s_mutexGlobalLock,
|
||||
"Taking global lock while already holding lock '%s'", t.m_arHeldLocks[ 0 ]->m_pszName
|
||||
);
|
||||
|
||||
// If they are only "trying", we allow out-of-order behaviour.
|
||||
if ( bTry )
|
||||
return;
|
||||
|
||||
// It's always OK to lock recursively.
|
||||
//
|
||||
// (Except for "short duration" locks, which are allowed to
|
||||
// use a mutex implementation that does not support this.)
|
||||
if ( !( m_nFlags & k_nFlag_ShortDuration ) )
|
||||
{
|
||||
for ( int i = 0 ; i < t.m_nHeldLocks ; ++i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Taking multiple object locks? This is allowed under certain circumstances
|
||||
if ( likely( pTopLock->m_nOrder == m_nOrder && m_nOrder == k_nOrder_ObjectOrTable ) )
|
||||
{
|
||||
|
||||
// If we hold the global lock, it's OK
|
||||
if ( bHoldGlobalLock )
|
||||
return;
|
||||
|
||||
// If the global lock isn't held, then no more than one
|
||||
// object lock is allowed, since two different threads
|
||||
// might take them in different order.
|
||||
constexpr int k_nObjectFlags = LockDebugInfo::k_nFlag_Connection | LockDebugInfo::k_nFlag_PollGroup;
|
||||
if (
|
||||
( ( m_nFlags & k_nObjectFlags ) != 0 )
|
||||
//|| ( m_nFlags & k_nFlag_Table ) // We actually do this in one place when we know it's OK. Not worth it right now to get this situation exempted from the checking.
|
||||
) {
|
||||
// We must not already hold any existing object locks (except perhaps this one)
|
||||
for ( int i = 0 ; i < t.m_nHeldLocks ; ++i )
|
||||
{
|
||||
const LockDebugInfo *pOtherLock = t.m_arHeldLocks[ i ];
|
||||
AssertMsg( pOtherLock == this || !( pOtherLock->m_nFlags & k_nObjectFlags ),
|
||||
"Taking lock '%s' and then '%s', while not holding the global lock", pOtherLock->m_pszName, m_pszName );
|
||||
}
|
||||
}
|
||||
|
||||
// Usage is OK if we didn't find any problems above
|
||||
return;
|
||||
}
|
||||
|
||||
AssertMsg( false, "Taking lock '%s' while already holding lock '%s'", m_pszName, pTopLock->m_pszName );
|
||||
}
|
||||
|
||||
void LockDebugInfo::OnLocked( const char *pszTag )
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
|
||||
Assert( t.m_nHeldLocks < ThreadLockDebugInfo::k_nMaxHeldLocks );
|
||||
t.m_arHeldLocks[ t.m_nHeldLocks++ ] = this;
|
||||
|
||||
if ( t.m_nHeldLocks == 1 )
|
||||
{
|
||||
SteamNetworkingMicroseconds usecNow = SteamNetworkingSockets_GetLocalTimestamp();
|
||||
SteamNetworkingMicroseconds usecTimeSpentWaitingOnLock = usecNow - t.m_usecOuterLockStartTime;
|
||||
t.m_usecLongLockWarningThreshold = k_usecDefaultLongLockHeldWarningThreshold;
|
||||
t.m_nTags = 0;
|
||||
|
||||
if ( usecTimeSpentWaitingOnLock > s_usecLockWaitWarningThreshold && usecNow > t.m_usecIgnoreLongLockWaitTimeUntil )
|
||||
{
|
||||
if ( pszTag )
|
||||
SpewWarning( "Waited %.1fms for SteamNetworkingSockets lock [%s]", usecTimeSpentWaitingOnLock*1e-3, pszTag );
|
||||
else
|
||||
SpewWarning( "Waited %.1fms for SteamNetworkingSockets lock", usecTimeSpentWaitingOnLock*1e-3 );
|
||||
ETW_LongOp( "lock wait", usecTimeSpentWaitingOnLock, pszTag );
|
||||
}
|
||||
|
||||
auto callback = s_fLockAcquiredCallback; // save to temp, to prevent very narrow race condition where variable is cleared after we null check it, and we call null
|
||||
if ( callback )
|
||||
callback( pszTag, usecTimeSpentWaitingOnLock );
|
||||
|
||||
t.m_usecOuterLockStartTime = usecNow;
|
||||
}
|
||||
|
||||
t.AddTag( pszTag );
|
||||
}
|
||||
|
||||
void LockDebugInfo::AboutToUnlock()
|
||||
{
|
||||
char tags[ 256 ];
|
||||
|
||||
SteamNetworkingMicroseconds usecElapsed = 0;
|
||||
SteamNetworkingMicroseconds usecElapsedTooLong = 0;
|
||||
auto lockHeldCallback = s_fLockHeldCallback;
|
||||
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
Assert( t.m_nHeldLocks > 0 );
|
||||
|
||||
// Unlocking the last lock?
|
||||
if ( t.m_nHeldLocks == 1 )
|
||||
{
|
||||
|
||||
// We're about to do the final release. How long did we hold the lock?
|
||||
usecElapsed = SteamNetworkingSockets_GetLocalTimestamp() - t.m_usecOuterLockStartTime;
|
||||
|
||||
// Too long? We need to check the threshold here because the threshold could
|
||||
// change by another thread immediately after we release the lock. Also, if
|
||||
// we're debugging, all bets are off. They could have hit a breakpoint, and
|
||||
// we don't want to create a bunch of confusing spew with spurious asserts
|
||||
if ( usecElapsed >= t.m_usecLongLockWarningThreshold && !Plat_IsInDebugSession() )
|
||||
{
|
||||
usecElapsedTooLong = usecElapsed;
|
||||
}
|
||||
|
||||
if ( usecElapsedTooLong > 0 || lockHeldCallback )
|
||||
{
|
||||
char *p = tags;
|
||||
char *end = tags + sizeof(tags) - 1;
|
||||
for ( int i = 0 ; i < t.m_nTags && p+5 < end ; ++i )
|
||||
{
|
||||
if ( p > tags )
|
||||
*(p++) = ',';
|
||||
|
||||
const ThreadLockDebugInfo::Tag_t &tag = t.m_arTags[i];
|
||||
int taglen = std::min( int(end-p), (int)V_strlen( tag.m_pszTag ) );
|
||||
memcpy( p, tag.m_pszTag, taglen );
|
||||
p += taglen;
|
||||
|
||||
if ( tag.m_nCount > 1 )
|
||||
{
|
||||
int l = end-p;
|
||||
if ( l <= 5 )
|
||||
break;
|
||||
p += V_snprintf( p, l, "(x%d)", tag.m_nCount );
|
||||
}
|
||||
}
|
||||
*p = '\0';
|
||||
}
|
||||
|
||||
t.m_nTags = 0;
|
||||
t.m_usecOuterLockStartTime = 0; // Just for grins.
|
||||
}
|
||||
|
||||
if ( usecElapsed > 0 && lockHeldCallback )
|
||||
{
|
||||
lockHeldCallback(tags, usecElapsed);
|
||||
}
|
||||
|
||||
// Yelp if we held the lock for longer than the threshold.
|
||||
if ( usecElapsedTooLong != 0 )
|
||||
{
|
||||
SpewWarning(
|
||||
"SteamNetworkingSockets lock held for %.1fms. (Performance warning.) %s\n"
|
||||
"This is usually a symptom of a general performance problem such as thread starvation.",
|
||||
usecElapsedTooLong*1e-3, tags
|
||||
);
|
||||
ETW_LongOp( "lock held", usecElapsedTooLong, tags );
|
||||
}
|
||||
|
||||
// NOTE: We are allowed to unlock out of order! We specifically
|
||||
// do this with the table lock!
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
--t.m_nHeldLocks;
|
||||
if ( i < t.m_nHeldLocks ) // Don't do the memmove in the common case of stack pop
|
||||
memmove( &t.m_arHeldLocks[i], &t.m_arHeldLocks[i+1], (t.m_nHeldLocks-i) * sizeof(t.m_arHeldLocks[0]) );
|
||||
t.m_arHeldLocks[t.m_nHeldLocks] = nullptr; // Just for grins
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AssertMsg( false, "Unlocked a lock '%s' that wasn't held?", m_pszName );
|
||||
}
|
||||
|
||||
void LockDebugInfo::_AssertHeldByCurrentThread( const char *pszFile, int line, const char *pszTag ) const
|
||||
{
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
for ( int i = t.m_nHeldLocks-1 ; i >= 0 ; --i )
|
||||
{
|
||||
if ( t.m_arHeldLocks[i] == this )
|
||||
{
|
||||
t.AddTag( pszTag );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AssertMsg( false, "%s(%d): Lock '%s' not held", pszFile, line, m_pszName );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::SetLongLockWarningThresholdMS( const char *pszTag, int msWarningThreshold )
|
||||
{
|
||||
AssertHeldByCurrentThread( pszTag );
|
||||
SteamNetworkingMicroseconds usecWarningThreshold = SteamNetworkingMicroseconds{msWarningThreshold}*1000;
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
if ( t.m_usecLongLockWarningThreshold < usecWarningThreshold )
|
||||
{
|
||||
t.m_usecLongLockWarningThreshold = usecWarningThreshold;
|
||||
t.m_usecIgnoreLongLockWaitTimeUntil = SteamNetworkingSockets_GetLocalTimestamp() + t.m_usecLongLockWarningThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::_AssertHeldByCurrentThread( const char *pszFile, int line )
|
||||
{
|
||||
s_mutexGlobalLock._AssertHeldByCurrentThread( pszFile, line, nullptr );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::_AssertHeldByCurrentThread( const char *pszFile, int line, const char *pszTag )
|
||||
{
|
||||
s_mutexGlobalLock._AssertHeldByCurrentThread( pszFile, line, pszTag );
|
||||
}
|
||||
|
||||
#endif // #if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
|
||||
void SteamNetworkingGlobalLock::Lock( const char *pszTag )
|
||||
{
|
||||
s_mutexGlobalLock.lock( pszTag );
|
||||
}
|
||||
|
||||
bool SteamNetworkingGlobalLock::TryLock( const char *pszTag, int msTimeout )
|
||||
{
|
||||
return s_mutexGlobalLock.try_lock_for( msTimeout, pszTag );
|
||||
}
|
||||
|
||||
void SteamNetworkingGlobalLock::Unlock()
|
||||
{
|
||||
s_mutexGlobalLock.unlock();
|
||||
}
|
||||
|
||||
static void SeedWeakRandomGenerator()
|
||||
{
|
||||
|
||||
// Seed cheesy random number generator using true source of entropy
|
||||
int temp;
|
||||
CCrypto::GenerateRandomBlock( &temp, sizeof(temp) );
|
||||
WeakRandomSeed( temp );
|
||||
}
|
||||
|
||||
static std::atomic<long long> s_usecTimeLastReturned;
|
||||
|
||||
// Start with an offset so that a timestamp of zero is always pretty far in the past.
|
||||
// But round it up to nice round number, so that looking at timestamps in the debugger
|
||||
// is easy to read.
|
||||
const long long k_nInitialTimestampMin = k_nMillion*24*3600*30;
|
||||
const long long k_nInitialTimestamp = 3000000000000ll;
|
||||
COMPILE_TIME_ASSERT( 2000000000000ll < k_nInitialTimestampMin );
|
||||
COMPILE_TIME_ASSERT( k_nInitialTimestampMin < k_nInitialTimestamp );
|
||||
static std::atomic<long long> s_usecTimeOffset( k_nInitialTimestamp );
|
||||
|
||||
static std::atomic<int> s_nLowLevelSupportRefCount(0);
|
||||
std::atomic<int> s_nLowLevelSupportRefCount(0);
|
||||
static volatile bool s_bManualPollMode;
|
||||
|
||||
// Try to guess if the route the specified address is probably "local".
|
||||
@@ -2129,14 +1704,6 @@ IRawUDPSocket *OpenRawUDPSocket( CRecvPacketCallback callback, SteamNetworkingEr
|
||||
return OpenRawUDPSocketInternal( callback, errMsg, pAddrLocal, pnAddressFamilies );
|
||||
}
|
||||
|
||||
static inline void AssertGlobalLockHeldExactlyOnce()
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
ThreadLockDebugInfo &t = GetThreadDebugInfo();
|
||||
Assert( t.m_nHeldLocks == 1 && t.m_arHeldLocks[0] == &s_mutexGlobalLock );
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Draw one specific UDP socket. Returns false if we detect a
|
||||
/// global shutdown attempt and abort
|
||||
static bool DrainSocket( CRawUDPSocketImpl *pSock )
|
||||
@@ -3814,60 +3381,6 @@ void SteamNetworkingSocketsLowLevelValidate( CValidator &validator )
|
||||
}
|
||||
#endif
|
||||
|
||||
SteamNetworkingMicroseconds SteamNetworkingSockets_GetLocalTimestamp()
|
||||
{
|
||||
SteamNetworkingMicroseconds usecResult;
|
||||
long long usecLastReturned;
|
||||
for (;;)
|
||||
{
|
||||
// Fetch values into locals (probably registers)
|
||||
usecLastReturned = SteamNetworkingSocketsLib::s_usecTimeLastReturned;
|
||||
long long usecOffset = SteamNetworkingSocketsLib::s_usecTimeOffset;
|
||||
|
||||
// Read raw timer
|
||||
uint64 usecRaw = Plat_USTime();
|
||||
|
||||
// Add offset to get value in "SteamNetworkingMicroseconds" time
|
||||
usecResult = usecRaw + usecOffset;
|
||||
|
||||
// How much raw timer time (presumed to be wall clock time) has elapsed since
|
||||
// we read the timer?
|
||||
SteamNetworkingMicroseconds usecElapsed = usecResult - usecLastReturned;
|
||||
Assert( usecElapsed >= 0 ); // Our raw timer function is not monotonic! We assume this never happens!
|
||||
if ( usecElapsed <= k_usecMaxTimestampDelta )
|
||||
{
|
||||
// Should be the common case - only a relatively small of time has elapsed
|
||||
break;
|
||||
}
|
||||
if ( SteamNetworkingSocketsLib::s_nLowLevelSupportRefCount.load(std::memory_order_acquire) <= 0 )
|
||||
{
|
||||
// We don't have any expectation that we should be updating the timer frequently,
|
||||
// so a big jump in the value just means they aren't calling it very often
|
||||
break;
|
||||
}
|
||||
|
||||
// NOTE: We should only rarely get here, and probably as a result of running under the debugger
|
||||
|
||||
// Adjust offset so that delta between timestamps is limited
|
||||
long long usecNewOffset = usecOffset - ( usecElapsed - k_usecMaxTimestampDelta );
|
||||
usecResult = usecRaw + usecNewOffset;
|
||||
|
||||
// Save the new offset.
|
||||
if ( SteamNetworkingSocketsLib::s_usecTimeOffset.compare_exchange_strong( usecOffset, usecNewOffset ) )
|
||||
break;
|
||||
|
||||
// Race condition which should be extremely rare. Some other thread changed the offset, in the time
|
||||
// between when we fetched it and now. (So, a really small race window!) Just start all over from
|
||||
// the beginning.
|
||||
}
|
||||
|
||||
// Save the last value returned. Unless another thread snuck in there while we were busy.
|
||||
// If so, that's OK.
|
||||
SteamNetworkingSocketsLib::s_usecTimeLastReturned.compare_exchange_strong( usecLastReturned, usecResult );
|
||||
|
||||
return usecResult;
|
||||
}
|
||||
|
||||
bool ResolveHostname( const char* pszHostname, CUtlVector< SteamNetworkingIPAddr > *pAddrs )
|
||||
{
|
||||
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_RESOLVEHOSTNAME
|
||||
@@ -4141,80 +3654,8 @@ STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_Poll( int msMaxWait
|
||||
SteamNetworkingGlobalLock::Unlock();
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockWaitWarningThreshold( SteamNetworkingMicroseconds usecTheshold )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_usecLockWaitWarningThreshold = usecTheshold;
|
||||
#endif
|
||||
s_usecServiceThreadLockWaitWarning = usecTheshold;
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockAcquiredCallback( void (*callback)( const char *tags, SteamNetworkingMicroseconds usecWaited ) )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_fLockAcquiredCallback = callback;
|
||||
#else
|
||||
// Should we assert here?
|
||||
#endif
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetLockHeldCallback( void (*callback)( const char *tags, SteamNetworkingMicroseconds usecWaited ) )
|
||||
{
|
||||
#if STEAMNETWORKINGSOCKETS_LOCK_DEBUG_LEVEL > 0
|
||||
s_fLockHeldCallback = callback;
|
||||
#else
|
||||
// Should we assert here?
|
||||
#endif
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetServiceThreadInitCallback( void (*callback)() )
|
||||
{
|
||||
AssertMsg( !IsServiceThreadRunning(), "Too late!" );
|
||||
s_fnServiceThreadInitCallback = callback;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// memory override
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <tier0/memdbgoff.h>
|
||||
|
||||
#ifdef STEAMNETWORKINGSOCKETS_ENABLE_MEM_OVERRIDE
|
||||
|
||||
static bool s_bHasAllocatedMemory = false;
|
||||
|
||||
static void* (*s_pfn_malloc)( size_t s ) = malloc;
|
||||
static void (*s_pfn_free)( void *p ) = free;
|
||||
static void* (*s_pfn_realloc)( void *p, size_t s ) = realloc;
|
||||
|
||||
void *SteamNetworkingSockets_Malloc( size_t s )
|
||||
{
|
||||
s_bHasAllocatedMemory = true;
|
||||
return (*s_pfn_malloc)( s );
|
||||
}
|
||||
|
||||
void *SteamNetworkingSockets_Realloc( void *p, size_t s )
|
||||
{
|
||||
s_bHasAllocatedMemory = true;
|
||||
return (*s_pfn_realloc)( p, s );
|
||||
}
|
||||
|
||||
void SteamNetworkingSockets_Free( void *p )
|
||||
{
|
||||
(*s_pfn_free)( p );
|
||||
}
|
||||
|
||||
STEAMNETWORKINGSOCKETS_INTERFACE void SteamNetworkingSockets_SetCustomMemoryAllocator(
|
||||
void* (*pfn_malloc)( size_t s ),
|
||||
void (*pfn_free)( void *p ),
|
||||
void* (*pfn_realloc)( void *p, size_t s )
|
||||
) {
|
||||
Assert( !s_bHasAllocatedMemory ); // Too late!
|
||||
|
||||
s_pfn_malloc = pfn_malloc;
|
||||
s_pfn_free = pfn_free;
|
||||
s_pfn_realloc = pfn_realloc;
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user