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

Use new .NET Core 3.0 API to get the total number of allocated bytes for all threads #1155

Merged
merged 3 commits into from
May 17, 2019
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
19 changes: 16 additions & 3 deletions src/BenchmarkDotNet/Engines/GcStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public struct GcStats

public static readonly long AllocationQuantum = CalculateAllocationQuantumSize();

private static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = GetAllocatedBytesForCurrentThread();
private static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
private static readonly Func<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();

public static readonly GcStats Empty = new GcStats(0, 0, 0, 0, 0);

Expand Down Expand Up @@ -143,19 +144,31 @@ private static long GetAllocatedBytes()
if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package
return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize;

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();
}

private static Func<long> GetAllocatedBytesForCurrentThread()
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;
}


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);

// we create delegate to avoid boxing, IMPORTANT!
return method != null ? (Func<bool, long>)method.CreateDelegate(typeof(Func<bool, long>)) : null;
}

public string ToOutputLine()
=> $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes} {TotalOperations}";

Expand Down
46 changes: 45 additions & 1 deletion tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public static IEnumerable<object[]> GetToolchains()
: new[]
{
new object[] { Job.Default.GetToolchain() },
// new object[] { InProcessToolchain.Instance }, // this test takes a LOT of time and since we have new InProcessEmitToolchain we can disable it
new object[] { InProcessEmitToolchain.Instance },
#if NETCOREAPP2_1
// we don't want to test CoreRT twice (for .NET 4.6 and Core 2.1) when running the integration tests (these tests take a lot of time)
Expand Down Expand Up @@ -211,6 +210,51 @@ public void AllocationQuantumIsNotAnIssueForNetCore21Plus(IToolchain toolchain)
});
}

public class MultiThreadedAllocation
{
public const int Size = 1_000_000;
public const int ThreadsCount = 10;

private Thread[] threads;

[IterationSetup]
public void SetupIteration()
{
threads = Enumerable.Range(0, ThreadsCount)
.Select(_ => new Thread(() => GC.KeepAlive(new byte[Size])))
.ToArray();
}

[Benchmark]
public void Allocate()
{
foreach (var thread in threads)
{
thread.Start();
thread.Join();
}
}
}

[TheoryNetCore30(".NET Core 3.0 preview6+ exposes a GC.GetTotalAllocatedBytes method which makes it possible to work"), MemberData(nameof(GetToolchains))]
[Trait(Constants.Category, Constants.BackwardCompatibilityCategory)]
public void MemoryDiagnoserIsAccurateForMultiThreadedBenchmarks(IToolchain toolchain)
{
if (toolchain is CoreRtToolchain) // the API has not been yet ported to CoreRT
return;

long objectAllocationOverhead = IntPtr.Size * 2; // pointer to method table + object header word
long arraySizeOverhead = IntPtr.Size; // array length
long memoryAllocatedPerArray = (MultiThreadedAllocation.Size + objectAllocationOverhead + arraySizeOverhead);
long threadStartAndJoinOverhead = 112; // this is more or less a magic number taken from memory profiler
long allocatedMemoryPerThread = memoryAllocatedPerArray + threadStartAndJoinOverhead;

AssertAllocations(toolchain, typeof(MultiThreadedAllocation), new Dictionary<string, long>
{
{ nameof(MultiThreadedAllocation.Allocate), allocatedMemoryPerThread * MultiThreadedAllocation.ThreadsCount }
});
}

private void AssertAllocations(IToolchain toolchain, Type benchmarkType, Dictionary<string, long> benchmarksAllocationsValidators)
{
var config = CreateConfig(toolchain);
Expand Down
15 changes: 15 additions & 0 deletions tests/BenchmarkDotNet.Tests/XUnit/TheoryNetCore30Attribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using BenchmarkDotNet.Toolchains.DotNetCli;
using Xunit;

namespace BenchmarkDotNet.Tests.XUnit
{
public class TheoryNetCore30Attribute : TheoryAttribute
{
// ReSharper disable once VirtualMemberCallInConstructor
public TheoryNetCore30Attribute(string skipReason)
{
if (NetCoreAppSettings.GetCurrentVersion() != NetCoreAppSettings.NetCoreApp30)
Skip = skipReason;
}
}
}