diff options
Diffstat (limited to 'gcsdk/job.cpp')
| -rw-r--r-- | gcsdk/job.cpp | 1451 |
1 files changed, 1451 insertions, 0 deletions
diff --git a/gcsdk/job.cpp b/gcsdk/job.cpp new file mode 100644 index 0000000..0eef357 --- /dev/null +++ b/gcsdk/job.cpp @@ -0,0 +1,1451 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= + + +#include "stdafx.h" + +#ifdef WIN32 +#include "typeinfo.h" +#else +#include <typeinfo> +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ +//----------------------------------------------------------------------------- +// Purpose: EJobPauseReason descriptions +//----------------------------------------------------------------------------- +static const char * const k_prgchJobPauseReason[] = +{ + "active", + "not started", + "netmsg", + "sleep for time", + "waiting for lock", + "yielding", + "SQL", + "work item", +}; + +COMPILE_TIME_ASSERT( ARRAYSIZE( k_prgchJobPauseReason ) == k_EJobPauseReasonCount ); + +CJob *g_pJobCur = NULL; + + +//----------------------------------------------------------------------------- +// Purpose: Delete a Job +// Input: pJob - The Job to delete +//----------------------------------------------------------------------------- +void CJob::DeleteJob( CJob *pJob ) +{ + // we can't delete the if we still have a pending work item + pJob->WaitForThreadFuncWorkItemBlocking(); + delete pJob; +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input: pServerParent - The server we belong to +//----------------------------------------------------------------------------- +CJob::CJob( CJobMgr &jobMgr, const char *pchJobName ) : m_JobMgr( jobMgr ), m_pchJobName( pchJobName ) +{ + m_ePauseReason = k_EJobPauseReasonNotStarted; + + m_JobID = jobMgr.GetNewJobID(); + m_pJobType = NULL; + m_bWorkItemCanceled = false; + m_hCoroutine = Coroutine_Create( &BRunProxy, this ); + m_pvStartParam = NULL; + m_bRunFromMsg = false; + m_pJobPrev = NULL; + m_pWaitingOnLock = NULL; + m_pJobToNotifyOnLockRelease = NULL; + m_pWaitingOnWorkItem = NULL; + m_STimeStarted.SetToJobTime(); + m_STimeSwitched.SetToJobTime(); + m_STimeNextHeartbeat.SetFromJobTime( k_cMicroSecJobHeartbeat ); + m_bIsLongRunning = false; + m_cLocksAttempted = 0; + m_cLocksWaitedFor = 0; + m_flags.m_uFlags = 0; + m_cyclecountTotal = 0; + m_unWaitMsgType = 0; + + GetJobMgr().InsertJob( *this ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CJob::~CJob() +{ + // don't want SendMsgToConnection to call back into us, we *know* + // we are replying to these other jobs now + g_pJobCur = NULL; + + // reset the job pointer + g_pJobCur = m_pJobPrev; + + // remove from the job tracking list + GetJobMgr().RemoveJob( *this ); + + // Forcefully release any locks + ReleaseLocks(); + + // free any network messages we've allocated + FOR_EACH_VEC( m_vecNetPackets, i ) + { + m_vecNetPackets[i]->Release(); + } + m_vecNetPackets.RemoveAll(); + + AssertMsg2( 0 == GetDoNotYieldDepth(), "Job ending with %d open Do Not Yields. Are we missing a END_DO_NOT_YIELD()? Innermost delared at %s", + GetDoNotYieldDepth(), m_stackDoNotYieldGuards[m_stackDoNotYieldGuards.Head()] ); +} + + +//----------------------------------------------------------------------------- +// Purpose: if necessary wait for the pending work item to finish +//----------------------------------------------------------------------------- +void CJob::WaitForThreadFuncWorkItemBlocking() +{ + if ( m_pWaitingOnWorkItem ) + { + switch ( GetPauseReason() ) + { + case k_EJobPauseReasonWorkItem: + // force the workitem to be canceled in case it's still in the in-queue + m_pWaitingOnWorkItem->ForceTimeOut(); + + // we can't shutdown the job while it's work item is currently running + // alot of work items refernce back into the job object + while ( m_pWaitingOnWorkItem->BIsRunning() ) + ThreadSleep( 25 ); + + m_pWaitingOnWorkItem = NULL; + break; + +#if 0 // not used in gcsdk + case k_EJobPauseReasonGeneric: + AssertMsg1( ( !m_pWaitingForGeneric || ( m_pWaitingForGeneric == ( void * ) 1 ) ), "CJob::WaitForThreadFuncWorkItemBlocking job %s will leak generic heap object", GetName() ); + // Let another assert fire later, don't null-out: m_pWaitingForGeneric = NULL; + break; +#endif + + default: + AssertMsg1( false, "CJob::WaitForThreadFuncWorkItemBlocking job %s has unexpected work item state", GetName() ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: returns the name of the job +//----------------------------------------------------------------------------- +const char *CJob::GetName() const +{ + if ( m_pchJobName ) + return m_pchJobName; + else if ( m_pJobType ) + return m_pJobType->m_pchName; + else + return ""; +} + + +//----------------------------------------------------------------------------- +// Purpose: string description of why we're paused +//----------------------------------------------------------------------------- +const char *CJob::GetPauseReasonDescription() const +{ + static char srgchPauseReason[k_cSmallBuff]; + if ( GetPauseReason() < Q_ARRAYSIZE( k_prgchJobPauseReason ) ) + { + switch( GetPauseReason() ) + { + case k_EJobPauseReasonWaitingForLock: + { + Q_snprintf( srgchPauseReason, k_cSmallBuff, "WOL: 0x%x (%s)", (unsigned int)m_pWaitingOnLock, m_pWaitingOnLock ? m_pWaitingOnLock->GetName() : "null" ); + return srgchPauseReason; + } + + case k_EJobPauseReasonNetworkMsg: + { + const char *pchMsgType; + if( g_theMessageList.GetMessage( m_unWaitMsgType, &pchMsgType, 0xFF ) ) + { + Q_snprintf( srgchPauseReason, k_cSmallBuff, "NetMsg: %s", pchMsgType ); + return srgchPauseReason; + } + else + { + Q_snprintf( srgchPauseReason, k_cSmallBuff, "NetMsg: Unknown %d", m_unWaitMsgType ); + return srgchPauseReason; + } + } + + default: + return k_prgchJobPauseReason[ GetPauseReason() ]; + } + } + + return "undefined"; +} + + +//----------------------------------------------------------------------------- +// Purpose: accessor to get access to the JobMgr from the server we belong to +//----------------------------------------------------------------------------- +CJobMgr &CJob::GetJobMgr() +{ + return m_JobMgr; +} + + +//----------------------------------------------------------------------------- +// Purpose: Starts the job, based on the current network msg +// Input : hConnection - Connection that the message was received form +// pubPkt - The raw message packet +// cubPkt - The size of the message packet +//----------------------------------------------------------------------------- +void CJob::StartJobFromNetworkMsg( IMsgNetPacket *pNetPacket, const JobID_t &gidJobIDSrc ) +{ + // hang on to the packet with the message that started this job + AddPacketToList( pNetPacket, gidJobIDSrc ); + SetFromFromMsg( true ); + // start running this job + InitCoroutine(); + //and start executing the job + Continue(); +} + +//----------------------------------------------------------------------------- +// Purpose: Starts the job +//----------------------------------------------------------------------------- +void CJob::StartJob( void * pvStartParam ) +{ + // job must not be started + AssertMsg1( m_ePauseReason == k_EJobPauseReasonNotStarted, "CJob::StartJob() called twice on job %s\n", GetName() ); + // save the start params for this job + SetStartParam( pvStartParam ); + m_JobMgr.CheckThreadID(); + + // start running this job + InitCoroutine(); + Continue(); +} + +//----------------------------------------------------------------------------- +// Purpose: Starts the job in a suspended state +//----------------------------------------------------------------------------- + +void CJob::StartJobDelayed( void * pvStartParam ) +{ + // job must not be started + AssertMsg1( m_ePauseReason == k_EJobPauseReasonNotStarted, "CJob::StartJob() called twice on job %s\n", GetName() ); + // save the start params for this job + SetStartParam( pvStartParam ); + m_JobMgr.CheckThreadID(); + + //init the job, but don't start it + InitCoroutine(); + + //set our job as suspended (a yield) + m_ePauseReason = k_EJobPauseReasonYield; + m_JobMgr.AddDelayedJobToYieldList( *this ); +} + +//----------------------------------------------------------------------------- +// Purpose: setup the debug memory and job name before running a job the first time +//----------------------------------------------------------------------------- +void CJob::InitCoroutine() +{ + // make sure we have an appropriate chunk of memory to store + // our debug alloc info + if( MemAlloc_GetDebugInfoSize() ) + { + m_memAllocStack.EnsureCapacity( MemAlloc_GetDebugInfoSize() ); + + // Set the job name as the root + MemAlloc_InitDebugInfo( m_memAllocStack.Base(), GetName(), 0 ); + } + + // Set the job name + if ( !m_pJobType && !m_pchJobName ) + { +#ifdef _WIN32 + m_pchJobName = typeid( *this ).raw_name(); + if ( m_pchJobName[0] == '.' && m_pchJobName[1] == '?' && m_pchJobName[2] == 'A') + m_pchJobName += 4; + if ( m_pchJobName[0] == '?' && m_pchJobName[1] == '$' ) + m_pchJobName += 2; +#else + m_pchJobName = typeid( *this ).name(); +#endif + } +} + + +//----------------------------------------------------------------------------- +// Purpose: proxy function for starting the job in the coroutine +//----------------------------------------------------------------------------- +void CJob::BRunProxy( void *pvThis ) +{ + CJob *pJob = (CJob *)pvThis; + + // run the job + bool bJobReturn = false; + if ( pJob->m_bRunFromMsg ) + { + Assert( pJob->m_vecNetPackets.Count() > 0 ); + bJobReturn = pJob->BYieldingRunJobFromMsg( pJob->m_vecNetPackets.Head() ); + } + else + { + bJobReturn = pJob->BYieldingRunJob( pJob->m_pvStartParam ); + } + + pJob->m_flags.m_bits.m_bJobFailed = ( true != bJobReturn ); + + // kill it + DeleteJob( pJob ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds this packet to the linked list of packets for this job +//----------------------------------------------------------------------------- +void CJob::AddPacketToList( IMsgNetPacket *pNetPacket, const GID_t gidJobIDSrc ) +{ + Assert( pNetPacket ); + pNetPacket->AddRef(); + + m_vecNetPackets.AddToTail( pNetPacket ); +} + + +//----------------------------------------------------------------------------- +// Purpose: marks a net packet as being finished with, releases the packet and frees the memory +//----------------------------------------------------------------------------- +void CJob::ReleaseNetPacket( IMsgNetPacket *pNetPacket ) +{ + int iVec = m_vecNetPackets.Find( pNetPacket ); + if ( iVec != m_vecNetPackets.InvalidIndex() ) + { + pNetPacket->Release(); + m_vecNetPackets.Remove( iVec ); + } + else + { + AssertMsg( false, "Job failed trying to release a IMsgNetPacket it doesn't own" ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: continues the current job +//----------------------------------------------------------------------------- +void CJob::Continue() +{ + AssertNotRunningThisJob(); + + m_pJobPrev = g_pJobCur; + g_pJobCur = this; + + // Record frame we're starting in and start a timer to track how long we work for within the frame + // Also, add in how much time has passed since we were last paused to track heartbeat requirements + m_FastTimerDelta.Start(); + + m_STimeSwitched.SetToJobTime(); + + // Check if we need to heartbeat + if ( BJobNeedsToHeartbeat() ) + { + Heartbeat(); + } + + m_JobMgr.GetJobStats().m_cTimeslices++; + + m_ePauseReason = k_EJobPauseReasonNone; +#if defined(_WIN32) && defined(COROUTINE_TRACE) + const char *pchRawName = typeid( *this ).raw_name(); + if ( pchRawName[0] == '.' && pchRawName[1] == '?' && pchRawName[2] == 'A') + pchRawName += 4; + if ( pchRawName[0] == '?' && pchRawName[1] == '$' ) + pchRawName += 2; +#else + const char *pchRawName = ""; +#endif + + // Save debug credit "call stack" + void *pvSaveDebugInfo = GetJobMgr().GetMainMemoryDebugInfo(); + MemAlloc_SaveDebugInfo( pvSaveDebugInfo ); + MemAlloc_RestoreDebugInfo( m_memAllocStack.Base() ); + + // continue the coroutine, with the profiling if necessary + bool bJobStillActive; +#if defined( VPROF_ENABLED ) + if ( g_VProfCurrentProfile.IsEnabled() ) + { + VPROF_BUDGET( GetName(), VPROF_BUDGETGROUP_JOBS_COROUTINES ); + bJobStillActive = Coroutine_Continue( m_hCoroutine, pchRawName ); + } + else +#endif + { + bJobStillActive = Coroutine_Continue( m_hCoroutine, pchRawName ); + } + + // WARNING: MEMBER VARIABLES ARE NOW UNSAFE TO ACCESS - this CJob may be deleted + + // Restore debug credit call stack + if( bJobStillActive ) + { + // only save off debug info for jobs that are still running + MemAlloc_SaveDebugInfo( m_memAllocStack.Base() ); + } + MemAlloc_RestoreDebugInfo( pvSaveDebugInfo ); + +} + +void CJob::Debug() +{ + AssertNotRunningThisJob(); + + // This function will 'load' this coroutine then immediately + // break into the debugger. When execution is continued, it + // will pop back out to this context + + // So, we don't set m_pJobPrev or g_pJobCur because nobody + // would ever have the chance to see them anyway. + Coroutine_DebugBreak( m_hCoroutine ); +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the current job +//----------------------------------------------------------------------------- +void CJob::Pause( EJobPauseReason eReason ) +{ + AssertRunningThisJob(); + AssertMsg1( 0 == m_stackDoNotYieldGuards.Count(), "Yielding while in a BEGIN_DO_NOT_YIELD() block declared at %s", m_stackDoNotYieldGuards[m_stackDoNotYieldGuards.Head()] ); + + g_pJobCur = m_pJobPrev; + + // End our timer so we know how much time we've spent + m_FastTimerDelta.End(); + m_cyclecountTotal += m_FastTimerDelta.GetDuration(); + + if ( m_FastTimerDelta.GetDuration().GetMicroseconds() > k_cMicroSecTaskGranularity * 10 ) + { + m_flags.m_bits.m_bLongInterYield = true; + } + // pause this job, remembering which frame and why + m_ePauseReason = eReason; + // We shouldn't have to set the frame -- it should be the same one + Assert( m_STimeSwitched.LTime() == CJobTime::LJobTimeCur() ); + Coroutine_YieldToMain(); +} + +void CJob::GenerateAssert( const char *pchMsg ) +{ + // Default message if they didn't provide a custom one + if ( !pchMsg ) + { + pchMsg = "Forced assert failure"; + } + + // Just for grins, allow this function to be called whether + // we are the current job or not + if ( this == g_pJobCur ) + { + AssertMsg1( !"Job assertion requested", "%s", pchMsg ); + } + else + { + Coroutine_DebugAssert( m_hCoroutine, pchMsg ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a network msg for the job arrives +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForMsg( IMsgNetPacket **ppNetPacket ) +{ + AssertRunningThisJob(); + *ppNetPacket = NULL; + + // await and retrieve the network message + if ( GetJobMgr().BYieldingWaitForMsg( *this ) ) + { + Assert( m_vecNetPackets.Count() > 0 ); + *ppNetPacket = m_vecNetPackets.Tail(); + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a network msg for the job arrives +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForMsg( CGCMsgBase *pMsg, MsgType_t eMsg ) +{ + IMsgNetPacket *pNetPacket = NULL; + + // Check if we already waited for a message of this type + // but timed out. If so, then we currently don't have a way + // to tell if the message we might receive is the reply + // to the old mesage, of the one we're about to send. + // So let's just disallow this entirely. + if ( BHasFailedToReceivedMsgType( eMsg ) ) + { + AssertMsg2( false, "Job %s cannot wait for msg %u, it has already failed to wait for that msg type.", GetName(), eMsg ); + return false; + } + + m_unWaitMsgType = eMsg; + if ( !BYieldingWaitForMsg( &pNetPacket) ) + { + // Remember this event, so we can at least detect if a reply comes late, we don't get confused. + MarkFailedToReceivedMsgType( eMsg ); + return false; + } + + pMsg->SetPacket( pNetPacket ); + + if ( pMsg->Hdr().m_eMsg != eMsg ) + { + // Remember this event, so we can at least detect if a reply comes late, we don't get confused. + MarkFailedToReceivedMsgType( eMsg ); + + AssertMsg2( false, "CJob::BYieldingWaitForMsg expected msg %u but received %u", eMsg, pMsg->Hdr().m_eMsg ); + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +bool CJob::BHasFailedToReceivedMsgType( MsgType_t m ) const +{ + FOR_EACH_VEC( m_vecMsgTypesFailedToReceive, i ) + { + if ( m_vecMsgTypesFailedToReceive[i] == m ) + return true; + } + return false; +} + +//----------------------------------------------------------------------------- +void CJob::MarkFailedToReceivedMsgType( MsgType_t m ) +{ + if ( !BHasFailedToReceivedMsgType( m ) ) + { + m_vecMsgTypesFailedToReceive.AddToTail( m ); + } +} + +//----------------------------------------------------------------------------- +void CJob::ClearFailedToReceivedMsgType( MsgType_t m ) +{ + m_vecMsgTypesFailedToReceive.FindAndFastRemove( m ); +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a network msg for the job arrives +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForMsg( CProtoBufMsgBase *pMsg, MsgType_t eMsg ) +{ + IMsgNetPacket *pNetPacket = NULL; + + // Check if we already waited for a message of this type + // but timed out. If so, then we currently don't have a way + // to tell if the message we might receive is the reply + // to the old mesage, of the one we're about to send. + // So let's just disallow this entirely. + if ( BHasFailedToReceivedMsgType( eMsg ) ) + { + AssertMsg2( false, "Job %s cannot wait for msg %u, it has already failed to wait for that msg type.", GetName(), eMsg ); + return false; + } + + m_unWaitMsgType = eMsg; + if ( !BYieldingWaitForMsg( &pNetPacket) ) + { + // Remember this event, so we can at least detect if a reply comes late, we don't get confused. + MarkFailedToReceivedMsgType( eMsg ); + return false; + } + + pMsg->InitFromPacket( pNetPacket ); + + if ( pMsg->GetEMsg() != eMsg ) + { + // Remember this event, so we can at least detect if a reply comes late, we don't get confused. + MarkFailedToReceivedMsgType( eMsg ); + + EmitError( SPEW_GC, "CJob::BYieldingWaitForMsg expected msg %u but received %u", eMsg, pMsg->GetEMsg() ); + return false; + } + + return true; +} + +#ifdef GC + +bool CJob::BYieldingWaitForMsg( CGCMsgBase *pMsg, MsgType_t eMsg, const CSteamID &expectedID ) +{ + if( !BYieldingWaitForMsg( pMsg, eMsg ) ) + return false; + + if( pMsg->Hdr().m_ulSteamID != expectedID.ConvertToUint64() ) + { + EmitError( SPEW_GC, "CJob::BYieldingWaitForMsg expected reply from steam ID %s, but instead got a response from %s for message %d\n", expectedID.Render(), CSteamID( pMsg->Hdr().m_ulSteamID ).Render(), eMsg ); + return false; + } + + return true; +} + + +bool CJob::BYieldingWaitForMsg( CProtoBufMsgBase *pMsg, MsgType_t eMsg, const CSteamID &expectedID ) +{ + if( !BYieldingWaitForMsg( pMsg, eMsg ) ) + return false; + + if( pMsg->GetClientSteamID() != expectedID ) + { + EmitError( SPEW_GC, "CJob::BYieldingWaitForMsg expected reply from steam ID %s, but instead got a response from %s for message %d\n", expectedID.Render(), pMsg->GetClientSteamID().Render(), eMsg ); + return false; + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a network msg for the job arrives +//----------------------------------------------------------------------------- +bool CJob::BYieldingRunQuery( CGCSQLQueryGroup *pQueryGroup, ESchemaCatalog eSchemaCatalog ) +{ + AssertRunningThisJob(); + + // await and retrieve the network message + return GetJobMgr().BYieldingRunQuery( *this, pQueryGroup, eSchemaCatalog ); +} +#endif + + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a work item callback occurs +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForWorkItem( const char *pszWorkItemName ) +{ + AssertRunningThisJob(); + + // await the work item completion + return GetJobMgr().BYieldingWaitForWorkItem( *this, pszWorkItemName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: CWorkItem for processing functions in CJob-derived classes on another thread +//----------------------------------------------------------------------------- +class CJobThreadFuncWorkItem : public CWorkItem +{ +public: + DECLARE_WORK_ITEM( CJobThreadFuncWorkItem ); + CJobThreadFuncWorkItem( CJob *pJob, JobThreadFunc_t jobThreadFunc, CFunctor *pFunctor ) : CWorkItem( pJob->GetJobID() ), + m_pJob( pJob ), + m_pJobThreadFunc( jobThreadFunc ), + m_pFunctor( pFunctor ) + { + } + + virtual bool ThreadProcess( CWorkThread *pThread ) + { + if ( m_pJobThreadFunc ) + (m_pJob->*m_pJobThreadFunc)(); + if ( m_pFunctor ) + (*m_pFunctor)(); + return true; + } + +private: + CJob *m_pJob; + JobThreadFunc_t m_pJobThreadFunc; + CFunctor *m_pFunctor; +}; + + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a work item callback occurs +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForThreadFuncWorkItem( CWorkItem *pItem ) +{ + AssertRunningThisJob(); + Assert( m_pWaitingOnWorkItem == NULL ); + Assert( pItem->GetJobID() == GetJobID() ); + + m_pWaitingOnWorkItem = pItem; + + // add it to a central thread pool + GetJobMgr().AddThreadedJobWorkItem( pItem ); + + // await the work item completion + bool bSuccess = GetJobMgr().BYieldingWaitForWorkItem( *this ); + + m_pWaitingOnWorkItem = NULL; + + return bSuccess; +} + + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until a work item callback occurs +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitForThreadFunc( CFunctor *jobFunctor ) +{ + // store off which function to launch when we're done + CJobThreadFuncWorkItem *pJobThreadFuncWorkItem = new CJobThreadFuncWorkItem( this, NULL, jobFunctor ); + + bool bSuccess = BYieldingWaitForThreadFuncWorkItem( pJobThreadFuncWorkItem ); + + // free the thread func + SafeRelease( jobFunctor ); + SAFE_RELEASE( pJobThreadFuncWorkItem ); + + return bSuccess; +} + + +//----------------------------------------------------------------------------- +// Purpose: Allows a job that was paused for a specific reason to resume +//----------------------------------------------------------------------------- +void CJob::EndPause( EJobPauseReason eExpectedState ) +{ + Assert( m_ePauseReason == eExpectedState ); + if( m_ePauseReason == eExpectedState ) + { + m_ePauseReason = k_EJobPauseReasonYield; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the number of heartbeats to wait before timing out this job +//----------------------------------------------------------------------------- +uint32 CJob::CHeartbeatsBeforeTimeout() +{ + return k_cJobHeartbeatsBeforeTimeoutDefault; +} + + +//----------------------------------------------------------------------------- +// Purpose: Send heartbeat messages to our listeners during long operations +// to let them know we're still alive and avoid timeouts +// This should be called by the CJobMgr +//----------------------------------------------------------------------------- +void CJob::Heartbeat() +{ + // Reset our counter + m_STimeNextHeartbeat.SetFromJobTime( k_cMicroSecJobHeartbeat ); +} + + + + +//----------------------------------------------------------------------------- +// Purpose: waits for specified time and checks for timeout. Useful when you +// need to repeatedly sleep while waiting for something to happen. +// This function uses STime (server "pseudo" time) to determine +// timeout conditions. +// +// Input: cMicrosecondsToSleep - duration to sleep this call +// stimeStarted - the time to calculate timeout from. (Typically, +// the time you start calling this in a loop, passing the same +// start time each time you call this method.) +// nMicroSecLimit - duration from stimeStarted to consider timed out +// Output : Returns true if not timed out yet, false if timed out +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitTimeWithLimit( uint32 cMicrosecondsToSleep, CJobTime &stimeStarted, int64 nMicroSecLimit ) +{ + if ( stimeStarted.CServerMicroSecsPassed() > nMicroSecLimit ) + return false; + + return BYieldingWaitTime( cMicrosecondsToSleep ); +} + + +//----------------------------------------------------------------------------- +// Purpose: waits for specified time and checks for timeout. Useful when you +// need to repeatedly sleep while waiting for something to happen. +// This function uses RTime (wall-clock "real" time) to determine +// timeout conditions. +// +// Input: cMicrosecondsToSleep - duration to sleep this call +// nSecLimit - duration from stimeStarted to consider timed out +// Output : Returns true if not timed out yet, false if timed out +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitTimeWithLimitRealTime( uint32 cMicrosecondsToSleep, int nSecondsLimit ) +{ + return BYieldingWaitTime( cMicrosecondsToSleep ); +} + + +//----------------------------------------------------------------------------- +// Purpose: pauses the job for the specified amount of time +// Input : m_cMicrosecondsToSleep - microseconds to wait for +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitTime( uint32 cMicrosecondsToSleep ) +{ + AssertRunningThisJob(); + return GetJobMgr().BYieldingWaitTime( *this, cMicrosecondsToSleep ); +} + +//----------------------------------------------------------------------------- +// Purpose: pauses the job until the next time the JobMgr Run() is called +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYield() +{ + AssertRunningThisJob(); + return GetJobMgr().BYield( *this ); +} + +//----------------------------------------------------------------------------- +// Purpose: Pauses the job ONLY IF JobMgr decides it needs to based on time run and priority +// If pausing, pauses until the next time the JobMgr Run() is called +// Input: pbYielded - Set to true if we did yield +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldIfNeeded( bool *pbYielded ) +{ + AssertRunningThisJob(); + + if ( pbYielded ) + *pbYielded = false; + + // Assume only low priority jobs need to yield + // Automatically bail out here if the job is not low priority + + return GetJobMgr().BYieldIfNeeded( *this, pbYielded ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Pauses the job for a single frame +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::BYieldingWaitOneFrame() +{ + return BYieldingWaitTime( 1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Blocks until we acquire the lock on the specified object +// Input : *pLock - object to lock +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::_BYieldingAcquireLock( CLock *pLock, const char *filename, int line ) +{ + AssertRunningThisJob(); + + // Skip the path info from the filename. It just maks the debug messages excessively long. + filename = V_GetFileName( filename ); + + // Is the lock locked by this job? If so, inc the ref count. + if ( pLock->GetJobLocking() == this ) + { + pLock->IncrementReference(); + return true; + } + + // jobs can have multiple locks as long as they are in priority order + FOR_EACH_VEC( m_vecLocks, i ) + { + if( m_vecLocks[i]->GetLockType() == pLock->GetLockType() ) + { + if( m_vecLocks[i]->GetLockSubType() <= pLock->GetLockSubType() ) + { + AssertMsg7( false, "Job %s Locking %s at %s:(%d) with yielding; holds lock %s from %s(%d)\n", + GetName(), + pLock->GetName(), + filename, line, + m_vecLocks[i]->GetName(), + m_vecLocks[i]->m_pFilename, m_vecLocks[i]->m_line ); + return false; + } + } + else if ( m_vecLocks[i]->GetLockType() < pLock->GetLockType() ) + { + AssertMsg7( false, "Job %s Locking %s at %s:(%d) with yielding; holds lock %s from %s(%d)\n", + GetName(), + pLock->GetName(), + filename, line, + m_vecLocks[i]->GetName(), + m_vecLocks[i]->m_pFilename, m_vecLocks[i]->m_line ); + return false; + } + } + + if( m_pWaitingOnLock != NULL ) + { + AssertMsg7( false, "Job (%s) locking %s at %s(%d); already waiting on %s at %s(%d).\n", + GetName(), + pLock->GetName(), + filename, line, + m_pWaitingOnLock->GetName(), + m_pWaitingOnLock->m_pFilename, m_pWaitingOnLock->m_line ); + return false; + } + + m_cLocksAttempted++; + if ( pLock->BIsLocked() ) + { + // tell the job we want the lock next + // But walking the entire linked list is slow so + // skip to the tail pointer + pLock->AddToWaitingQueue( this ); + + // We should be the tail of the list + Assert( NULL == m_pJobToNotifyOnLockRelease ); + + // yield until we get the lock + m_pWaitingOnLock = pLock; + m_pWaitingOnLockFilename = filename; + m_waitingOnLockLine = line; + m_cLocksWaitedFor++; + Pause( k_EJobPauseReasonWaitingForLock ); + m_pWaitingOnLock = NULL; + + // make sure we actually got it, instead of timing out + int index = m_vecLocks.Find( pLock ); + if ( index != m_vecLocks.InvalidIndex() && this == pLock->GetJobLocking() ) + { + pLock->IncrementReference(); + return true; + } + else + { + m_flags.m_bits.m_bLocksFailed = true; + EmitWarning( SPEW_JOB, LOG_ALWAYS, "Failed to get lock %s at %s(%d) after waiting in %s\n", pLock->GetName(), filename, line, GetName() ); + if ( m_vecLocks.Count() == 0 ) + { + EmitWarning( SPEW_JOB, LOG_ALWAYS, "m_vecLocks.Count(): %d, this: 0x%p, pLock->GetJobLocking(): %s (0x%p)\n", + m_vecLocks.Count(), this, pLock->GetJobLocking() ? pLock->GetJobLocking()->GetName() : "(null)", pLock->GetJobLocking() ); + } + else + { + EmitWarning( SPEW_JOB, LOG_ALWAYS, "m_vecLocks.Count(): %d this: 0x%p, pLock: 0x%p pLock->GetJobLocking(): %s (0x%p)\n", + m_vecLocks.Count(), this, pLock, pLock->GetJobLocking() ? pLock->GetJobLocking()->GetName() : "(null)", pLock->GetJobLocking() ); + FOR_EACH_VEC( m_vecLocks, i ) + { + EmitWarning( SPEW_JOB, LOG_ALWAYS, "m_vecLocks[%d]: %s (0x%p) %s(%d)\n", + i, m_vecLocks[i] ? m_vecLocks[i]->GetName() : "(null)", m_vecLocks[i], m_vecLocks[i]->m_pFilename, m_vecLocks[i]->m_line ); + } + } + return false; + } + } + else + { + // unused, take it for ourself + pLock->IncrementReference(); + _SetLock( pLock, filename, line ); + return true; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Either locks on the specified object immediately or returns failure +// Input : *pLock - object to lock +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CJob::_BAcquireLockImmediate( CLock *pLock, const char *filename, int line ) +{ + AssertRunningThisJob(); + + AssertMsg5( m_pWaitingOnLock == NULL, "Job (%s) at %s(%d) trying to take a lock while it was already waiting for the first one at %s(%d)", GetName(), filename, line, m_pWaitingOnLockFilename, m_waitingOnLockLine ); + + m_cLocksAttempted++; + + // Is the lock locked by this job? If so, inc the ref count. + if ( pLock->GetJobLocking() == this ) + { + pLock->IncrementReference(); + return true; + } + + if ( !pLock->BIsLocked() ) + { + // unused, take it for ourself + pLock->IncrementReference(); + _SetLock( pLock, filename, line ); + return true; + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Releases the specified lock, passing it on to the next job if necessary +//----------------------------------------------------------------------------- +void CJob::_ReleaseLock( CLock *pLock, bool bForce, const char *filename, int line ) +{ + Assert( pLock ); + if ( !pLock ) + return; + + Assert( m_vecLocks.HasElement( pLock ) ); + if ( !m_vecLocks.HasElement( pLock ) ) + { + EmitError( SPEW_JOB, "Job %s trying to release lock %s at %s(%d) it's not holding\n", GetName(), pLock->GetName(), filename, line ); + return; + } + + if ( pLock->GetJobLocking() != this ) + { + EmitError( SPEW_JOB, "Job %s trying to release lock %s at %s(%d) though the lock is held by %s\n", GetName(), pLock->GetName(), filename, line, pLock->GetJobLocking()->GetName() ); + return; + } + + if ( bForce ) + { + // Force clear reference count + pLock->ClearReference(); + } + else + { + // Dec the reference count. If it is not yet zero, don't fully unlock + if ( pLock->DecrementReference() > 0 ) + { + return; + } + } + + if ( pLock->m_pJobToNotifyOnLockRelease ) + { + // post a message to the main system to wakeup the next lock + PassLockToJob( pLock->m_pJobToNotifyOnLockRelease, pLock ); + m_pJobToNotifyOnLockRelease = NULL; + + Assert( this != pLock->m_pJobWaitingQueueTail ); + } + else + { + // just release + UnsetLock( pLock ); + Assert( NULL == pLock->m_pJobWaitingQueueTail || this == pLock->m_pJobWaitingQueueTail ); + pLock->m_pJobWaitingQueueTail = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Release all locks this job holds. This is only to be used by long lived +// jobs that don't destruct. +//----------------------------------------------------------------------------- +void CJob::ReleaseLocks() +{ + // release any locks - do this in reverse order because they're being removed from the vector in the loop + FOR_EACH_VEC_BACK( m_vecLocks, nLock ) + { + _ReleaseLock( m_vecLocks[nLock], true, __FILE__, __LINE__ ); + } + m_vecLocks.RemoveAll(); +} + +//----------------------------------------------------------------------------- +// Purpose: Assert that we don't hold any locks, and if we hold them, release them +//----------------------------------------------------------------------------- +void CJob::ShouldNotHoldAnyLocks() +{ + if ( m_vecLocks.Count() == 0 ) + return; + + CUtlString sErrMsg; + sErrMsg.Format( "Job %s detected and cleaned up leak of %d lock(s):\n", GetName(), m_vecLocks.Count() ); + FOR_EACH_VEC_BACK( m_vecLocks, nLock ) + { + CLock *pLock = m_vecLocks[nLock]; + sErrMsg.Append( CFmtStr( " Lock %s, acquired %s(%d)\n", pLock->GetName(), pLock->m_pFilename, pLock->m_line ).Access() ); + } + + AssertMsg1( false, "%s", sErrMsg.String() ); + + // Now release them + ReleaseLocks(); +} + + +//----------------------------------------------------------------------------- +// Purpose: sets up the job to notify when we've release our locks +//----------------------------------------------------------------------------- +void CJob::AddJobToNotifyOnLockRelease( CJob *pJob ) +{ + // if we already are going to be notifying someone, then have them notify the new requester + if ( m_pJobToNotifyOnLockRelease ) + { + AssertMsg( false, "AddJobToNotifyOnLockRelease attempting to walk the linked list. We've optimized this out." ); + m_pJobToNotifyOnLockRelease->AddJobToNotifyOnLockRelease( pJob ); + } + else + { + m_pJobToNotifyOnLockRelease = pJob; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets the lock +//----------------------------------------------------------------------------- +void CJob::_SetLock( CLock *pLock, const char *filename, int line ) +{ + Assert( !m_vecLocks.HasElement( pLock ) ); + Assert( !pLock->BIsLocked() ); + + pLock->m_pJob = this; + pLock->m_sTimeAcquired.SetToJobTime(); + pLock->m_pFilename = filename; + pLock->m_line = line; + m_vecLocks.AddToTail( pLock ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Removes the lock +//----------------------------------------------------------------------------- +void CJob::UnsetLock( CLock *pLock ) +{ + Assert( pLock->GetJobLocking() == this ); + + pLock->m_pJob = NULL; + // if we've held the lock for more than a few seconds, make noise. + if ( /*!BIsTest() &&*/ pLock->m_sTimeAcquired.CServerMicroSecsPassed() >= 10 * k_nMillion ) + { + m_flags.m_bits.m_bLocksLongHeld = true; + if ( pLock->m_pJobToNotifyOnLockRelease ) + { + pLock->m_pJobToNotifyOnLockRelease->m_flags.m_bits.m_bLocksLongWait = true; + EmitWarning( SPEW_JOB, 4, "Job of type %s held lock for %.2f seconds while job of type %s was waiting\n", GetName(), (double) pLock->m_sTimeAcquired.CServerMicroSecsPassed() / k_nMillion, pLock->m_pJobToNotifyOnLockRelease->GetName() ); + } + else + EmitWarning( SPEW_JOB, 4, "Job of type %s held lock for %.2f seconds\n", GetName(), (double) pLock->m_sTimeAcquired.CServerMicroSecsPassed() / k_nMillion ); + } + m_vecLocks.FindAndRemove( pLock ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Releases the lock from the old job, and immediately passes it on to the waiting job +//----------------------------------------------------------------------------- +void CJob::PassLockToJob( CJob *pNewJob, CLock *pLock ) +{ + Assert( pNewJob->GetPauseReason() == k_EJobPauseReasonWaitingForLock ); + Assert( pNewJob->m_pWaitingOnLock == pLock ); + + pLock->m_pJobToNotifyOnLockRelease = pNewJob->m_pJobToNotifyOnLockRelease; + if ( NULL == pLock->m_pJobToNotifyOnLockRelease ) + { + pLock->m_pJobWaitingQueueTail = NULL; + } + + pNewJob->m_pJobToNotifyOnLockRelease = NULL; + Assert( pLock->m_nWaitingCount > 0 ); + pLock->m_nWaitingCount--; + + // release the lock + UnsetLock( pLock ); + + // If the other job isn't waiting on a lock, then we certainly don't + // want to call SetLock() on it + if ( pNewJob->GetPauseReason() == k_EJobPauseReasonWaitingForLock && pNewJob->m_pWaitingOnLock == pLock ) + { + // give the job the lock + pNewJob->_SetLock( pLock, pNewJob->m_pWaitingOnLockFilename, m_waitingOnLockLine ); + + // set the job with the newly acquired lock to wakeup + pNewJob->GetJobMgr().WakeupLockedJob( *pNewJob ); + } + else + { + EmitError( SPEW_JOB, "Job passed lock it wasn't waiting for. Job: %s, Lock: %s %s(%d), Paused for %s, Waiting on %s\n", + pNewJob->GetName(), pLock->GetName(), pLock->m_pFilename, pLock->m_line, pNewJob->GetPauseReasonDescription(), m_pWaitingOnLock ? m_pWaitingOnLock->GetName() : "none" ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: a lock is letting us know it's been deleted +// fail all jobs trying to get the lock +//----------------------------------------------------------------------------- +void CJob::OnLockDeleted( CLock *pLock ) +{ + //EmitWarning( SPEW_JOB, SPEW_ALWAYS, "Deleting lock %s\n", GetName() ); + + Assert( pLock->BIsLocked() ); + Assert( pLock->m_pJob == this ); + + // fail all the jobs waiting on the lock + CJob *pJob = pLock->m_pJobToNotifyOnLockRelease; + while ( pJob ) + { + // insert the job into the sleep list with 0 time, so it wakes up immediately + // it will see it doesn't have the desired lock and suicide + pJob->GetJobMgr().WakeupLockedJob( *pJob ); + + // move to the next job + CJob *pJobT = pJob; + pJob = pJob->m_pJobToNotifyOnLockRelease; + pJobT->m_pJobToNotifyOnLockRelease = NULL; + } + + m_pJobToNotifyOnLockRelease = NULL; + pLock->m_pJobToNotifyOnLockRelease = NULL; + pLock->m_pJobWaitingQueueTail = NULL; + + // remove the lock + UnsetLock( pLock ); +} + +//----------------------------------------------------------------------------- +// Purpose: Reports how many Do Not Yield guards the job currently has +//----------------------------------------------------------------------------- +int32 CJob::GetDoNotYieldDepth() const +{ + return m_stackDoNotYieldGuards.Count(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a Do Not Yield guard to the job +//----------------------------------------------------------------------------- +void CJob::PushDoNotYield( const char *pchFileAndLine ) +{ + m_stackDoNotYieldGuards.AddToHead( pchFileAndLine ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Removes the last-added Do Not Yield guard from the job +//----------------------------------------------------------------------------- +void CJob::PopDoNotYield() +{ + AssertMsg( m_stackDoNotYieldGuards.Count() > 0, "Could not pop a Do Not Yield guard when the job's stack is empty" ); + if ( m_stackDoNotYieldGuards.Count() > 0 ) + { + m_stackDoNotYieldGuards.Remove( m_stackDoNotYieldGuards.Head() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Implementation of the stack-scope Do Not Yield guard +//----------------------------------------------------------------------------- +CDoNotYieldScope::CDoNotYieldScope( const char *pchFileAndLine ) +{ + AssertRunningJob(); + + GJobCur().PushDoNotYield( pchFileAndLine ); +} + +CDoNotYieldScope::~CDoNotYieldScope() +{ + AssertRunningJob(); + + GJobCur().PopDoNotYield(); +} + + +#ifdef DBGFLAG_VALIDATE +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void CJob::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); + + ValidateObj( m_stackDoNotYieldGuards ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our static data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void CJob::ValidateStatics( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE_STATIC( "CJob class statics" ); +} +#endif // DBGFLAG_VALIDATE + + +CLock::CLock( ) +: m_pJob( NULL ), +m_pJobToNotifyOnLockRelease( NULL ), +m_pJobWaitingQueueTail( NULL ), +m_nWaitingCount(0), +m_nsLockType(0), +m_nsNameType( k_ENameTypeNone ), +m_ulID( 0 ), +m_pchConstStr( NULL ), +m_unLockSubType ( 0 ), +m_nRefCount( 0 ), +m_pFilename( "unknown" ), +m_line( 0 ) +{ +} + + +CLock::~CLock() +{ + if ( m_pJob ) + { + m_pJob->OnLockDeleted( this ); + } +} + + +void CLock::AddToWaitingQueue( CJob *pJob ) +{ + if ( m_pJobWaitingQueueTail ) + { + Assert( NULL == m_pJobWaitingQueueTail->m_pJobToNotifyOnLockRelease ); + m_pJobWaitingQueueTail->AddJobToNotifyOnLockRelease( pJob ); + } + else + { + Assert( NULL == m_pJobToNotifyOnLockRelease ); + m_pJobToNotifyOnLockRelease = pJob; + } + + m_pJobWaitingQueueTail = pJob; + m_nWaitingCount++; +} + + +void CLock::SetName( const char *pchName ) +{ + m_nsNameType = k_ENameTypeConstStr; + m_pchConstStr = pchName; +} + + +void CLock::SetName( const char *pchPrefix, uint64 ulID ) +{ + m_nsNameType = k_ENameTypeConcat; + m_pchConstStr = pchPrefix; + m_ulID = ulID; +} + + +void CLock::SetName( const CSteamID &steamID ) +{ + m_nsNameType = k_ENameTypeSteamID; + m_ulID = steamID.ConvertToUint64(); +} + + +const char *CLock::GetName() const +{ + switch ( m_nsNameType ) + { + case k_ENameTypeNone: + return "None"; + case k_ENameTypeSteamID: + return CSteamID::Render( m_ulID ); + case k_ENameTypeConstStr: + return m_pchConstStr; + case k_ENameTypeConcat: + if ( !m_strName.Length() ) + m_strName.Format( "%s %llu", m_pchConstStr, m_ulID ); + return m_strName.Get(); + default: + AssertMsg1( false, "Invalid lock name type %d", m_nsNameType ); + return "(Unknown)"; + } +} + +#define REF_COUNT_ASSERT 1000 + +void CLock::IncrementReference() +{ + m_nRefCount++; + Assert( m_nRefCount != REF_COUNT_ASSERT ); +} + +int CLock::DecrementReference() +{ + Assert( m_nRefCount > 0 ); + if ( m_nRefCount > 0 ) + { + m_nRefCount--; + } + return m_nRefCount; +} + +void CLock::Dump( const char *pszPrefix, int nPrintMax, bool bPrintWaiting ) const +{ + if ( m_pJob != NULL ) + { + EmitInfo( SPEW_JOB, SPEW_ALWAYS, LOG_ALWAYS, "%s%s: Lock owner: %s, Type: %d, %d Waiting\n", pszPrefix, GetName(), CFmtStr( "%s (%llu), Reason: %s", m_pJob->GetName(), m_pJob->GetJobID(), m_pJob->GetPauseReasonDescription() ).Access(), (int32)m_nsLockType, m_nWaitingCount ); + EmitInfo( SPEW_JOB, SPEW_ALWAYS, LOG_ALWAYS, "%sLock acquired: %s:%d\n", pszPrefix, m_pFilename, m_line ); + } + else + { + EmitInfo( SPEW_JOB, SPEW_ALWAYS, LOG_ALWAYS, "%s%s: Lock owner: None, Type: %d, %d Waiting\n", pszPrefix, GetName(), (int32)m_nsLockType, m_nWaitingCount ); + } + + CJob *pCurrWaiting = m_pJobToNotifyOnLockRelease; + int nPrinted = 0; + int nTotal = 0; + while( pCurrWaiting != NULL && nPrinted < nPrintMax ) + { + bool bPrint = false; + if ( nPrinted < nPrintMax ) + { + bPrint = true; + } + if ( pCurrWaiting->GetPauseReason() == k_EJobPauseReasonWaitingForLock && pCurrWaiting->m_pWaitingOnLock == this ) + { + bPrint = true; + } + + if ( bPrint && bPrintWaiting ) + { + EmitInfo( SPEW_JOB, SPEW_ALWAYS, LOG_ALWAYS, "%s\tOther jobs waiting for this lock: %s (%llu)\n", pszPrefix, pCurrWaiting->GetName(), pCurrWaiting->GetJobID() ); + if ( pCurrWaiting->GetPauseReason() == k_EJobPauseReasonWaitingForLock && pCurrWaiting->m_pWaitingOnLock == this ) + { + EmitInfo( SPEW_CONSOLE, SPEW_ALWAYS, 3, "%s\tAt: %s:%d\n", pszPrefix, pCurrWaiting->m_pWaitingOnLockFilename, pCurrWaiting->m_waitingOnLockLine ); + } + nPrinted++; + } + pCurrWaiting = pCurrWaiting->m_pJobToNotifyOnLockRelease; + nTotal++; + } + if ( bPrintWaiting || nTotal != 0 ) + { + EmitInfo( SPEW_JOB, SPEW_ALWAYS, LOG_ALWAYS, "%s%d out of %d waiting jobs printed.\n", pszPrefix, nPrinted, nTotal ); + } +} + +} // namespace GCSDK |