From d6f0415f872135577eb1f461c4f5f115b1cf5a71 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 16 May 2019 16:19:39 -0700 Subject: [PATCH 1/3] add tests --- .../MemoryDiagnoserTests.cs | 46 ++++++++++++++++++- .../XUnit/TheoryNetCore30Attribute.cs | 15 ++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/BenchmarkDotNet.Tests/XUnit/TheoryNetCore30Attribute.cs diff --git a/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs b/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs index 4a912c2d35..fd98eea64b 100755 --- a/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests/MemoryDiagnoserTests.cs @@ -37,7 +37,6 @@ public static IEnumerable 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) @@ -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 + { + { nameof(MultiThreadedAllocation.Allocate), allocatedMemoryPerThread * MultiThreadedAllocation.ThreadsCount } + }); + } + private void AssertAllocations(IToolchain toolchain, Type benchmarkType, Dictionary benchmarksAllocationsValidators) { var config = CreateConfig(toolchain); diff --git a/tests/BenchmarkDotNet.Tests/XUnit/TheoryNetCore30Attribute.cs b/tests/BenchmarkDotNet.Tests/XUnit/TheoryNetCore30Attribute.cs new file mode 100644 index 0000000000..0b9d373cfa --- /dev/null +++ b/tests/BenchmarkDotNet.Tests/XUnit/TheoryNetCore30Attribute.cs @@ -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; + } + } +} \ No newline at end of file From 30b836aad399b6031639c5b1d44997ae608fc62b Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 16 May 2019 16:38:29 -0700 Subject: [PATCH 2/3] use GC.GetTotalAllocatedBytes if available, fixes #1153, fixes #723 --- src/BenchmarkDotNet/Engines/GcStats.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 59509c9aa8..2acf64638c 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -12,7 +12,8 @@ public struct GcStats public static readonly long AllocationQuantum = CalculateAllocationQuantumSize(); - private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = GetAllocatedBytesForCurrentThread(); + private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); + private static readonly Func GetGetTotalAllocatedBytesDelegate = CreateGetGetTotalAllocatedBytesDelegate(); public static readonly GcStats Empty = new GcStats(0, 0, 0, 0, 0); @@ -143,11 +144,14 @@ private static long GetAllocatedBytes() if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; + if (GetGetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available + return GetGetTotalAllocatedBytesDelegate.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 GetAllocatedBytesForCurrentThread() + 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); @@ -155,7 +159,16 @@ private static Func GetAllocatedBytesForCurrentThread() // we create delegate to avoid boxing, IMPORTANT! return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; } - + + private static Func CreateGetGetTotalAllocatedBytesDelegate() + { + // 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)method.CreateDelegate(typeof(Func)) : null; + } + public string ToOutputLine() => $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes} {TotalOperations}"; From 0abef2b17b45cb26037a139930a0b2b21db831d9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 17 May 2019 11:12:22 -0700 Subject: [PATCH 3/3] fix naming --- src/BenchmarkDotNet/Engines/GcStats.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/BenchmarkDotNet/Engines/GcStats.cs b/src/BenchmarkDotNet/Engines/GcStats.cs index 2acf64638c..76e46c83c0 100644 --- a/src/BenchmarkDotNet/Engines/GcStats.cs +++ b/src/BenchmarkDotNet/Engines/GcStats.cs @@ -13,7 +13,7 @@ public struct GcStats public static readonly long AllocationQuantum = CalculateAllocationQuantumSize(); private static readonly Func GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate(); - private static readonly Func GetGetTotalAllocatedBytesDelegate = CreateGetGetTotalAllocatedBytesDelegate(); + private static readonly Func GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate(); public static readonly GcStats Empty = new GcStats(0, 0, 0, 0, 0); @@ -144,8 +144,8 @@ private static long GetAllocatedBytes() if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize; - if (GetGetTotalAllocatedBytesDelegate != null) // it's .NET Core 3.0 with the new API available - return GetGetTotalAllocatedBytesDelegate.Invoke(true); // true for the "precise" argument + 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(); @@ -160,7 +160,7 @@ private static Func CreateGetAllocatedBytesForCurrentThreadDelegate() return method != null ? (Func)method.CreateDelegate(typeof(Func)) : null; } - private static Func CreateGetGetTotalAllocatedBytesDelegate() + 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);