diff --git a/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs index 41183e9be85875..c024c6caa14baf 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs @@ -190,5 +190,13 @@ public static void PulseAll(object obj) [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ObjectNative_GetMonitorLockContentionCount")] private static partial long GetLockContentionCount(); + + /// + /// Gets the number of times there was a pause upon using 's wait so far. + /// + public static long WaitCount => GetWaitCount(); + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ObjectNative_GetMonitorWaitCount")] + private static partial long GetWaitCount(); } } diff --git a/src/coreclr/classlibnative/bcltype/objectnative.cpp b/src/coreclr/classlibnative/bcltype/objectnative.cpp index 01192057756d72..dcd7a7b789243f 100644 --- a/src/coreclr/classlibnative/bcltype/objectnative.cpp +++ b/src/coreclr/classlibnative/bcltype/objectnative.cpp @@ -375,3 +375,17 @@ extern "C" INT64 QCALLTYPE ObjectNative_GetMonitorLockContentionCount() END_QCALL; return result; } + +extern "C" INT64 QCALLTYPE ObjectNative_GetMonitorWaitCount() +{ + QCALL_CONTRACT; + + INT64 result = 0; + + BEGIN_QCALL; + + result = (INT64)Thread::GetTotalMonitorWaitCount(); + + END_QCALL; + return result; +} diff --git a/src/coreclr/classlibnative/bcltype/objectnative.h b/src/coreclr/classlibnative/bcltype/objectnative.h index 819469a9834583..8c8f06dfdaa101 100644 --- a/src/coreclr/classlibnative/bcltype/objectnative.h +++ b/src/coreclr/classlibnative/bcltype/objectnative.h @@ -41,4 +41,5 @@ class ObjectNative }; extern "C" INT64 QCALLTYPE ObjectNative_GetMonitorLockContentionCount(); +extern "C" INT64 QCALLTYPE ObjectNative_GetMonitorWaitCount(); #endif // _OBJECTNATIVE_H_ diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs index c8fdbb60384348..89045c93036f29 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Condition.cs @@ -28,6 +28,9 @@ private static Waiter GetWaiterForCurrentThread() return waiter; } + private static long _waitCount; + internal static long WaitCount => _waitCount; + private readonly Lock _lock; private Waiter? _waitersHead; private Waiter? _waitersTail; @@ -111,6 +114,7 @@ public unsafe bool Wait(int millisecondsTimeout) bool success = false; try { + Interlocked.Increment(ref _waitCount); success = waiter.ev.WaitOne(millisecondsTimeout); } finally diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs index 66016246231fe6..77bb36f2b5886c 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Monitor.NativeAot.cs @@ -172,6 +172,11 @@ public static void PulseAll(object obj) /// public static long LockContentionCount => Lock.ContentionCount; + /// + /// Gets the number of times there was a pause upon using 's wait so far. + /// + public static long WaitCount => Condition.WaitCount; + #endregion } } diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index 3682a4cef1691d..15cd270f02c414 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -244,6 +244,7 @@ static const Entry s_QCall[] = DllImportEntry(FileLoadException_GetMessageForHR) DllImportEntry(Interlocked_MemoryBarrierProcessWide) DllImportEntry(ObjectNative_GetMonitorLockContentionCount) + DllImportEntry(ObjectNative_GetMonitorWaitCount) DllImportEntry(ReflectionInvocation_RunClassConstructor) DllImportEntry(ReflectionInvocation_RunModuleConstructor) DllImportEntry(ReflectionInvocation_CompileMethod) diff --git a/src/coreclr/vm/syncblk.cpp b/src/coreclr/vm/syncblk.cpp index 13ab5daf4aa836..b63627770a7570 100644 --- a/src/coreclr/vm/syncblk.cpp +++ b/src/coreclr/vm/syncblk.cpp @@ -2851,6 +2851,7 @@ BOOL SyncBlock::Wait(INT32 timeOut) OBJECTREF obj = m_Monitor.GetOwningObject(); + Thread::IncrementMonitorWaitCount(pCurThread); m_Monitor.IncrementTransientPrecious(); // While we are in this frame the thread is considered blocked on the diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index fccbcc7bb3bfb1..7135a52c05152f 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -133,6 +133,7 @@ PTR_ThreadLocalModule ThreadLocalBlock::GetTLMIfExists(MethodTable* pMT) BOOL Thread::s_fCleanFinalizedThread = FALSE; UINT64 Thread::s_monitorLockContentionCountOverflow = 0; +UINT64 Thread::s_monitorWaitCountOverflow = 0; CrstStatic g_DeadlockAwareCrst; @@ -5316,6 +5317,9 @@ BOOL ThreadStore::RemoveThread(Thread *target) InterlockedExchangeAdd64( (LONGLONG *)&Thread::s_monitorLockContentionCountOverflow, target->m_monitorLockContentionCount); + InterlockedExchangeAdd64( + (LONGLONG *)&Thread::s_monitorWaitCountOverflow, + target->m_monitorWaitCount); _ASSERTE(s_pThreadStore->m_ThreadCount >= 0); _ASSERTE(s_pThreadStore->m_BackgroundThreadCount >= 0); diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index f69d3df1e7493c..43492433d38ea6 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3426,7 +3426,9 @@ class Thread private: UINT32 m_monitorLockContentionCount; + UINT32 m_monitorWaitCount; static UINT64 s_monitorLockContentionCountOverflow; + static UINT64 s_monitorWaitCountOverflow; #ifndef DACCESS_COMPILE private: @@ -3495,6 +3497,24 @@ class Thread WRAPPER_NO_CONTRACT; return GetTotalCount(offsetof(Thread, m_monitorLockContentionCount), &s_monitorLockContentionCountOverflow); } + + static void IncrementMonitorWaitCount(Thread *pThread) + { + WRAPPER_NO_CONTRACT; + IncrementCount(pThread, offsetof(Thread, m_monitorWaitCount), &s_monitorWaitCountOverflow); + } + + static UINT64 GetMonitorWaitCountOverflow() + { + WRAPPER_NO_CONTRACT; + return GetOverflowCount(&s_monitorWaitCountOverflow); + } + + static UINT64 GetTotalMonitorWaitCount() + { + WRAPPER_NO_CONTRACT; + return GetTotalCount(offsetof(Thread, m_monitorWaitCount), &s_monitorWaitCountOverflow); + } #endif // !DACCESS_COMPILE public: diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/RuntimeEventSource.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/RuntimeEventSource.cs index 649ef854a0f38d..4c87aa8d2136df 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/RuntimeEventSource.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/RuntimeEventSource.cs @@ -32,6 +32,7 @@ public static class Keywords private PollingCounter? _workingSetCounter; private PollingCounter? _threadPoolThreadCounter; private IncrementingPollingCounter? _monitorContentionCounter; + private IncrementingPollingCounter? _monitorWaitCounter; private PollingCounter? _threadPoolQueueCounter; private IncrementingPollingCounter? _completedItemsCounter; private IncrementingPollingCounter? _allocRateCounter; @@ -106,6 +107,7 @@ protected override void OnEventCommand(EventCommandEventArgs command) _gen0BudgetCounter ??= new PollingCounter("gen-0-gc-budget", this, () => GC.GetGenerationBudget(0) / 1_000_000) { DisplayName = "Gen 0 GC Budget", DisplayUnits = "MB" }; _threadPoolThreadCounter ??= new PollingCounter("threadpool-thread-count", this, () => ThreadPool.ThreadCount) { DisplayName = "ThreadPool Thread Count" }; _monitorContentionCounter ??= new IncrementingPollingCounter("monitor-lock-contention-count", this, () => Monitor.LockContentionCount) { DisplayName = "Monitor Lock Contention Count", DisplayRateTimeScale = new TimeSpan(0, 0, 1) }; + _monitorWaitCounter ??= new IncrementingPollingCounter("monitor-wait-count", this, () => Monitor.WaitCount) { DisplayName = "Monitor Wait Count", DisplayRateTimeScale = new TimeSpan(0, 0, 1) }; _threadPoolQueueCounter ??= new PollingCounter("threadpool-queue-length", this, () => ThreadPool.PendingWorkItemCount) { DisplayName = "ThreadPool Queue Length" }; _completedItemsCounter ??= new IncrementingPollingCounter("threadpool-completed-items-count", this, () => ThreadPool.CompletedWorkItemCount) { DisplayName = "ThreadPool Completed Work Item Count", DisplayRateTimeScale = new TimeSpan(0, 0, 1) }; _allocRateCounter ??= new IncrementingPollingCounter("alloc-rate", this, () => GC.GetTotalAllocatedBytes()) { DisplayName = "Allocation Rate", DisplayUnits = "B", DisplayRateTimeScale = new TimeSpan(0, 0, 1) }; diff --git a/src/libraries/System.Threading/ref/System.Threading.cs b/src/libraries/System.Threading/ref/System.Threading.cs index 765875597415b2..c67441317b7d33 100644 --- a/src/libraries/System.Threading/ref/System.Threading.cs +++ b/src/libraries/System.Threading/ref/System.Threading.cs @@ -329,6 +329,7 @@ public void Wait(System.Threading.CancellationToken cancellationToken) { } public static partial class Monitor { public static long LockContentionCount { get { throw null; } } + public static long WaitCount { get { throw null; } } public static void Enter(object obj) { } public static void Enter(object obj, ref bool lockTaken) { } public static void Exit(object obj) { } diff --git a/src/libraries/System.Threading/tests/MonitorTests.cs b/src/libraries/System.Threading/tests/MonitorTests.cs index ca8ebe9544be1c..ab40301d5e2183 100644 --- a/src/libraries/System.Threading/tests/MonitorTests.cs +++ b/src/libraries/System.Threading/tests/MonitorTests.cs @@ -457,6 +457,14 @@ public static void Enter_HasToWait_LockContentionCountTest() Assert.True(Monitor.LockContentionCount - initialLockContentionCount >= 2); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + public static void WaitTest_WaitCountTest() + { + long initialWaitCount = Monitor.WaitCount; + WaitTest(); + Assert.True(Monitor.WaitCount - initialWaitCount >= 4); + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] public static void ObjectHeaderSyncBlockTransitionTryEnterRaceTest() { diff --git a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs index 1765124e341d74..df2bd878fb7da9 100644 --- a/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs +++ b/src/mono/System.Private.CoreLib/src/System/Threading/Monitor.Mono.cs @@ -149,5 +149,10 @@ private static void ReliableEnterTimeout(object obj, int timeout, ref bool lockT [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern long Monitor_get_lock_contention_count(); + + public static long WaitCount => Monitor_get_wait_count() + Condition.WaitCount; + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + private static extern long Monitor_get_wait_count(); } } diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index b18d285c421ca0..f5b7510d9d91bd 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -590,6 +590,7 @@ ICALL_TYPE(MONIT, "System.Threading.Monitor", MONIT_0) HANDLES(MONIT_0, "Enter", ves_icall_System_Threading_Monitor_Monitor_Enter, void, 1, (MonoObject)) HANDLES(MONIT_1, "InternalExit", mono_monitor_exit_icall, void, 1, (MonoObject)) NOHANDLES(ICALL(MONIT_8, "Monitor_get_lock_contention_count", ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count)) +NOHANDLES(ICALL(MONIT_8, "Monitor_get_wait_count", ves_icall_System_Threading_Monitor_Monitor_get_wait_count)) HANDLES(MONIT_2, "Monitor_pulse", ves_icall_System_Threading_Monitor_Monitor_pulse, void, 1, (MonoObject)) HANDLES(MONIT_3, "Monitor_pulse_all", ves_icall_System_Threading_Monitor_Monitor_pulse_all, void, 1, (MonoObject)) HANDLES(MONIT_7, "Monitor_wait", ves_icall_System_Threading_Monitor_Monitor_wait, MonoBoolean, 3, (MonoObject, guint32, MonoBoolean)) diff --git a/src/mono/mono/metadata/monitor.c b/src/mono/mono/metadata/monitor.c index 6a72695e9fbedc..242230d2871712 100644 --- a/src/mono/mono/metadata/monitor.c +++ b/src/mono/mono/metadata/monitor.c @@ -787,6 +787,7 @@ signal_monitor (gpointer mon_untyped) } static gint64 thread_contentions; /* for Monitor.LockContentionCount */ +static gint64 thread_waits; /* for Monitor.WaitCount */ /* If allow_interruption==TRUE, the method will be interrupted if abort or suspend * is requested. In this case it returns -1. @@ -1340,6 +1341,8 @@ mono_monitor_wait (MonoObjectHandle obj_handle, guint32 ms, MonoBoolean allow_in LOCK_DEBUG (g_message ("%s: (%d) Unlocked %p lock %p", __func__, id, obj, mon)); + mono_atomic_inc_i64 (&thread_waits); + /* There's no race between unlocking mon and waiting for the * event, because auto reset events are sticky, and this event * is private to this thread. Therefore even if the event was @@ -1438,3 +1441,9 @@ ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count (void) { return thread_contentions; } + +gint64 +ves_icall_System_Threading_Monitor_Monitor_get_wait_count (void) +{ + return thread_waits; +} diff --git a/src/mono/mono/metadata/monitor.h b/src/mono/mono/metadata/monitor.h index d1900a889976da..3ed0bbd77e6329 100644 --- a/src/mono/mono/metadata/monitor.h +++ b/src/mono/mono/metadata/monitor.h @@ -131,6 +131,10 @@ ICALL_EXPORT gint64 ves_icall_System_Threading_Monitor_Monitor_get_lock_contention_count (void); +ICALL_EXPORT +gint64 +ves_icall_System_Threading_Monitor_Monitor_get_wait_count (void); + #ifdef HOST_WASM void mono_set_string_interned_internal (MonoObject* obj); diff --git a/src/tests/tracing/eventcounter/runtimecounters.cs b/src/tests/tracing/eventcounter/runtimecounters.cs index c89767a9fae185..0ee9cc51491ce8 100644 --- a/src/tests/tracing/eventcounter/runtimecounters.cs +++ b/src/tests/tracing/eventcounter/runtimecounters.cs @@ -28,6 +28,7 @@ public RuntimeCounterListener() { "gen-2-gc-count", false }, { "threadpool-thread-count", false }, { "monitor-lock-contention-count", false }, + { "monitor-wait-count", false }, { "threadpool-queue-length", false }, { "threadpool-completed-items-count", false }, { "alloc-rate", false },