/*========================================================================== * * Copyright (C) 1999 Microsoft Corporation. All Rights Reserved. * * File: Timer.cpp * Content: This file contains code to for the Protocol timers * * History: * Date By Reason * ==== == ====== * 06/04/98 aarono Created * 07/01/00 masonb Assumed Ownership * ****************************************************************************/ #include "dnproti.h" /* ** Quick Insert Optimizers ** ** In a very high user system there are going to be many timers being set and cancelled. Timer ** cancels and timer fires are already optimized, but as the timer list grows the SetTimer operations ** become higher and higher overhead as we walk through a longer and longer chain for our insertion-sort. ** ** Front First for Short Timers ** ** When very short timers are being set we can assume that they will insert towards the front of the ** timer list. So it would be smarter to walk the list front-to-back instead of the back-to-front default ** behavior which correctly assumes that most new timers will be firing after timers already set. If the ** the Timeout value of a new timer is near the current timer resolution then we will try the front-first ** insertion-sort instead. This will hopefully reduce short timer sets to fairly quick operations ** ** Standard Long Timers ** ** Standard means that they will all have the same duration. If we keep a seperate chain ** for all these timers with a constant duration they can be trivally inserted at the end of the chain. This ** will be used for the periodic background checks run on each endpoint every couple of seconds. ** ** Quick Set Timer Array ** ** The really big optimization is an array of timeout lists, with a current pointer. Periodic timeout ** will walk the array a number of slots corresponding to the interval since it was last run. All events ** on those lists will be scheduled. This turns all SetTimer ops into constant time operations ** no matter how many timers are running in the system. This can be used for all timers within the ** range of the array (resolution X number of slots) which may be 4ms * 256 slots or a 1K ms range. We expect ** most link timers to fall into this range, although it can be doubled or quadrupled quite trivially. ** ** I plan to run QST algorithm on any server platform, which will replace Front First Short Timers for ** obvious reasons. Client or Peer servers will use FFS instead. Both configs will benefit from StdLTs ** unless the range of the QST array grows to encompass the standard length timeout. */ #define DEFAULT_TIME_RESOLUTION 4 /* ms */ #define MAX_TIMER_THREADS_PER_PROCESSOR 8 DWORD WINAPI TimerWorkerThread(LPVOID); CBilink g_blMyTimerList; // Random Timer List CBilink g_blStdTimerList; // Standard Length Timer List DNCRITICAL_SECTION g_csMyTimerListLock; // One lock will guard both lists LPFPOOL g_pTimerPool = NULL; DWORD g_dwWorkaroundTimerID; DWORD g_dwUnique = 0; UINT g_uiTimeSetEventFlags = TIME_PERIODIC; DNCRITICAL_SECTION g_csThreadListLock; // locks ALL this stuff. CBilink g_blThreadList; // ThreadPool grabs work from here. DWORD g_nThreads = 0; // number of running threads. DWORD g_dwActiveRequests = 0; // number of requests being processed. DWORD g_fShutDown = TRUE; DWORD g_dwExtraSignals = 0; HANDLE g_hWorkToDoSem = 0; SYSTEM_INFO g_SystemInfo; DWORD g_dwMaxTimerThreads = MAX_TIMER_THREADS_PER_PROCESSOR; HANDLE *g_phTimerThreadHandles = NULL; /*** * * QUICK-START TIMER SUPPORT * ***/ #define QST_SLOTCOUNT 2048 // 2048 seperate timer queues #define QST_GRANULARITY 4 // 4 ms clock granularity * 2048 slots == 8192 ms max timeout value #define QST_MAX_TIMEOUT (QST_SLOTCOUNT * QST_GRANULARITY) #define QST_MOD_MASK (QST_SLOTCOUNT - 1) // Calculate a quick modulo operation for wrapping around the array #if ( (QST_GRANULARITY - 1) & QST_GRANULARITY ) This Will Not Compile -- ASSERT that QST_GRANULARITY is power of 2! #endif #if ( (QST_SLOTCOUNT - 1) & QST_SLOTCOUNT ) This Will Not Compile -- ASSERT that QST_SLOTCOUNT is power of 2! #endif CBilink g_rgblQSTimerArray[QST_SLOTCOUNT]; UINT g_uiQSTCurrentIndex; // Last array slot that was executed DWORD g_dwQSTLastRunTime; // Tick count when QSTs last ran /* * END OF QST SUPPORT */ #undef Lock #undef Unlock #define Lock DNEnterCriticalSection #define Unlock DNLeaveCriticalSection /* ** Periodic Timer ** ** This runs every RESOLUTION millisecs and checks for expired timers. It must check two lists ** for expired timers, plus a variable number of slots in the QST array. */ #undef DPF_MODNAME #define DPF_MODNAME "PeriodicTimer" void CALLBACK PeriodicTimer (UINT uID, UINT uMsg, DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2) { DWORD time; PMYTIMER pTimerWalker; CBilink *pBilink; DWORD dwReleaseCount = 0; INT interval; DWORD slot_count; if(g_fShutDown) { return; } time = GETTIMESTAMP(); Lock(&g_csMyTimerListLock); Lock(&g_csThreadListLock); time += (DEFAULT_TIME_RESOLUTION/2); // Service QST lists: Calculate how many array slots have expired and // service any timers in those slots. interval = (INT) (time - g_dwQSTLastRunTime); if( (interval) > 0) { slot_count = ((DWORD) interval) / QST_GRANULARITY; slot_count = MIN(slot_count, QST_SLOTCOUNT); if(slot_count < QST_SLOTCOUNT) { g_dwQSTLastRunTime += (slot_count * QST_GRANULARITY); } else { // If there was a LONG delay in scheduling this, (longer then the range of the whole array) // then we must complete everything that is on the array and then re-synchronize the times slot_count = QST_SLOTCOUNT; g_dwQSTLastRunTime = time; } while(slot_count--) { while( (pBilink = g_rgblQSTimerArray[g_uiQSTCurrentIndex].GetNext()) != &g_rgblQSTimerArray[g_uiQSTCurrentIndex] ) { pTimerWalker = CONTAINING_RECORD(pBilink, MYTIMER, Bilink); pBilink->RemoveFromList(); pTimerWalker->Bilink.InsertBefore( &g_blThreadList); pTimerWalker->TimerState = QueuedForThread; dwReleaseCount++; } g_uiQSTCurrentIndex = (g_uiQSTCurrentIndex + 1) & QST_MOD_MASK; } } // Walk the sorted timer list. Expired timers will all be at the front of the // list so we can stop checking as soon as we find any un-expired timer. pBilink = g_blMyTimerList.GetNext(); while(pBilink != &g_blMyTimerList) { pTimerWalker = CONTAINING_RECORD(pBilink, MYTIMER, Bilink); pBilink = pBilink->GetNext(); if(((INT)(time-pTimerWalker->TimeOut) > 0)) { pTimerWalker->Bilink.RemoveFromList(); pTimerWalker->Bilink.InsertBefore( &g_blThreadList); pTimerWalker->TimerState = QueuedForThread; dwReleaseCount++; } else { break; } } // Next walk the Standard Length list. Same rules apply pBilink=g_blStdTimerList.GetNext(); while(pBilink != &g_blStdTimerList) { pTimerWalker = CONTAINING_RECORD(pBilink, MYTIMER, Bilink); pBilink = pBilink->GetNext(); if(((INT)(time-pTimerWalker->TimeOut) > 0)) { pTimerWalker->Bilink.RemoveFromList(); pTimerWalker->Bilink.InsertBefore( &g_blThreadList); pTimerWalker->TimerState = QueuedForThread; dwReleaseCount++; } else { break; } } g_dwActiveRequests += dwReleaseCount; Unlock(&g_csThreadListLock); Unlock(&g_csMyTimerListLock); ReleaseSemaphore(g_hWorkToDoSem,dwReleaseCount,NULL); } #undef DPF_MODNAME #define DPF_MODNAME "ScheduleTimerThread" VOID ScheduleTimerThread(MYTIMERCALLBACK TimerCallBack, PVOID UserContext, PVOID *pHandle, PUINT pUnique) { PMYTIMER pTimer; if(g_fShutDown) { ASSERT(0); *pHandle = 0; *pUnique = 0; return; } pTimer = static_cast( g_pTimerPool->Get(g_pTimerPool) ); if (!pTimer) { *pHandle = 0; *pUnique = 0; return; } DPFX(DPFPREP,DPF_TIMER_LVL, "Parameters: TimerCallBack[%p], UserContext[%p] - Timer[%p]", TimerCallBack, UserContext, pTimer); pTimer->CallBack = TimerCallBack; pTimer->Context = UserContext; Lock(&g_csMyTimerListLock); Lock(&g_csThreadListLock); *pUnique = ++g_dwUnique; if(g_dwUnique == 0) { *pUnique = ++g_dwUnique; } pTimer->Unique = *pUnique; *pHandle = pTimer; pTimer->Bilink.InsertBefore( &g_blThreadList); pTimer->TimerState = QueuedForThread; g_dwActiveRequests++; Unlock(&g_csThreadListLock); Unlock(&g_csMyTimerListLock); ReleaseSemaphore(g_hWorkToDoSem,1,NULL); } #undef DPF_MODNAME #define DPF_MODNAME "SetMyTimer" VOID SetMyTimer(DWORD dwTimeOut, DWORD, MYTIMERCALLBACK TimerCallBack, PVOID UserContext, PVOID *pHandle, PUINT pUnique) { CBilink* pBilink; PMYTIMER pMyTimerWalker, pTimer; DWORD time; BOOL fInserted=FALSE; UINT Offset; UINT Index; if (g_fShutDown) { ASSERT(0); *pHandle = 0; *pUnique = 0; return; } time = GETTIMESTAMP(); pTimer = static_cast( g_pTimerPool->Get(g_pTimerPool) ); if (!pTimer) { *pHandle = 0; *pUnique = 0; return; } DPFX(DPFPREP,DPF_TIMER_LVL, "Parameters: dwTimeOut[%d], TimerCallBack[%p], UserContext[%p] - Timer[%p]", dwTimeOut, TimerCallBack, UserContext, pTimer); pTimer->CallBack = TimerCallBack; pTimer->Context = UserContext; Lock(&g_csMyTimerListLock); *pUnique = ++g_dwUnique; if(g_dwUnique == 0) { *pUnique = ++g_dwUnique; } pTimer->Unique = *pUnique; *pHandle = pTimer; pTimer->TimeOut=time+dwTimeOut; pTimer->TimerState=WaitingForTimeout; if(dwTimeOut < QST_MAX_TIMEOUT) { Offset = (dwTimeOut + (QST_GRANULARITY/2)) / QST_GRANULARITY; // Round nearest and convert time to slot offset Index = (Offset + g_uiQSTCurrentIndex) & QST_MOD_MASK; // Our index will be Current + Offset MOD TableSize pTimer->Bilink.InsertBefore( &g_rgblQSTimerArray[Index]); // Its called Quick-Start for a reason. } // OPTIMIZE FOR STANDARD TIMER // // Rather then calling a special API for StandardLongTimers as described above, we can just pull out // any timer with the correct Timeout value and stick it on the end of the StandardTimerList. I believe // this is the most straightforward way to do it. Now really, we could put anything with a TO +/- resolution // on the standard list too, but that might not be all that useful... else if(dwTimeOut == STANDARD_LONG_TIMEOUT_VALUE) { // This is a STANDARD TIMEOUT so add it to the end of the standard list. pTimer->Bilink.InsertBefore( &g_blStdTimerList); } // OPTIMIZE FOR SHORT TIMERS !! DONT NEED TO DO THIS IF USING Quick Start Timers !! // // If the timer has a very small Timeout value (~20ms) lets insert from the head of the list // instead of from the tail. else { // DEFAULT - Assume new timers will likely sort to the end of the list. // // Insert this guy in the sorted list by timeout time, walking from the tail forward. pBilink=g_blMyTimerList.GetPrev(); while(pBilink != &g_blMyTimerList) { pMyTimerWalker=CONTAINING_RECORD(pBilink, MYTIMER, Bilink); pBilink=pBilink->GetPrev(); if((int)(pTimer->TimeOut-pMyTimerWalker->TimeOut) > 0 ) { pTimer->Bilink.InsertAfter( &pMyTimerWalker->Bilink); fInserted=TRUE; break; } } if(!fInserted) { pTimer->Bilink.InsertAfter( &g_blMyTimerList); } } Unlock(&g_csMyTimerListLock); return; } #undef DPF_MODNAME #define DPF_MODNAME "CancelMyTimer" HRESULT CancelMyTimer(PVOID dwTimer, DWORD Unique) { PMYTIMER pTimer = (PMYTIMER)dwTimer; HRESULT hr = DPNERR_GENERIC; if(pTimer == 0) { return DPN_OK; } DPFX(DPFPREP,DPF_TIMER_LVL, "Parameters: Timer[%p]", pTimer); Lock(&g_csMyTimerListLock); Lock(&g_csThreadListLock); if(pTimer->Unique == Unique) { switch(pTimer->TimerState) { case WaitingForTimeout: pTimer->Bilink.RemoveFromList(); pTimer->TimerState = End; pTimer->Unique = 0; g_pTimerPool->Release(g_pTimerPool, pTimer); hr=DPN_OK; break; case QueuedForThread: pTimer->Bilink.RemoveFromList(); pTimer->TimerState = End; pTimer->Unique = 0; g_pTimerPool->Release(g_pTimerPool, pTimer); if(g_dwActiveRequests) { g_dwActiveRequests--; } g_dwExtraSignals++; hr = DPN_OK; break; default: DPFX(DPFPREP,DPF_TIMER_LVL, "Couldn't cancel timer - Timer[%p]", pTimer); break; } } Unlock(&g_csThreadListLock); Unlock(&g_csMyTimerListLock); return hr; } #undef DPF_MODNAME #define DPF_MODNAME "TimerInit" /* This function is for initialization that is done only once for the life of the module */ HRESULT TimerInit() { DWORD iSlot; DPFX(DPFPREP,DPF_TIMER_LVL, "Timer module-level initialization"); if (DNOSIsXPOrGreater()) { g_uiTimeSetEventFlags |= TIME_KILL_SYNCHRONOUS; } // Determine the maximum number of worker threads we will allow // Returns void, can't fail apparently GetSystemInfo(&g_SystemInfo); if (g_SystemInfo.dwNumberOfProcessors < 1) { g_SystemInfo.dwNumberOfProcessors = 1; } g_dwMaxTimerThreads = g_SystemInfo.dwNumberOfProcessors * MAX_TIMER_THREADS_PER_PROCESSOR; // Track thread handles in an array so we can wait on them at shutdown. g_phTimerThreadHandles = new HANDLE[g_dwMaxTimerThreads]; if ( g_phTimerThreadHandles == NULL) { return DPNERR_OUTOFMEMORY; } g_blMyTimerList.Initialize(); g_blStdTimerList.Initialize(); g_blThreadList.Initialize(); // Initialize all of the CBilink's for(iSlot = 0; iSlot < QST_SLOTCOUNT; iSlot++) { g_rgblQSTimerArray[iSlot].Initialize(); } if (DNInitializeCriticalSection(&g_csMyTimerListLock) == FALSE) { delete[] g_phTimerThreadHandles; g_phTimerThreadHandles = NULL; return DPNERR_OUTOFMEMORY; } DebugSetCriticalSectionRecursionCount(&g_csMyTimerListLock,0); if (DNInitializeCriticalSection(&g_csThreadListLock) == FALSE) { DNDeleteCriticalSection(&g_csMyTimerListLock); delete[] g_phTimerThreadHandles; g_phTimerThreadHandles = NULL; return DPNERR_OUTOFMEMORY; } DebugSetCriticalSectionRecursionCount(&g_csThreadListLock,0); g_pTimerPool = FPM_Create(sizeof(MYTIMER),NULL,NULL,NULL,NULL); if(!g_pTimerPool) { DNDeleteCriticalSection(&g_csThreadListLock); DNDeleteCriticalSection(&g_csMyTimerListLock); delete[] g_phTimerThreadHandles; g_phTimerThreadHandles = NULL; return DPNERR_OUTOFMEMORY; } // Set our time resolution to 1ms, ignore failure. (VOID)timeBeginPeriod(1); return DPN_OK; } #undef DPF_MODNAME #define DPF_MODNAME "TimerDeinit" /* This function is for initialization that is done only once for the life of the module */ VOID TimerDeinit() { ASSERT(g_fShutDown); DPFX(DPFPREP,DPF_TIMER_LVL, "Timer module-level deinitialization"); timeEndPeriod(1); DNDeleteCriticalSection(&g_csMyTimerListLock); DNDeleteCriticalSection(&g_csThreadListLock); if(g_pTimerPool) { g_pTimerPool->Fini(g_pTimerPool); } if (g_phTimerThreadHandles) { delete[] g_phTimerThreadHandles; g_phTimerThreadHandles = NULL; } } #undef DPF_MODNAME #define DPF_MODNAME "InitTimerWorkaround" HRESULT InitTimerWorkaround() { DWORD dwJunk; DWORD iSlot; DPFX(DPFPREP,DPF_TIMER_LVL, "Initialize Timer Package"); // Reinitialize globals g_nThreads = 0; // number of running threads. g_dwActiveRequests = 0; // number of requests being processed. g_dwExtraSignals = 0; ASSERT(g_phTimerThreadHandles); memset(g_phTimerThreadHandles, 0, sizeof(HANDLE) * g_dwMaxTimerThreads); ASSERT(g_blMyTimerList.IsEmpty()); ASSERT(g_blStdTimerList.IsEmpty()); ASSERT(g_blThreadList.IsEmpty()); #ifdef DEBUG for(iSlot = 0; iSlot < QST_SLOTCOUNT; iSlot++) { ASSERT(g_rgblQSTimerArray[iSlot].IsEmpty()); } #endif g_uiQSTCurrentIndex = 0; g_dwQSTLastRunTime = GETTIMESTAMP(); g_hWorkToDoSem = CreateSemaphore(NULL, 0, 65535, NULL); if (!g_hWorkToDoSem) { return DPNERR_OUTOFMEMORY; } // Start the timer g_dwWorkaroundTimerID = timeSetEvent(DEFAULT_TIME_RESOLUTION, DEFAULT_TIME_RESOLUTION, PeriodicTimer, 0, g_uiTimeSetEventFlags); if(!g_dwWorkaroundTimerID) { FiniTimerWorkaround(); return DPNERR_OUTOFMEMORY; } // We are up and running. Do this before starting the thread. g_fShutDown = FALSE; g_nThreads = 1; g_phTimerThreadHandles[0] = CreateThread(NULL, 4096, TimerWorkerThread, 0, 0, &dwJunk); if( !g_phTimerThreadHandles[0]) { g_nThreads = 0; FiniTimerWorkaround(); return DPNERR_OUTOFMEMORY; } return DPN_OK; } #undef DPF_MODNAME #define DPF_MODNAME "PurgeTimerList" VOID PurgeTimerList(CBilink *pList) { PMYTIMER pTimer; while(!pList->IsEmpty()) { pTimer = CONTAINING_RECORD(pList->GetNext(), MYTIMER, Bilink); pTimer->Unique = 0; pTimer->TimerState = End; pTimer->Bilink.RemoveFromList(); g_pTimerPool->Release(g_pTimerPool, pTimer); } } #undef DPF_MODNAME #define DPF_MODNAME "FiniTimerWorkaround" VOID FiniTimerWorkaround() { DWORD iSlot; DPFX(DPFPREP,DPF_TIMER_LVL, "Deinitialize Timer Package"); // At this point: // 1) No one else will call SetMyTimer or ScheduleTimerThread // 2) The only timer left should be AdjustTimerResolution // Kill the timer so it never fires again if(g_dwWorkaroundTimerID) { // We have to do this outside the lock because on XP this will be waiting on the last timer to fire // which may be waiting for the lock. timeKillEvent(g_dwWorkaroundTimerID); if (!(g_uiTimeSetEventFlags & TIME_KILL_SYNCHRONOUS)) { // The WinMM timer may try to fire again, so wait a little while for it DPFX(DPFPREP,DPF_TIMER_LVL, "OS is not XP or better, waiting for WinMM timer to finish"); Sleep(2000); } } // At this point: // 1) The winmm timer will not fire again and therefore PeriodicTimer will not be called again // Tell all remaining timer threads to shutdown Lock(&g_csThreadListLock); g_fShutDown = TRUE; Unlock(&g_csThreadListLock); ReleaseSemaphore(g_hWorkToDoSem, g_dwMaxTimerThreads, NULL); // At this point: // 1) No threads should be waiting in TimerWorkerThread and no new ones will be scheduled Lock(&g_csThreadListLock); for (iSlot = 0; iSlot < g_dwMaxTimerThreads; iSlot++) { // We can stop at the first NULL handle if (!g_phTimerThreadHandles[iSlot]) { break; } Unlock(&g_csThreadListLock); WaitForSingleObject(g_phTimerThreadHandles[iSlot], INFINITE); CloseHandle(g_phTimerThreadHandles[iSlot]); Lock(&g_csThreadListLock); g_phTimerThreadHandles[iSlot] = 0; } Unlock(&g_csThreadListLock); // At this point: // 1) All TimerWorkerThreads are gone CloseHandle(g_hWorkToDoSem); g_hWorkToDoSem = 0; PurgeTimerList(&g_blMyTimerList); PurgeTimerList(&g_blStdTimerList); PurgeTimerList(&g_blThreadList); for(iSlot = 0; iSlot < QST_SLOTCOUNT; iSlot++) { PurgeTimerList(&g_rgblQSTimerArray[iSlot]); } ASSERT(g_blMyTimerList.IsEmpty()); ASSERT(g_blStdTimerList.IsEmpty()); ASSERT(g_blThreadList.IsEmpty()); #ifdef DEBUG for(iSlot = 0; iSlot < QST_SLOTCOUNT; iSlot++) { ASSERT(g_rgblQSTimerArray[iSlot].IsEmpty()); } #endif } #undef DPF_MODNAME #define DPF_MODNAME "TimerWorkerThread" DWORD WINAPI TimerWorkerThread(LPVOID) { CBilink *pBilink; PMYTIMER pTimer; DWORD dwJunk; DWORD iThread; HRESULT hr; DPFX(DPFPREP,DPF_TIMER_LVL, "Timer thread starting 0x%x", GetCurrentThreadId()); if ((hr = COM_CoInitialize(NULL)) != S_OK) { DPFX(DPFPREP,0, "Timer thread failed to initialize COM hr=0x%x", hr); goto Exit; } while (1) { WaitForSingleObject(g_hWorkToDoSem, INFINITE); Lock(&g_csThreadListLock); if(g_fShutDown) { Unlock(&g_csThreadListLock); break; } if(g_dwExtraSignals) { g_dwExtraSignals--; Unlock(&g_csThreadListLock); continue; } if (g_dwActiveRequests > g_nThreads && g_nThreads < g_dwMaxTimerThreads) { ASSERT(g_phTimerThreadHandles[0] != 0); // The first slot should never be empty // Find the first empty slot. for (iThread = 0; iThread < g_dwMaxTimerThreads; iThread++) { if (g_phTimerThreadHandles[iThread] == 0) { // NOTE: CreateThread takes a long time and we are stalling all work by having // the lock when we call it. Revise in future. g_phTimerThreadHandles[iThread] = CreateThread(NULL, 4096, TimerWorkerThread, 0, 0, &dwJunk); if (g_phTimerThreadHandles[iThread]) { g_nThreads++; } // If CreateThread failed no harm is done we just don't get the extra help of // another worker thread. } } } pBilink = g_blThreadList.GetNext(); if(pBilink == &g_blThreadList) { Unlock(&g_csThreadListLock); continue; }; pBilink->RemoveFromList(); // pull off the list. pTimer = CONTAINING_RECORD(pBilink, MYTIMER, Bilink); // Call a callback DPFX(DPFPREP,DPF_TIMER_LVL, "Servicing Timer Job - Timer[%p], Context[%p], Callback[%p]", pTimer, pTimer->Context, pTimer->CallBack); pTimer->TimerState=InCallBack; Unlock(&g_csThreadListLock); (pTimer->CallBack)(pTimer, (UINT) pTimer->Unique, pTimer->Context); pTimer->Unique = 0; pTimer->TimerState = End; g_pTimerPool->Release(g_pTimerPool, pTimer); Lock(&g_csThreadListLock); if(g_dwActiveRequests) { g_dwActiveRequests--; } Unlock(&g_csThreadListLock); } COM_CoUninitialize(); Exit: // Thread is terminating. DPFX(DPFPREP,DPF_TIMER_LVL, "Timer thread exiting 0x%x", GetCurrentThreadId()); return 0; }