From 5e2aafb93a9a77352afda40cd3f53b89c7ca38c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 02:52:17 +0100 Subject: [PATCH 1/8] Enable MemoryDiagnoser on Legacy Mono This enables MemoryDiagnoser on Legacy Mono and makes it more resilient to errors. --- src/BenchmarkDotNet/Engines/GcStats.cs | 88 ++++++++++++++++++++------ 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index a156c88161..2916ea32f9 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -15,8 +15,10 @@ public struct GcStats : IEquatable public static readonly long AllocationQuantum = CalculateAllocationQuantumSize(); #if !NET6_0_OR_GREATER - private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); + // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first private static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); + private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); + private static readonly bool CanUseMonitoringTotalAllocatedMemorySize = CheckMonitoringTotalAllocatedMemorySize(); #endif public static readonly GcStats Empty = default; @@ -143,9 +145,6 @@ public static GcStats FromForced(int forcedFullGarbageCollections) private static long? GetAllocatedBytes() { - if (RuntimeInformation.IsOldMono) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- - return null; - // we have no tests for WASM and don't want to risk introducing a new bug (https://github.com/dotnet/BenchmarkDotNet/issues/2226) if (RuntimeInformation.IsWasm) return null; @@ -155,37 +154,88 @@ public static GcStats FromForced(int forcedFullGarbageCollections) // so we enforce GC.Collect here just to make sure we get accurate results GC.Collect(); - if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package - return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; - #if NET6_0_OR_GREATER return GC.GetTotalAllocatedBytes(precise: true); #else if (GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available return GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument - // https://apisof.net/catalog/System.GC.GetAllocatedBytesForCurrentThread() is not part of the .NET Standard, so we use reflection to call it.. - return GetAllocatedBytesForCurrentThreadDelegate.Invoke(); + if (CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- + return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; + + if (GetAllocatedBytesForCurrentThreadDelegate != null) + return GetAllocatedBytesForCurrentThreadDelegate.Invoke(); + + return null; #endif } +#if !NET6_0_OR_GREATER + private static Func CreateGetTotalAllocatedBytesDelegate() + { + try + { + // this method is not a part of .NET Standard so we need to use reflection + var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static); + + if (method == null) + return null; + + // we create delegate to avoid boxing, IMPORTANT! + var del = (Func)method.CreateDelegate(typeof(Func)); + + // verify the api works + return del(true) >= 0 ? del : null; + } + catch + { + return null; + } + } + private static Func CreateGetAllocatedBytesForCurrentThreadDelegate() { - // this method is not a part of .NET Standard so we need to use reflection - var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static); + try + { + // this method is not a part of .NET Standard so we need to use reflection + var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static); - // we create delegate to avoid boxing, IMPORTANT! - return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; + if (method == null) + return null; + + // we create delegate to avoid boxing, IMPORTANT! + var del = (Func)method.CreateDelegate(typeof(Func)); + + // verify the api works + return del() >= 0 ? del : null; + } + catch + { + return null; + } } - private static Func CreateGetTotalAllocatedBytesDelegate() + private static bool CheckMonitoringTotalAllocatedMemorySize() { - // this method is not a part of .NET Standard so we need to use reflection - var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static); + try + { + // we potentially don't want to enable monitoring if we don't need it + if (GetTotalAllocatedBytesDelegate != null) + return false; + + // check if monitoring is enabled + if (!AppDomain.MonitoringIsEnabled) + AppDomain.MonitoringIsEnabled = true; - // we create delegate to avoid boxing, IMPORTANT! - return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; + // verify the api works + return AppDomain.MonitoringIsEnabled && AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize >= 0; + } + catch + { + return false; + } } +#endif public string ToOutputLine() => $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes?.ToString() ?? MetricColumn.UnknownRepresentation} {TotalOperations}"; @@ -261,4 +311,4 @@ private static long CalculateAllocationQuantumSize() public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); } -} \ No newline at end of file +} From 19f296000c406d78a0e85fc4d64bb6dcf19fab8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 02:56:40 +0100 Subject: [PATCH 2/8] Update diagnosers.md --- docs/articles/configs/diagnosers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/articles/configs/diagnosers.md b/docs/articles/configs/diagnosers.md index cfb5532a40..cb0facc3a9 100644 --- a/docs/articles/configs/diagnosers.md +++ b/docs/articles/configs/diagnosers.md @@ -86,7 +86,6 @@ In BenchmarkDotNet, 1kB = 1024B, 1MB = 1024kB, and so on. The column Gen X means * In order to not affect main results we perform a separate run if any diagnoser is used. That's why it might take more time to execute benchmarks. * MemoryDiagnoser: - * Mono currently [does not](https://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes-in-mono) expose any api to get the number of allocated bytes. That's why our Mono users will get `?` in Allocated column. * In order to get the number of allocated bytes in cross platform way we are using `GC.GetAllocatedBytesForCurrentThread` which recently got [exposed](https://github.com/dotnet/corefx/pull/12489) for netcoreapp1.1. That's why BenchmarkDotNet does not support netcoreapp1.0 from version 0.10.1. * MemoryDiagnoser is `99.5%` accurate about allocated memory when using default settings or Job.ShortRun (or any longer job than it). * Threading Diagnoser: From 9b44e2d9123a4c0b891f9aa49907c18e98426b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:46:47 +0100 Subject: [PATCH 3/8] Enable tests --- tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs index c1f2c622c6..ae2193eb16 100755 --- a/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs @@ -34,9 +34,6 @@ public class MemoryDiagnoserTests public static IEnumerable GetToolchains() { - if (RuntimeInformation.IsOldMono) // https://github.com/mono/mono/issues/8397 - yield break; - yield return new object[] { Job.Default.GetToolchain() }; yield return new object[] { InProcessEmitToolchain.Instance }; } From 96a76a8bfd546af64dc5279b87992ede23b9a384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:33:58 +0100 Subject: [PATCH 4/8] Update GcStats.cs --- src/BenchmarkDotNet/Engines/GcStats.cs | 159 +++++++++++++------------ 1 file changed, 80 insertions(+), 79 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 2916ea32f9..379e87ff69 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -14,13 +14,6 @@ public struct GcStats : IEquatable public static readonly long AllocationQuantum = CalculateAllocationQuantumSize(); -#if !NET6_0_OR_GREATER - // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first - private static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); - private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); - private static readonly bool CanUseMonitoringTotalAllocatedMemorySize = CheckMonitoringTotalAllocatedMemorySize(); -#endif - public static readonly GcStats Empty = default; private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long? allocatedBytes, long totalOperations) @@ -157,86 +150,19 @@ public static GcStats FromForced(int forcedFullGarbageCollections) #if NET6_0_OR_GREATER return GC.GetTotalAllocatedBytes(precise: true); #else - if (GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available - return GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument + if (ReflectionHelper.GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available + return ReflectionHelper.GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument - if (CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- + if (ReflectionHelper.CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; - if (GetAllocatedBytesForCurrentThreadDelegate != null) - return GetAllocatedBytesForCurrentThreadDelegate.Invoke(); + if (ReflectionHelper.GetAllocatedBytesForCurrentThreadDelegate != null) + return ReflectionHelper.GetAllocatedBytesForCurrentThreadDelegate.Invoke(); return null; #endif } -#if !NET6_0_OR_GREATER - private static Func CreateGetTotalAllocatedBytesDelegate() - { - try - { - // this method is not a part of .NET Standard so we need to use reflection - var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static); - - if (method == null) - return null; - - // we create delegate to avoid boxing, IMPORTANT! - var del = (Func)method.CreateDelegate(typeof(Func)); - - // verify the api works - return del(true) >= 0 ? del : null; - } - catch - { - return null; - } - } - - private static Func CreateGetAllocatedBytesForCurrentThreadDelegate() - { - try - { - // this method is not a part of .NET Standard so we need to use reflection - var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static); - - if (method == null) - return null; - - // we create delegate to avoid boxing, IMPORTANT! - var del = (Func)method.CreateDelegate(typeof(Func)); - - // verify the api works - return del() >= 0 ? del : null; - } - catch - { - return null; - } - } - - private static bool CheckMonitoringTotalAllocatedMemorySize() - { - try - { - // we potentially don't want to enable monitoring if we don't need it - if (GetTotalAllocatedBytesDelegate != null) - return false; - - // check if monitoring is enabled - if (!AppDomain.MonitoringIsEnabled) - AppDomain.MonitoringIsEnabled = true; - - // verify the api works - return AppDomain.MonitoringIsEnabled && AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize >= 0; - } - catch - { - return false; - } - } -#endif - public string ToOutputLine() => $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes?.ToString() ?? MetricColumn.UnknownRepresentation} {TotalOperations}"; @@ -310,5 +236,80 @@ private static long CalculateAllocationQuantumSize() public override bool Equals(object obj) => obj is GcStats other && Equals(other); public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); + + #if !NET6_0_OR_GREATER + private static class ReflectionHelper + { + // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first + public static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); + public static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); + public static readonly bool CanUseMonitoringTotalAllocatedMemorySize = CheckMonitoringTotalAllocatedMemorySize(); + + private static Func CreateGetTotalAllocatedBytesDelegate() + { + try + { + // this method is not a part of .NET Standard so we need to use reflection + var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static); + + if (method == null) + return null; + + // we create delegate to avoid boxing, IMPORTANT! + var del = (Func)method.CreateDelegate(typeof(Func)); + + // verify the api works + return del.Invoke(true) >= 0 ? del : null; + } + catch + { + return null; + } + } + + private static Func CreateGetAllocatedBytesForCurrentThreadDelegate() + { + try + { + // this method is not a part of .NET Standard so we need to use reflection + var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static); + + if (method == null) + return null; + + // we create delegate to avoid boxing, IMPORTANT! + var del = (Func)method.CreateDelegate(typeof(Func)); + + // verify the api works + return del.Invoke() >= 0 ? del : null; + } + catch + { + return null; + } + } + + private static bool CheckMonitoringTotalAllocatedMemorySize() + { + try + { + // we potentially don't want to enable monitoring if we don't need it + if (GetTotalAllocatedBytesDelegate != null) + return false; + + // check if monitoring is enabled + if (!AppDomain.MonitoringIsEnabled) + AppDomain.MonitoringIsEnabled = true; + + // verify the api works + return AppDomain.MonitoringIsEnabled && AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize >= 0; + } + catch + { + return false; + } + } + } +#endif } } From bfa8952840df614da42656eba3a9f46732ed3b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:38:30 +0100 Subject: [PATCH 5/8] Update GcStats.cs --- src/BenchmarkDotNet/Engines/GcStats.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 379e87ff69..7bae24d336 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -237,7 +237,7 @@ private static long CalculateAllocationQuantumSize() public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); - #if !NET6_0_OR_GREATER +#if !NET6_0_OR_GREATER private static class ReflectionHelper { // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first From 4e2cb659ffbb955654f8eb85dea3c4cef33edaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:41:11 +0100 Subject: [PATCH 6/8] Update GcStats.cs --- src/BenchmarkDotNet/Engines/GcStats.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 7bae24d336..94b88f489b 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -150,14 +150,14 @@ public static GcStats FromForced(int forcedFullGarbageCollections) #if NET6_0_OR_GREATER return GC.GetTotalAllocatedBytes(precise: true); #else - if (ReflectionHelper.GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available - return ReflectionHelper.GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument + if (GcHelpers.GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available + return GcHelpers.GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument - if (ReflectionHelper.CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- + if (GcHelpers.CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes- return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; - if (ReflectionHelper.GetAllocatedBytesForCurrentThreadDelegate != null) - return ReflectionHelper.GetAllocatedBytesForCurrentThreadDelegate.Invoke(); + if (GcHelpers.GetAllocatedBytesForCurrentThreadDelegate != null) + return GcHelpers.GetAllocatedBytesForCurrentThreadDelegate.Invoke(); return null; #endif @@ -238,7 +238,7 @@ private static long CalculateAllocationQuantumSize() public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); #if !NET6_0_OR_GREATER - private static class ReflectionHelper + private static class GcHelpers { // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first public static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); From 41d973b6b56ea1e876f07a870f712de162d968f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Thu, 2 Nov 2023 22:02:13 +0100 Subject: [PATCH 7/8] Update GcStats.cs --- src/BenchmarkDotNet/Engines/GcStats.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 94b88f489b..66b9481588 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -238,6 +238,7 @@ private static long CalculateAllocationQuantumSize() public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); #if !NET6_0_OR_GREATER + // separate class to have the cctor run lazily private static class GcHelpers { // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first From 2be7a53e40315940ee9d60f2c9904fe5088bcf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Petryka?= <35800402+MichalPetryka@users.noreply.github.com> Date: Fri, 3 Nov 2023 17:27:59 +0100 Subject: [PATCH 8/8] Update src/BenchmarkDotNet/Engines/GcStats.cs Co-authored-by: Tim Cassell <35501420+timcassell@users.noreply.github.com> --- src/BenchmarkDotNet/Engines/GcStats.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 66b9481588..ca60b0deea 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -238,7 +238,7 @@ private static long CalculateAllocationQuantumSize() public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations); #if !NET6_0_OR_GREATER - // separate class to have the cctor run lazily + // Separate class to have the cctor run lazily, to avoid enabling monitoring before the benchmarks are ran. private static class GcHelpers { // do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first