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: diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index a156c88161..ca60b0deea 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -14,11 +14,6 @@ public struct GcStats : IEquatable public static readonly long AllocationQuantum = CalculateAllocationQuantumSize(); -#if !NET6_0_OR_GREATER - private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); - private static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); -#endif - public static readonly GcStats Empty = default; private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long? allocatedBytes, long totalOperations) @@ -143,9 +138,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,36 +147,20 @@ 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 + 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 - // https://apisof.net/catalog/System.GC.GetAllocatedBytesForCurrentThread() is not part of the .NET Standard, so we use reflection to call it.. - return GetAllocatedBytesForCurrentThreadDelegate.Invoke(); -#endif - } - - 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); - - // we create delegate to avoid boxing, IMPORTANT! - return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; - } + 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; - private static Func CreateGetTotalAllocatedBytesDelegate() - { - // 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 (GcHelpers.GetAllocatedBytesForCurrentThreadDelegate != null) + return GcHelpers.GetAllocatedBytesForCurrentThreadDelegate.Invoke(); - // we create delegate to avoid boxing, IMPORTANT! - return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; + return null; +#endif } public string ToOutputLine() @@ -260,5 +236,81 @@ 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 + // 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 + 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 } -} \ No newline at end of file +} diff --git a/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs index 62992f07b8..be8a8141e4 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 }; }