Skip to content

Commit 7eb5ef2

Browse files
[release/9.0-staging] [Profiler] Avoid Recursive ThreadStoreLock in Profiling Thread Enumerator (#110665)
* [Profiler] Avoid Recursive ThreadStoreLock Profiling Enumerators look to acquire the ThreadStoreLock. In release config, re-acquiring the ThreadStoreLock and releasing it in ProfilerThreadEnum::Init will cause problems if the callback invoking EnumThread has logic that depends on the ThreadStoreLock being held. To avoid recursively acquiring the ThreadStoreLock, expand the condition when the profiling thread enumerator shouldn't acquire the ThreadStoreLock. * [Profiler] Change order to set fProfilerRequestedRuntimeSuspend There was a potential race condition when setting the flag before suspending and resetting the flag after restarting. For example, if the thread restarting runtime is preempted right after resuming runtime, the flag could remain unset by the time another thread looks to suspend runtime, which would see that the flag as set. * [Profiler][Tests] Add unit test for EnumThreads during suspension * [Profiler][Tests] Fixup EnumThreads test --------- Co-authored-by: mdh1418 <[email protected]>
1 parent 7f7a8b6 commit 7eb5ef2

File tree

8 files changed

+229
-6
lines changed

8 files changed

+229
-6
lines changed

src/coreclr/vm/profilingenumerators.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,9 +556,11 @@ HRESULT ProfilerThreadEnum::Init()
556556
}
557557
CONTRACTL_END;
558558

559+
// If EnumThreads is called from a profiler callback where the runtime is already suspended,
560+
// don't recursively acquire/release the ThreadStore Lock.
559561
// If a profiler has requested that the runtime suspend to do stack snapshots, it
560562
// will be holding the ThreadStore lock already
561-
ThreadStoreLockHolder tsLock(!g_profControlBlock.fProfilerRequestedRuntimeSuspend);
563+
ThreadStoreLockHolder tsLock(!ThreadStore::HoldingThreadStore() && !g_profControlBlock.fProfilerRequestedRuntimeSuspend);
562564

563565
Thread * pThread = NULL;
564566

src/coreclr/vm/proftoeeinterfaceimpl.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6873,8 +6873,8 @@ HRESULT ProfToEEInterfaceImpl::SuspendRuntime()
68736873
return CORPROF_E_SUSPENSION_IN_PROGRESS;
68746874
}
68756875

6876-
g_profControlBlock.fProfilerRequestedRuntimeSuspend = TRUE;
68776876
ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_REASON::SUSPEND_FOR_PROFILER);
6877+
g_profControlBlock.fProfilerRequestedRuntimeSuspend = TRUE;
68786878
return S_OK;
68796879
}
68806880

@@ -6912,8 +6912,8 @@ HRESULT ProfToEEInterfaceImpl::ResumeRuntime()
69126912
return CORPROF_E_UNSUPPORTED_CALL_SEQUENCE;
69136913
}
69146914

6915-
ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */);
69166915
g_profControlBlock.fProfilerRequestedRuntimeSuspend = FALSE;
6916+
ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */);
69176917
return S_OK;
69186918
}
69196919

@@ -7705,8 +7705,8 @@ HRESULT ProfToEEInterfaceImpl::EnumerateGCHeapObjects(ObjectCallback callback, v
77057705
// SuspendEE() may race with other threads by design and this thread may block
77067706
// arbitrarily long inside SuspendEE() for other threads to complete their own
77077707
// suspensions.
7708-
g_profControlBlock.fProfilerRequestedRuntimeSuspend = TRUE;
77097708
ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_REASON::SUSPEND_FOR_PROFILER);
7709+
g_profControlBlock.fProfilerRequestedRuntimeSuspend = TRUE;
77107710
ownEESuspension = TRUE;
77117711
}
77127712

@@ -7738,8 +7738,8 @@ HRESULT ProfToEEInterfaceImpl::EnumerateGCHeapObjects(ObjectCallback callback, v
77387738

77397739
if (ownEESuspension)
77407740
{
7741-
ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */);
77427741
g_profControlBlock.fProfilerRequestedRuntimeSuspend = FALSE;
7742+
ThreadSuspend::RestartEE(FALSE /* bFinishedGC */, TRUE /* SuspendSucceeded */);
77437743
}
77447744

77457745
return hr;

src/tests/profiler/native/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ project(Profiler)
55
set(SOURCES
66
assemblyprofiler/assemblyprofiler.cpp
77
eltprofiler/slowpatheltprofiler.cpp
8+
enumthreadsprofiler/enumthreadsprofiler.cpp
89
eventpipeprofiler/eventpipereadingprofiler.cpp
910
eventpipeprofiler/eventpipewritingprofiler.cpp
1011
eventpipeprofiler/eventpipemetadatareader.cpp
1112
gcallocateprofiler/gcallocateprofiler.cpp
12-
nongcheap/nongcheap.cpp
1313
gcbasicprofiler/gcbasicprofiler.cpp
1414
gcheapenumerationprofiler/gcheapenumerationprofiler.cpp
1515
gcheapenumerationprofiler/gcheapenumerationprofiler.def
@@ -20,6 +20,7 @@ set(SOURCES
2020
metadatagetdispenser/metadatagetdispenser.cpp
2121
moduleload/moduleload.cpp
2222
multiple/multiple.cpp
23+
nongcheap/nongcheap.cpp
2324
nullprofiler/nullprofiler.cpp
2425
rejitprofiler/rejitprofiler.cpp
2526
rejitprofiler/ilrewriter.cpp

src/tests/profiler/native/classfactory.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "classfactory.h"
55
#include "eltprofiler/slowpatheltprofiler.h"
6+
#include "enumthreadsprofiler/enumthreadsprofiler.h"
67
#include "eventpipeprofiler/eventpipereadingprofiler.h"
78
#include "eventpipeprofiler/eventpipewritingprofiler.h"
89
#include "getappdomainstaticaddress/getappdomainstaticaddress.h"
@@ -144,6 +145,10 @@ HRESULT STDMETHODCALLTYPE ClassFactory::CreateInstance(IUnknown *pUnkOuter, REFI
144145
{
145146
profiler = new GCHeapEnumerationProfiler();
146147
}
148+
else if (clsid == EnumThreadsProfiler::GetClsid())
149+
{
150+
profiler = new EnumThreadsProfiler();
151+
}
147152
else
148153
{
149154
printf("No profiler found in ClassFactory::CreateInstance. Did you add your profiler to the list?\n");
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#include "enumthreadsprofiler.h"
5+
6+
GUID EnumThreadsProfiler::GetClsid()
7+
{
8+
// {0742962D-2ED3-44B0-BA84-06B1EF0A0A0B}
9+
GUID clsid = { 0x0742962d, 0x2ed3, 0x44b0,{ 0xba, 0x84, 0x06, 0xb1, 0xef, 0x0a, 0x0a, 0x0b } };
10+
return clsid;
11+
}
12+
13+
HRESULT EnumThreadsProfiler::Initialize(IUnknown* pICorProfilerInfoUnk)
14+
{
15+
Profiler::Initialize(pICorProfilerInfoUnk);
16+
printf("EnumThreadsProfiler::Initialize\n");
17+
18+
HRESULT hr = S_OK;
19+
if (FAILED(hr = pCorProfilerInfo->SetEventMask2(COR_PRF_MONITOR_GC | COR_PRF_MONITOR_SUSPENDS, COR_PRF_HIGH_MONITOR_NONE)))
20+
{
21+
printf("FAIL: ICorProfilerInfo::SetEventMask2() failed hr=0x%x", hr);
22+
IncrementFailures();
23+
}
24+
25+
return hr;
26+
}
27+
28+
HRESULT STDMETHODCALLTYPE EnumThreadsProfiler::GarbageCollectionStarted(int cGenerations, BOOL generationCollected[], COR_PRF_GC_REASON reason)
29+
{
30+
SHUTDOWNGUARD();
31+
32+
printf("EnumThreadsProfiler::GarbageCollectionStarted\n");
33+
_gcStarts.fetch_add(1, std::memory_order_relaxed);
34+
if (_gcStarts < _gcFinishes)
35+
{
36+
IncrementFailures();
37+
printf("EnumThreadsProfiler::GarbageCollectionStarted: FAIL: Expected GCStart >= GCFinish. Start=%d, Finish=%d\n", (int)_gcStarts, (int)_gcFinishes);
38+
}
39+
40+
return S_OK;
41+
}
42+
43+
HRESULT STDMETHODCALLTYPE EnumThreadsProfiler::GarbageCollectionFinished()
44+
{
45+
SHUTDOWNGUARD();
46+
47+
printf("EnumThreadsProfiler::GarbageCollectionFinished\n");
48+
_gcFinishes.fetch_add(1, std::memory_order_relaxed);
49+
if (_gcStarts < _gcFinishes)
50+
{
51+
IncrementFailures();
52+
printf("EnumThreadsProfiler::GarbageCollectionFinished: FAIL: Expected GCStart >= GCFinish. Start=%d, Finish=%d\n", (int)_gcStarts, (int)_gcFinishes);
53+
}
54+
55+
return S_OK;
56+
}
57+
58+
HRESULT STDMETHODCALLTYPE EnumThreadsProfiler::RuntimeSuspendFinished()
59+
{
60+
SHUTDOWNGUARD();
61+
62+
printf("EnumThreadsProfiler::RuntimeSuspendFinished\n");
63+
64+
ICorProfilerThreadEnum* threadEnum = nullptr;
65+
HRESULT enumThreadsHR = pCorProfilerInfo->EnumThreads(&threadEnum);
66+
printf("Finished enumerating threads\n");
67+
_profilerEnumThreadsCompleted.fetch_add(1, std::memory_order_relaxed);
68+
threadEnum->Release();
69+
return enumThreadsHR;
70+
}
71+
72+
HRESULT EnumThreadsProfiler::Shutdown()
73+
{
74+
Profiler::Shutdown();
75+
76+
if (_gcStarts == 0)
77+
{
78+
printf("EnumThreadsProfiler::Shutdown: FAIL: Expected GarbageCollectionStarted to be called\n");
79+
}
80+
else if (_gcFinishes == 0)
81+
{
82+
printf("EnumThreadsProfiler::Shutdown: FAIL: Expected GarbageCollectionFinished to be called\n");
83+
}
84+
else if (_profilerEnumThreadsCompleted == 0)
85+
{
86+
printf("EnumThreadsProfiler::Shutdown: FAIL: Expected RuntimeSuspendFinished to be called and EnumThreads completed\n");
87+
}
88+
else if(_failures == 0)
89+
{
90+
printf("PROFILER TEST PASSES\n");
91+
}
92+
else
93+
{
94+
// failures were printed earlier when _failures was incremented
95+
}
96+
fflush(stdout);
97+
98+
return S_OK;
99+
}
100+
101+
void EnumThreadsProfiler::IncrementFailures()
102+
{
103+
_failures.fetch_add(1, std::memory_order_relaxed);
104+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
#pragma once
5+
6+
#include "../profiler.h"
7+
8+
class EnumThreadsProfiler : public Profiler
9+
{
10+
public:
11+
EnumThreadsProfiler() : Profiler(),
12+
_gcStarts(0),
13+
_gcFinishes(0),
14+
_profilerEnumThreadsCompleted(0),
15+
_failures(0)
16+
{}
17+
18+
// Profiler callbacks override
19+
static GUID GetClsid();
20+
virtual HRESULT STDMETHODCALLTYPE Initialize(IUnknown* pICorProfilerInfoUnk);
21+
virtual HRESULT STDMETHODCALLTYPE GarbageCollectionStarted(int cGenerations, BOOL generationCollected[], COR_PRF_GC_REASON reason);
22+
virtual HRESULT STDMETHODCALLTYPE GarbageCollectionFinished();
23+
virtual HRESULT STDMETHODCALLTYPE RuntimeSuspendFinished();
24+
virtual HRESULT STDMETHODCALLTYPE Shutdown();
25+
26+
// Helper methods
27+
void IncrementFailures();
28+
29+
private:
30+
std::atomic<int> _gcStarts;
31+
std::atomic<int> _gcFinishes;
32+
std::atomic<int> _profilerEnumThreadsCompleted;
33+
std::atomic<int> _failures;
34+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace Profiler.Tests
7+
{
8+
class EnumThreadsTests
9+
{
10+
static readonly Guid EnumThreadsProfilerGuid = new Guid("0742962D-2ED3-44B0-BA84-06B1EF0A0A0B");
11+
12+
public static int EnumerateThreadsWithNonProfilerRequestedRuntimeSuspension()
13+
{
14+
GC.Collect();
15+
return 100;
16+
}
17+
18+
public static int Main(string[] args)
19+
{
20+
if (args.Length > 0 && args[0].Equals("RunTest", StringComparison.OrdinalIgnoreCase))
21+
{
22+
switch (args[1])
23+
{
24+
case nameof(EnumerateThreadsWithNonProfilerRequestedRuntimeSuspension):
25+
return EnumerateThreadsWithNonProfilerRequestedRuntimeSuspension();
26+
default:
27+
return 102;
28+
}
29+
}
30+
31+
if (!RunProfilerTest(nameof(EnumerateThreadsWithNonProfilerRequestedRuntimeSuspension)))
32+
{
33+
return 101;
34+
}
35+
36+
return 100;
37+
}
38+
39+
private static bool RunProfilerTest(string testName)
40+
{
41+
try
42+
{
43+
return ProfilerTestRunner.Run(profileePath: System.Reflection.Assembly.GetExecutingAssembly().Location,
44+
testName: "EnumThreads",
45+
profilerClsid: EnumThreadsProfilerGuid,
46+
profileeArguments: testName
47+
) == 100;
48+
}
49+
catch (Exception ex)
50+
{
51+
Console.WriteLine(ex);
52+
}
53+
return false;
54+
}
55+
}
56+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFrameworkIdentifier>.NETCoreApp</TargetFrameworkIdentifier>
4+
<OutputType>exe</OutputType>
5+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
6+
<Optimize>true</Optimize>
7+
<!-- This test provides no interesting scenarios for GCStress -->
8+
<GCStressIncompatible>true</GCStressIncompatible>
9+
<!-- The test launches a secondary process and process launch creates
10+
an infinite event loop in the SocketAsyncEngine on Linux. Since
11+
runincontext loads even framework assemblies into the unloadable
12+
context, locals in this loop prevent unloading -->
13+
<UnloadabilityIncompatible>true</UnloadabilityIncompatible>
14+
</PropertyGroup>
15+
<ItemGroup>
16+
<Compile Include="$(MSBuildProjectName).cs" />
17+
<ProjectReference Include="$(TestSourceDir)Common/CoreCLRTestLibrary/CoreCLRTestLibrary.csproj" />
18+
<ProjectReference Include="../common/profiler_common.csproj" />
19+
<CMakeProjectReference Include="$(MSBuildThisFileDirectory)/../native/CMakeLists.txt" />
20+
</ItemGroup>
21+
</Project>

0 commit comments

Comments
 (0)