From 77f6bbf0ce7e57e1c19058dfbc351762fce6ee3f Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 4 Feb 2026 18:14:59 -0500 Subject: [PATCH 1/4] Fix SampleType field in ThreadSample event from SampleProfiler The SampleType field was always reporting EP_SAMPLE_PROFILER_SAMPLE_TYPE_EXTERNAL even when threads were executing managed code. This regressed in .NET 9 when SuspendAllThreads was ported from NativeAOT to CoreCLR (commit 00a897369db). Instead of iterating all threads during SuspendAllThreads (which scales poorly on many-core machines), each thread now saves its own GC mode when it voluntarily suspends: - In RareDisablePreemptiveGC when hitting the IsTrappingThreadsForSuspension check - In RedirectedHandledJITCase when a hijacked thread self-suspends This is pay-for-play: only threads that were actually executing managed code participate, and only during suspension. Fixes #123996 --- src/coreclr/vm/threadsuspend.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index 0dc93643a5985d..dfb27e1b618c54 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2138,6 +2138,11 @@ void Thread::RareDisablePreemptiveGC() if (ThreadStore::IsTrappingThreadsForSuspension()) { +#ifdef FEATURE_PERFTRACING + // Save the GC mode so the sample profiler can determine this thread was in managed code. + SaveGCModeOnSuspension(); +#endif // FEATURE_PERFTRACING + EnablePreemptiveGC(); #ifdef PROFILING_SUPPORTED @@ -2650,6 +2655,11 @@ void __stdcall Thread::RedirectedHandledJITCase(RedirectReason reason) reason == RedirectReason_DebugSuspension || reason == RedirectReason_UserSuspension); +#ifdef FEATURE_PERFTRACING + // Save the GC mode so the sample profiler can determine this thread was in managed code. + pThread->SaveGCModeOnSuspension(); +#endif // FEATURE_PERFTRACING + // Actual self-suspension. // Leave and reenter COOP mode to be trapped on the way back. GCX_PREEMP_NO_DTOR(); From c201efe3a5b8c44ebaa53d6ea56fa3595612d988 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 4 Feb 2026 23:41:27 -0500 Subject: [PATCH 2/4] Remove redundant SaveGCModeOnSuspension call in RedirectedHandledJITCase GCX_PREEMP_NO_DTOR_END() re-enters cooperative mode via DisablePreemptiveGC, which hits RareDisablePreemptiveGC where SaveGCModeOnSuspension is already called. --- src/coreclr/vm/threadsuspend.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index dfb27e1b618c54..cd4d76155a80fe 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2655,11 +2655,6 @@ void __stdcall Thread::RedirectedHandledJITCase(RedirectReason reason) reason == RedirectReason_DebugSuspension || reason == RedirectReason_UserSuspension); -#ifdef FEATURE_PERFTRACING - // Save the GC mode so the sample profiler can determine this thread was in managed code. - pThread->SaveGCModeOnSuspension(); -#endif // FEATURE_PERFTRACING - // Actual self-suspension. // Leave and reenter COOP mode to be trapped on the way back. GCX_PREEMP_NO_DTOR(); From 6b54e0921e79a1ae0171796491d89c82671982a8 Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Wed, 4 Feb 2026 23:55:51 -0500 Subject: [PATCH 3/4] Use TS_SuspensionTrapped thread state bit instead of m_gcModeOnSuspension Per feedback, replace the m_gcModeOnSuspension field with a ThreadState bit: - Add TS_SuspensionTrapped (0x00000002) to indicate thread is trapped for suspension - Set the bit when entering the IsTrappingThreadsForSuspension scope - Reset the bit when exiting that scope - Sample profiler checks this bit to determine if thread was in managed code - Delete m_gcModeOnSuspension field and its accessor methods This is cleaner because it directly indicates the thread state rather than redundantly storing the GC mode. --- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 10 +++----- src/coreclr/vm/threads.h | 23 +------------------ src/coreclr/vm/threadsuspend.cpp | 10 ++++---- 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 8b486c67b678d5..eb5fabdc6c7b54 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -127,10 +127,9 @@ walk_managed_stack_for_threads ( // Walk the stack and write it out as an event. if (ep_rt_coreclr_walk_managed_stack_for_thread (target_thread, current_stack_contents) && !ep_stack_contents_is_empty (current_stack_contents)) { - // Set the payload. If the GC mode on suspension > 0, then the thread was in cooperative mode. - // Even though there are some cases where this is not managed code, we assume it is managed code here. - // If the GC mode on suspension == 0 then the thread was in preemptive mode, which we qualify as external here. - uint32_t payload_data = target_thread->GetGCModeOnSuspension () ? EP_SAMPLE_PROFILER_SAMPLE_TYPE_MANAGED : EP_SAMPLE_PROFILER_SAMPLE_TYPE_EXTERNAL; + // Set the payload. If the thread is trapped for suspension, it was in cooperative mode (managed code). + // Otherwise, it was in preemptive mode (external code). + uint32_t payload_data = target_thread->HasThreadState (Thread::TS_SuspensionTrapped) ? EP_SAMPLE_PROFILER_SAMPLE_TYPE_MANAGED : EP_SAMPLE_PROFILER_SAMPLE_TYPE_EXTERNAL; // Write the sample. ep_write_sample_profile_event ( @@ -141,9 +140,6 @@ walk_managed_stack_for_threads ( (uint8_t *)&payload_data, sizeof (payload_data)); } - - // Reset the GC mode. - target_thread->ClearGCModeOnSuspension (); } ep_stack_contents_fini (current_stack_contents); diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index f54ee613485dc6..7250e49721cf4f 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -506,7 +506,7 @@ class Thread TS_AbortRequested = 0x00000001, // Abort the thread - // unused = 0x00000002, + TS_SuspensionTrapped = 0x00000002, // Thread is trapped waiting for suspension to complete (was in managed code) TS_GCSuspendRedirected = 0x00000004, // ThreadSuspend::SuspendRuntime has redirected the thread to suspention routine. TS_DebugSuspendPending = 0x00000008, // Is the debugger suspending threads? @@ -3584,32 +3584,11 @@ class Thread #ifdef FEATURE_PERFTRACING private: - // SampleProfiler thread state. This is set on suspension and cleared before restart. - // True if the thread was in cooperative mode. False if it was in preemptive when the suspension started. - Volatile m_gcModeOnSuspension; - // The activity ID for the current thread. // An activity ID of zero means the thread is not executing in the context of an activity. GUID m_activityId; public: - bool GetGCModeOnSuspension() - { - LIMITED_METHOD_CONTRACT; - return m_gcModeOnSuspension != 0U; - } - - void SaveGCModeOnSuspension() - { - LIMITED_METHOD_CONTRACT; - m_gcModeOnSuspension = m_fPreemptiveGCDisabled; - } - - void ClearGCModeOnSuspension() - { - m_gcModeOnSuspension = 0; - } - LPCGUID GetActivityId() const { LIMITED_METHOD_CONTRACT; diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index cd4d76155a80fe..dafb078348c34a 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2138,10 +2138,9 @@ void Thread::RareDisablePreemptiveGC() if (ThreadStore::IsTrappingThreadsForSuspension()) { -#ifdef FEATURE_PERFTRACING - // Save the GC mode so the sample profiler can determine this thread was in managed code. - SaveGCModeOnSuspension(); -#endif // FEATURE_PERFTRACING + // Mark that this thread is trapped for suspension. + // Used by the sample profiler to determine this thread was in managed code. + SetThreadState(TS_SuspensionTrapped); EnablePreemptiveGC(); @@ -2182,6 +2181,9 @@ void Thread::RareDisablePreemptiveGC() // disable preemptive gc. m_fPreemptiveGCDisabled.StoreWithoutBarrier(1); + // Clear the suspension trapped flag now that we're resuming. + ResetThreadState(TS_SuspensionTrapped); + // check again if we have something to do continue; } From 9287c030ffb5b162113160b36c052f9999aefe3e Mon Sep 17 00:00:00 2001 From: Steve Pfister Date: Thu, 5 Feb 2026 19:51:38 -0500 Subject: [PATCH 4/4] Fix OFFSETOF__Thread__m_pInterpThreadContext after m_gcModeOnSuspension removal --- src/coreclr/vm/amd64/asmconstants.h | 8 ++++---- src/coreclr/vm/arm/asmconstants.h | 2 +- src/coreclr/vm/arm64/asmconstants.h | 6 +++--- src/coreclr/vm/riscv64/asmconstants.h | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/coreclr/vm/amd64/asmconstants.h b/src/coreclr/vm/amd64/asmconstants.h index 6f8b50e5f87def..5a1dd76dd21240 100644 --- a/src/coreclr/vm/amd64/asmconstants.h +++ b/src/coreclr/vm/amd64/asmconstants.h @@ -570,15 +570,15 @@ ASMCONSTANTS_C_ASSERT(OFFSETOF__InterpMethod__pCallStub == offsetof(InterpMethod #ifdef TARGET_UNIX #ifdef _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0xb00 +#define OFFSETOF__Thread__m_pInterpThreadContext 0xaf8 #else // _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0x298 +#define OFFSETOF__Thread__m_pInterpThreadContext 0x290 #endif // _DEBUG #else // TARGET_UNIX #ifdef _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0xb58 +#define OFFSETOF__Thread__m_pInterpThreadContext 0xb50 #else // _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0x2e0 +#define OFFSETOF__Thread__m_pInterpThreadContext 0x2d8 #endif // _DEBUG #endif // TARGET_UNIX ASMCONSTANTS_C_ASSERT(OFFSETOF__Thread__m_pInterpThreadContext == offsetof(Thread, m_pInterpThreadContext)) diff --git a/src/coreclr/vm/arm/asmconstants.h b/src/coreclr/vm/arm/asmconstants.h index e07132357c5cdd..0c4fae1d2a3815 100644 --- a/src/coreclr/vm/arm/asmconstants.h +++ b/src/coreclr/vm/arm/asmconstants.h @@ -212,7 +212,7 @@ ASMCONSTANTS_C_ASSERT(OFFSETOF__ThreadLocalInfo__m_pThread == offsetof(ThreadLoc ASMCONSTANTS_C_ASSERT(OFFSETOF__InterpMethod__pCallStub == offsetof(InterpMethod, pCallStub)) #ifdef TARGET_UNIX -#define OFFSETOF__Thread__m_pInterpThreadContext 0x634 +#define OFFSETOF__Thread__m_pInterpThreadContext 0x630 #else // TARGET_UNIX #define OFFSETOF__Thread__m_pInterpThreadContext 0x0 #endif // TARGET_UNIX diff --git a/src/coreclr/vm/arm64/asmconstants.h b/src/coreclr/vm/arm64/asmconstants.h index 9f78786c3da059..01bc8d14aa38d7 100644 --- a/src/coreclr/vm/arm64/asmconstants.h +++ b/src/coreclr/vm/arm64/asmconstants.h @@ -306,12 +306,12 @@ ASMCONSTANTS_C_ASSERT(OFFSETOF__InterpMethod__pCallStub == offsetof(InterpMethod #ifdef TARGET_UNIX #ifdef _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0xb28 +#define OFFSETOF__Thread__m_pInterpThreadContext 0xb20 #else // _DEBUG -#define OFFSETOF__Thread__m_pInterpThreadContext 0x2c0 +#define OFFSETOF__Thread__m_pInterpThreadContext 0x2b8 #endif // _DEBUG #else // TARGET_UNIX -#define OFFSETOF__Thread__m_pInterpThreadContext 0xb50 +#define OFFSETOF__Thread__m_pInterpThreadContext 0xb48 #endif // TARGET_UNIX ASMCONSTANTS_C_ASSERT(OFFSETOF__Thread__m_pInterpThreadContext == offsetof(Thread, m_pInterpThreadContext)) diff --git a/src/coreclr/vm/riscv64/asmconstants.h b/src/coreclr/vm/riscv64/asmconstants.h index 12efe007f07c4f..7a892c5a918e7a 100644 --- a/src/coreclr/vm/riscv64/asmconstants.h +++ b/src/coreclr/vm/riscv64/asmconstants.h @@ -242,7 +242,7 @@ ASMCONSTANTS_C_ASSERT(OFFSETOF__ThreadLocalInfo__m_pThread == offsetof(ThreadLoc #endif ASMCONSTANTS_C_ASSERT(OFFSETOF__InterpMethod__pCallStub == offsetof(InterpMethod, pCallStub)) -#define OFFSETOF__Thread__m_pInterpThreadContext 0xB28 +#define OFFSETOF__Thread__m_pInterpThreadContext 0xB20 ASMCONSTANTS_C_ASSERT(OFFSETOF__Thread__m_pInterpThreadContext == offsetof(Thread, m_pInterpThreadContext)) #define OFFSETOF__InterpThreadContext__pStackPointer 0x10