Skip to content

Commit 02d048e

Browse files
committed
Avoid nested spin lock in WriteAllBuffersToFile and GetNextEvent
1 parent 30130c3 commit 02d048e

File tree

2 files changed

+166
-62
lines changed

2 files changed

+166
-62
lines changed

src/vm/eventpipebuffermanager.cpp

Lines changed: 161 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -479,25 +479,43 @@ void EventPipeBufferManager::WriteAllBuffersToFile(EventPipeFile *pFile, LARGE_I
479479
// 9. Process again (go to 3).
480480
// 10. Continue until there are no more buffers to process.
481481

482-
// Take the lock before walking the buffer list.
483-
SpinLockHolder _slh(&m_lock);
484-
485482
// Naively walk the circular buffer, writing the event stream in timestamp order.
486483
m_numEventsWritten = 0;
487484
while(true)
488485
{
489486
EventPipeEventInstance *pOldestInstance = NULL;
490487
EventPipeBuffer *pOldestContainingBuffer = NULL;
491488
EventPipeBufferList *pOldestContainingList = NULL;
492-
SListElem<EventPipeBufferList*> *pElem = m_pPerThreadBufferList->GetHead();
493-
while(pElem != NULL)
489+
490+
CQuickArrayList<EventPipeBuffer*> bufferList;
491+
CQuickArrayList<EventPipeBufferList*> bufferListList;
494492
{
495-
EventPipeBufferList *pBufferList = pElem->GetValue();
493+
// Take the lock before walking the buffer list.
494+
SpinLockHolder _slh(&m_lock);
495+
SListElem<EventPipeBufferList*> *pElem = m_pPerThreadBufferList->GetHead();
496+
while(pElem != NULL)
497+
{
498+
EventPipeBufferList* pBufferList = pElem->GetValue();
499+
EventPipeBuffer* pBuffer = pBufferList->TryGetBuffer(stopTimeStamp);
500+
if (pBuffer != nullptr)
501+
{
502+
bufferListList.Push(pBufferList);
503+
bufferList.Push(pBuffer);
504+
}
505+
pElem = m_pPerThreadBufferList->GetNext(pElem);
506+
}
507+
}
496508

497-
// Peek the next event out of the list.
498-
EventPipeBuffer *pContainingBuffer = NULL;
499-
EventPipeEventInstance *pNext = pBufferList->PeekNextEvent(stopTimeStamp, &pContainingBuffer);
500-
if(pNext != NULL)
509+
for (size_t i = 0 ; i < bufferList.Size(); i++)
510+
{
511+
EventPipeBufferList* pBufferList = bufferListList[i];
512+
EventPipeBuffer* pBuffer = bufferList[i];
513+
pBufferList->ConvertBufferToReadOnly(pBuffer);
514+
515+
// Peek the next event out of the buffer.
516+
EventPipeBuffer *pContainingBuffer = pBuffer;
517+
EventPipeEventInstance *pNext = pBuffer->PeekNext(stopTimeStamp);
518+
if (pNext != NULL)
501519
{
502520
// If it's the oldest event we've seen, then save it.
503521
if((pOldestInstance == NULL) ||
@@ -507,9 +525,7 @@ void EventPipeBufferManager::WriteAllBuffersToFile(EventPipeFile *pFile, LARGE_I
507525
pOldestContainingBuffer = pContainingBuffer;
508526
pOldestContainingList = pBufferList;
509527
}
510-
}
511-
512-
pElem = m_pPerThreadBufferList->GetNext(pElem);
528+
}
513529
}
514530

515531
if(pOldestInstance == NULL)
@@ -522,8 +538,12 @@ void EventPipeBufferManager::WriteAllBuffersToFile(EventPipeFile *pFile, LARGE_I
522538
pFile->WriteEvent(*pOldestInstance);
523539

524540
m_numEventsWritten++;
525-
// Pop the event from the buffer.
526-
pOldestContainingList->PopNextEvent(stopTimeStamp);
541+
542+
{
543+
SpinLockHolder _slh(&m_lock);
544+
// Pop the event from the buffer.
545+
pOldestContainingList->PopNextEvent(stopTimeStamp);
546+
}
527547
}
528548

529549
if (m_numEventsWritten > 0)
@@ -541,62 +561,78 @@ EventPipeEventInstance* EventPipeBufferManager::GetNextEvent()
541561
}
542562
CONTRACTL_END;
543563

544-
// Take the lock before walking the buffer list.
545-
SpinLockHolder _slh(&m_lock);
546-
547-
// Naively walk the circular buffer, getting the event stream in timestamp order.
548564
LARGE_INTEGER stopTimeStamp;
549565
QueryPerformanceCounter(&stopTimeStamp);
550-
while (true)
566+
567+
EventPipeEventInstance *pOldestInstance = NULL;
568+
EventPipeBuffer *pOldestContainingBuffer = NULL;
569+
EventPipeBufferList *pOldestContainingList = NULL;
570+
571+
CQuickArrayList<EventPipeBuffer*> bufferList;
572+
CQuickArrayList<EventPipeBufferList*> bufferListList;
551573
{
552-
EventPipeEventInstance *pOldestInstance = NULL;
553-
EventPipeBuffer *pOldestContainingBuffer = NULL;
554-
EventPipeBufferList *pOldestContainingList = NULL;
574+
// Take the lock before walking the buffer list.
575+
SpinLockHolder _slh(&m_lock);
555576
SListElem<EventPipeBufferList*> *pElem = m_pPerThreadBufferList->GetHead();
556-
while (pElem != NULL)
577+
while(pElem != NULL)
557578
{
558-
EventPipeBufferList *pBufferList = pElem->GetValue();
559-
560-
// Peek the next event out of the list.
561-
EventPipeBuffer *pContainingBuffer = NULL;
562-
563-
// PERF: This may be too aggressive? If this method is being called frequently enough to keep pace with the
564-
// writing threads we could be in a state of high lock contention and lots of churning buffers. Each writer
565-
// would take several locks, allocate a new buffer, write one event into it, then the reader would take the
566-
// lock, convert the buffer to read-only and read the single event out of it. Allowing more events to accumulate
567-
// in the buffers before converting between writable and read-only amortizes a lot of the overhead. One way
568-
// to achieve that would be picking a stopTimeStamp that was Xms in the past. This would let Xms of events
569-
// to accumulate in the write buffer before we converted it and forced the writer to allocate another. Other more
570-
// sophisticated approaches would probably build a low overhead synchronization mechanism to read and write the
571-
// buffer at the same time.
572-
EventPipeEventInstance *pNext = pBufferList->PeekNextEvent(stopTimeStamp, &pContainingBuffer);
573-
if (pNext != NULL)
579+
EventPipeBufferList* pBufferList = pElem->GetValue();
580+
EventPipeBuffer* pBuffer = pBufferList->TryGetBuffer(stopTimeStamp);
581+
if (pBuffer != nullptr)
574582
{
575-
// If it's the oldest event we've seen, then save it.
576-
if ((pOldestInstance == NULL) ||
577-
(pOldestInstance->GetTimeStamp()->QuadPart > pNext->GetTimeStamp()->QuadPart))
578-
{
579-
pOldestInstance = pNext;
580-
pOldestContainingBuffer = pContainingBuffer;
581-
pOldestContainingList = pBufferList;
582-
}
583+
bufferListList.Push(pBufferList);
584+
bufferList.Push(pBuffer);
583585
}
584-
585586
pElem = m_pPerThreadBufferList->GetNext(pElem);
586587
}
588+
}
587589

588-
if (pOldestInstance == NULL)
590+
for (size_t i = 0 ; i < bufferList.Size(); i++)
591+
{
592+
EventPipeBufferList* pBufferList = bufferListList[i];
593+
EventPipeBuffer* pBuffer = bufferList[i];
594+
pBufferList->ConvertBufferToReadOnly(pBuffer);
595+
596+
// Peek the next event out of the buffer.
597+
EventPipeBuffer *pContainingBuffer = pBuffer;
598+
599+
// PERF: This may be too aggressive? If this method is being called frequently enough to keep pace with the
600+
// writing threads we could be in a state of high lock contention and lots of churning buffers. Each writer
601+
// would take several locks, allocate a new buffer, write one event into it, then the reader would take the
602+
// lock, convert the buffer to read-only and read the single event out of it. Allowing more events to accumulate
603+
// in the buffers before converting between writable and read-only amortizes a lot of the overhead. One way
604+
// to achieve that would be picking a stopTimeStamp that was Xms in the past. This would let Xms of events
605+
// to accumulate in the write buffer before we converted it and forced the writer to allocate another. Other more
606+
// sophisticated approaches would probably build a low overhead synchronization mechanism to read and write the
607+
// buffer at the same time.
608+
EventPipeEventInstance *pNext = pBuffer->PeekNext(stopTimeStamp);
609+
if (pNext != NULL)
589610
{
590-
// We're done. There are no more events.
591-
return NULL;
592-
}
611+
// If it's the oldest event we've seen, then save it.
612+
if((pOldestInstance == NULL) ||
613+
(pOldestInstance->GetTimeStamp()->QuadPart > pNext->GetTimeStamp()->QuadPart))
614+
{
615+
pOldestInstance = pNext;
616+
pOldestContainingBuffer = pContainingBuffer;
617+
pOldestContainingList = pBufferList;
618+
}
619+
}
620+
}
621+
622+
if(pOldestInstance == NULL)
623+
{
624+
// We're done. There are no more events.
625+
return nullptr;
626+
}
593627

628+
{
629+
SpinLockHolder _slh(&m_lock);
594630
// Pop the event from the buffer.
595631
pOldestContainingList->PopNextEvent(stopTimeStamp);
596-
597-
// Return the oldest event that hasn't yet been processed.
598-
return pOldestInstance;
599632
}
633+
634+
// Return the oldest event that hasn't yet been processed.
635+
return pOldestInstance;
600636
}
601637

602638
void EventPipeBufferManager::SuspendWriteEvent()
@@ -875,6 +911,69 @@ unsigned int EventPipeBufferList::GetCount() const
875911
return m_bufferCount;
876912
}
877913

914+
EventPipeBuffer* EventPipeBufferList::TryGetBuffer(LARGE_INTEGER beforeTimeStamp)
915+
{
916+
LIMITED_METHOD_CONTRACT;
917+
_ASSERTE(EventPipe::IsBufferManagerLockOwnedByCurrentThread());
918+
/**
919+
* There are 4 cases we need to handle in this function:
920+
* 1) There is no buffer in the list, in this case, return nullptr
921+
* 2) The head buffer is written to but not read yet, in this case, return that buffer
922+
* 2.1) It is possible that the head buffer is the only buffer that is created and is empty, or
923+
* 2.2) The head buffer is written to but not read
924+
* We cannot differentiate the two cases without reading it - but it is okay, in both cases, the buffer represents the head of the buffer list.
925+
* Note that writing to the buffer can happen after we return from this function, and it is also okay.
926+
* 3.) The head buffer is read but not completely reading, and
927+
* 4.) The head buffer is read completely.
928+
* This case requires special attention because it is possible that the next buffer in the list contain the oldest event. Fortunately, it is
929+
* already read so it is safe to read it to determine this case.
930+
*/
931+
932+
if (this->m_pHeadBuffer == nullptr)
933+
{
934+
// Case 1
935+
return nullptr;
936+
}
937+
if (this->m_pHeadBuffer->GetCreationTimeStamp().QuadPart >= beforeTimeStamp.QuadPart)
938+
{
939+
// If the oldest buffer is still newer than the beforeTimeStamp, we can stop.
940+
return nullptr;
941+
}
942+
EventPipeBufferState bufferState = this->m_pHeadBuffer->GetVolatileState();
943+
if (bufferState != EventPipeBufferState::READ_ONLY)
944+
{
945+
// Case 2 (2.1 or 2.2)
946+
return this->m_pHeadBuffer;
947+
}
948+
else
949+
{
950+
if (this->m_pHeadBuffer->PeekNext(beforeTimeStamp))
951+
{
952+
// Case 3
953+
return this->m_pHeadBuffer;
954+
}
955+
else
956+
{
957+
// Case 4
958+
return this->m_pHeadBuffer->GetNext();
959+
}
960+
}
961+
}
962+
963+
void EventPipeBufferList::ConvertBufferToReadOnly(EventPipeBuffer* pNewReadBuffer)
964+
{
965+
LIMITED_METHOD_CONTRACT;
966+
_ASSERTE(pNewReadBuffer != nullptr);
967+
_ASSERTE(!EventPipe::IsBufferManagerLockOwnedByCurrentThread());
968+
{
969+
SpinLockHolder _slh(m_pThread->GetLock());
970+
if (m_pThread->GetWriteBuffer() == pNewReadBuffer)
971+
{
972+
m_pThread->SetWriteBuffer(nullptr);
973+
}
974+
}
975+
}
976+
878977
EventPipeBuffer* EventPipeBufferList::TryGetReadBuffer(LARGE_INTEGER beforeTimeStamp, EventPipeBuffer* pNewReadBuffer)
879978
{
880979
LIMITED_METHOD_CONTRACT;
@@ -961,13 +1060,13 @@ EventPipeEventInstance* EventPipeBufferList::PopNextEvent(LARGE_INTEGER beforeTi
9611060
// Check to see if we need to clean-up the buffer that contained the previously popped event.
9621061
if(pContainingBuffer->GetPrevious() != NULL)
9631062
{
964-
// Remove the previous node. The previous node should always be the head node.
965-
EventPipeBuffer *pRemoved = GetAndRemoveHead();
966-
_ASSERTE(pRemoved != pContainingBuffer);
967-
_ASSERTE(pContainingBuffer == GetHead());
1063+
// Remove the previous node. The previous node should always be the head node.
1064+
EventPipeBuffer *pRemoved = GetAndRemoveHead();
1065+
_ASSERTE(pRemoved != pContainingBuffer);
1066+
_ASSERTE(pContainingBuffer == GetHead());
9681067

969-
// De-allocate the buffer.
970-
m_pManager->DeAllocateBuffer(pRemoved);
1068+
// De-allocate the buffer.
1069+
m_pManager->DeAllocateBuffer(pRemoved);
9711070
}
9721071

9731072
// If the event is non-NULL, pop it.

src/vm/eventpipebuffermanager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ class EventPipeBufferList
210210
// Get the thread associated with this list.
211211
EventPipeThread* GetThread();
212212

213+
// Get the first buffer that might contain the oldest event
214+
EventPipeBuffer* TryGetBuffer(LARGE_INTEGER beforeTimeStamp);
215+
216+
// Convert the buffer into read only
217+
void ConvertBufferToReadOnly(EventPipeBuffer *pNewReadBuffer);
213218

214219
#ifdef _DEBUG
215220
// Validate the consistency of the list.

0 commit comments

Comments
 (0)