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 },