Skip to content

Commit

Permalink
Enable MemoryDiagnoser on Legacy Mono (#2459)
Browse files Browse the repository at this point in the history
* Enable MemoryDiagnoser on Legacy Mono

This enables MemoryDiagnoser on Legacy Mono and makes it more resilient to errors.

* Update diagnosers.md

* Enable tests

* Update GcStats.cs

* Update GcStats.cs

* Update GcStats.cs

* Update GcStats.cs

* Update src/BenchmarkDotNet/Engines/GcStats.cs

Co-authored-by: Tim Cassell <35501420+timcassell@users.noreply.github.com>

---------

Co-authored-by: Tim Cassell <35501420+timcassell@users.noreply.github.com>
  • Loading branch information
MichalPetryka and timcassell authored Nov 15, 2023
1 parent e903115 commit fc7afed
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 37 deletions.
1 change: 0 additions & 1 deletion docs/articles/configs/diagnosers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
118 changes: 85 additions & 33 deletions src/BenchmarkDotNet/Engines/GcStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ public struct GcStats : IEquatable<GcStats>

public static readonly long AllocationQuantum = CalculateAllocationQuantumSize();

#if !NET6_0_OR_GREATER
private static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
private static readonly Func<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();
#endif

public static readonly GcStats Empty = default;

private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long? allocatedBytes, long totalOperations)
Expand Down Expand Up @@ -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;
Expand All @@ -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<long> 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<long>)method.CreateDelegate(typeof(Func<long>)) : 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<bool, long> 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<bool, long>)method.CreateDelegate(typeof(Func<bool, long>)) : null;
return null;
#endif
}

public string ToOutputLine()
Expand Down Expand Up @@ -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<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();
public static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
public static readonly bool CanUseMonitoringTotalAllocatedMemorySize = CheckMonitoringTotalAllocatedMemorySize();

private static Func<bool, long> 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<bool, long>)method.CreateDelegate(typeof(Func<bool, long>));

// verify the api works
return del.Invoke(true) >= 0 ? del : null;
}
catch
{
return null;
}
}

private static Func<long> 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<long>)method.CreateDelegate(typeof(Func<long>));

// 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ public class MemoryDiagnoserTests

public static IEnumerable<object[]> 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 };
}
Expand Down

0 comments on commit fc7afed

Please sign in to comment.