Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable MemoryDiagnoser on Legacy Mono #2459

Merged
merged 8 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
MichalPetryka marked this conversation as resolved.
Show resolved Hide resolved
{
// 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