Skip to content

Commit fc7afed

Browse files
Enable MemoryDiagnoser on Legacy Mono (#2459)
* 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 <[email protected]> --------- Co-authored-by: Tim Cassell <[email protected]>
1 parent e903115 commit fc7afed

File tree

3 files changed

+85
-37
lines changed

3 files changed

+85
-37
lines changed

docs/articles/configs/diagnosers.md

-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ In BenchmarkDotNet, 1kB = 1024B, 1MB = 1024kB, and so on. The column Gen X means
8686

8787
* 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.
8888
* MemoryDiagnoser:
89-
* 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.
9089
* 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.
9190
* MemoryDiagnoser is `99.5%` accurate about allocated memory when using default settings or Job.ShortRun (or any longer job than it).
9291
* Threading Diagnoser:

src/BenchmarkDotNet/Engines/GcStats.cs

+85-33
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ public struct GcStats : IEquatable<GcStats>
1414

1515
public static readonly long AllocationQuantum = CalculateAllocationQuantumSize();
1616

17-
#if !NET6_0_OR_GREATER
18-
private static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
19-
private static readonly Func<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();
20-
#endif
21-
2217
public static readonly GcStats Empty = default;
2318

2419
private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long? allocatedBytes, long totalOperations)
@@ -143,9 +138,6 @@ public static GcStats FromForced(int forcedFullGarbageCollections)
143138

144139
private static long? GetAllocatedBytes()
145140
{
146-
if (RuntimeInformation.IsOldMono) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes-
147-
return null;
148-
149141
// we have no tests for WASM and don't want to risk introducing a new bug (https://github.com/dotnet/BenchmarkDotNet/issues/2226)
150142
if (RuntimeInformation.IsWasm)
151143
return null;
@@ -155,36 +147,20 @@ public static GcStats FromForced(int forcedFullGarbageCollections)
155147
// so we enforce GC.Collect here just to make sure we get accurate results
156148
GC.Collect();
157149

158-
if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package
159-
return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize;
160-
161150
#if NET6_0_OR_GREATER
162151
return GC.GetTotalAllocatedBytes(precise: true);
163152
#else
164-
if (GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available
165-
return GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument
153+
if (GcHelpers.GetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available
154+
return GcHelpers.GetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument
166155

167-
// https://apisof.net/catalog/System.GC.GetAllocatedBytesForCurrentThread() is not part of the .NET Standard, so we use reflection to call it..
168-
return GetAllocatedBytesForCurrentThreadDelegate.Invoke();
169-
#endif
170-
}
171-
172-
private static Func<long> CreateGetAllocatedBytesForCurrentThreadDelegate()
173-
{
174-
// this method is not a part of .NET Standard so we need to use reflection
175-
var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static);
176-
177-
// we create delegate to avoid boxing, IMPORTANT!
178-
return method != null ? (Func<long>)method.CreateDelegate(typeof(Func<long>)) : null;
179-
}
156+
if (GcHelpers.CanUseMonitoringTotalAllocatedMemorySize) // Monitoring is not available in Mono, see http://stackoverflow.com/questions/40234948/how-to-get-the-number-of-allocated-bytes-
157+
return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize;
180158

181-
private static Func<bool, long> CreateGetTotalAllocatedBytesDelegate()
182-
{
183-
// this method is not a part of .NET Standard so we need to use reflection
184-
var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static);
159+
if (GcHelpers.GetAllocatedBytesForCurrentThreadDelegate != null)
160+
return GcHelpers.GetAllocatedBytesForCurrentThreadDelegate.Invoke();
185161

186-
// we create delegate to avoid boxing, IMPORTANT!
187-
return method != null ? (Func<bool, long>)method.CreateDelegate(typeof(Func<bool, long>)) : null;
162+
return null;
163+
#endif
188164
}
189165

190166
public string ToOutputLine()
@@ -260,5 +236,81 @@ private static long CalculateAllocationQuantumSize()
260236
public override bool Equals(object obj) => obj is GcStats other && Equals(other);
261237

262238
public override int GetHashCode() => HashCode.Combine(Gen0Collections, Gen1Collections, Gen2Collections, AllocatedBytes, TotalOperations);
239+
240+
#if !NET6_0_OR_GREATER
241+
// Separate class to have the cctor run lazily, to avoid enabling monitoring before the benchmarks are ran.
242+
private static class GcHelpers
243+
{
244+
// do not reorder these, CheckMonitoringTotalAllocatedMemorySize relies on GetTotalAllocatedBytesDelegate being initialized first
245+
public static readonly Func<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();
246+
public static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
247+
public static readonly bool CanUseMonitoringTotalAllocatedMemorySize = CheckMonitoringTotalAllocatedMemorySize();
248+
249+
private static Func<bool, long> CreateGetTotalAllocatedBytesDelegate()
250+
{
251+
try
252+
{
253+
// this method is not a part of .NET Standard so we need to use reflection
254+
var method = typeof(GC).GetTypeInfo().GetMethod("GetTotalAllocatedBytes", BindingFlags.Public | BindingFlags.Static);
255+
256+
if (method == null)
257+
return null;
258+
259+
// we create delegate to avoid boxing, IMPORTANT!
260+
var del = (Func<bool, long>)method.CreateDelegate(typeof(Func<bool, long>));
261+
262+
// verify the api works
263+
return del.Invoke(true) >= 0 ? del : null;
264+
}
265+
catch
266+
{
267+
return null;
268+
}
269+
}
270+
271+
private static Func<long> CreateGetAllocatedBytesForCurrentThreadDelegate()
272+
{
273+
try
274+
{
275+
// this method is not a part of .NET Standard so we need to use reflection
276+
var method = typeof(GC).GetTypeInfo().GetMethod("GetAllocatedBytesForCurrentThread", BindingFlags.Public | BindingFlags.Static);
277+
278+
if (method == null)
279+
return null;
280+
281+
// we create delegate to avoid boxing, IMPORTANT!
282+
var del = (Func<long>)method.CreateDelegate(typeof(Func<long>));
283+
284+
// verify the api works
285+
return del.Invoke() >= 0 ? del : null;
286+
}
287+
catch
288+
{
289+
return null;
290+
}
291+
}
292+
293+
private static bool CheckMonitoringTotalAllocatedMemorySize()
294+
{
295+
try
296+
{
297+
// we potentially don't want to enable monitoring if we don't need it
298+
if (GetTotalAllocatedBytesDelegate != null)
299+
return false;
300+
301+
// check if monitoring is enabled
302+
if (!AppDomain.MonitoringIsEnabled)
303+
AppDomain.MonitoringIsEnabled = true;
304+
305+
// verify the api works
306+
return AppDomain.MonitoringIsEnabled && AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize >= 0;
307+
}
308+
catch
309+
{
310+
return false;
311+
}
312+
}
313+
}
314+
#endif
263315
}
264-
}
316+
}

tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs

-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public class MemoryDiagnoserTests
3434

3535
public static IEnumerable<object[]> GetToolchains()
3636
{
37-
if (RuntimeInformation.IsOldMono) // https://github.com/mono/mono/issues/8397
38-
yield break;
39-
4037
yield return new object[] { Job.Default.GetToolchain() };
4138
yield return new object[] { InProcessEmitToolchain.Instance };
4239
}

0 commit comments

Comments
 (0)