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

built-in accurate and cross platform Memory Diagnoser #284

Merged
merged 14 commits into from
Nov 25, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
built-in accurate and cross platform Memory Diagnoser, fixes #186, fixes
  • Loading branch information
adamsitnik committed Oct 16, 2016
commit 23f3b29b4cf0c13f49f47609b26b32a30d10289e
2 changes: 1 addition & 1 deletion BenchmarkDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchmarkDotNet.Integration
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BenchmarkDotNet.Samples.FSharp", "samples\BenchmarkDotNet.Samples.FSharp\BenchmarkDotNet.Samples.FSharp.fsproj", "{A329F00E-4B9D-4BC6-B688-92698D773CBF}"
ProjectSection(ProjectDependencies) = postProject
{95F5D645-19E3-432F-95D4-C5EA374DD15B} = {95F5D645-19E3-432F-95D4-C5EA374DD15B}
{AF1E6F8A-5C63-465F-96F4-5E5F183A33B9} = {AF1E6F8A-5C63-465F-96F4-5E5F183A33B9}
EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "BenchmarkDotNet.IntegrationTests.FSharp", "tests\BenchmarkDotNet.IntegrationTests.FSharp\BenchmarkDotNet.IntegrationTests.FSharp.fsproj", "{367FAFE1-A1C8-4AA1-9334-F4762E128DBB}"
Expand Down
4 changes: 1 addition & 3 deletions samples/BenchmarkDotNet.Samples/Intro/IntroGcMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace BenchmarkDotNet.Samples.Intro
{
[Config(typeof(Config))]
[OrderProvider(SummaryOrderPolicy.FastestToSlowest)]
[MemoryDiagnoser]
public class IntroGcMode
{
private class Config : ManualConfig
Expand All @@ -18,9 +19,6 @@ public Config()
Add(Job.MediumRun.WithGcServer(true).WithGcForce(false).WithId("Server"));
Add(Job.MediumRun.WithGcServer(false).WithGcForce(true).WithId("Workstation"));
Add(Job.MediumRun.WithGcServer(false).WithGcForce(false).WithId("WorkstationForce"));
#if !CORE
Add(new Diagnostics.Windows.MemoryDiagnoser());
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
using System;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;

namespace BenchmarkDotNet.Diagnostics.Windows.Configs
namespace BenchmarkDotNet.Attributes
{
public class MemoryDiagnoserAttribute : Attribute, IConfigSource
{
public IConfig Config { get; }

public MemoryDiagnoserAttribute()
{
Config = ManualConfig.CreateEmpty().With(new MemoryDiagnoser());
Config = ManualConfig.CreateEmpty().With(MemoryDiagnoser.Default);
}
}
}
5 changes: 4 additions & 1 deletion src/BenchmarkDotNet.Core/Configs/DefaultConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ public IEnumerable<IValidator> GetValidators()

public bool KeepBenchmarkFiles => false;

public IEnumerable<IDiagnoser> GetDiagnosers() => Enumerable.Empty<IDiagnoser>();
public IEnumerable<IDiagnoser> GetDiagnosers()
{
yield return MemoryDiagnoser.Default;
}

// Make the Diagnosers lazy-loaded, so they are only instantiated if neededs
public static readonly Lazy<IDiagnoser[]> LazyLoadedDiagnosers =
Expand Down
2 changes: 1 addition & 1 deletion src/BenchmarkDotNet.Core/Diagnosers/CompositeDiagnoser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class CompositeDiagnoser : IDiagnoser

public CompositeDiagnoser(params IDiagnoser[] diagnosers)
{
this.diagnosers = diagnosers;
this.diagnosers = diagnosers.Distinct().ToArray();
}

public IColumnProvider GetColumnProvider()
Expand Down
110 changes: 110 additions & 0 deletions src/BenchmarkDotNet.Core/Diagnosers/MemoryDiagnoser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Collections.Generic;
using System.Diagnostics;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Loggers;
using BenchmarkDotNet.Reports;
using BenchmarkDotNet.Running;
using System.Linq;
using BenchmarkDotNet.Engines;

namespace BenchmarkDotNet.Diagnosers
{
public class MemoryDiagnoser : IDiagnoser
{
public static readonly MemoryDiagnoser Default = new MemoryDiagnoser();

private readonly Dictionary<Benchmark, GcStats> results = new Dictionary<Benchmark, GcStats>();

public IColumnProvider GetColumnProvider() => new SimpleColumnProvider(
new GCCollectionColumn(results, 0),
new GCCollectionColumn(results, 1),
new GCCollectionColumn(results, 2),
new AllocationColumn(results));

// the methods are left empty on purpose
// the action takes places in other process, and the values are gathered by Engine
public void BeforeAnythingElse(Process process, Benchmark benchmark) { }
public void AfterSetup(Process process, Benchmark benchmark) { }
public void BeforeCleanup() { }

public void ProcessResults(Benchmark benchmark, BenchmarkReport report)
{
results.Add(benchmark, report.GcStats);
}

public void DisplayResults(ILogger logger) { }

public class AllocationColumn : IColumn
{
private readonly Dictionary<Benchmark, GcStats> results;

public AllocationColumn(Dictionary<Benchmark, GcStats> results)
{
this.results = results;
}

public string Id => nameof(AllocationColumn);
public string ColumnName => "Bytes Allocated/Op";
public bool IsDefault(Summary summary, Benchmark benchmark) => false;
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
public ColumnCategory Category => ColumnCategory.Diagnoser;
public int PriorityInCategory => 0;

public string GetValue(Summary summary, Benchmark benchmark)
{
#if !CORE
if (results.ContainsKey(benchmark))
{
var result = results[benchmark];
// TODO scale this based on the minimum value in the column, i.e. use B/KB/MB as appropriate

This comment was marked as spam.

return (result.AllocatedBytes / result.TotalOperations).ToString("N2", HostEnvironmentInfo.MainCultureInfo);
}
return "N/A";
#else
return "?";
#endif
}
}

public class GCCollectionColumn : IColumn
{
private Dictionary<Benchmark, GcStats> results;
private int generation;
// TODO also need to find a sensible way of including this in the column name?
private long opsPerGCCount;

public GCCollectionColumn(Dictionary<Benchmark, GcStats> results, int generation)
{
ColumnName = $"Gen {generation}";
this.results = results;
this.generation = generation;
opsPerGCCount = results.Any() ? results.Min(r => r.Value.TotalOperations) : 1;
}

public bool IsDefault(Summary summary, Benchmark benchmark) => true;
public string Id => $"{nameof(GCCollectionColumn)}{generation}";
public string ColumnName { get; }
public bool IsAvailable(Summary summary) => true;
public bool AlwaysShow => true;
public ColumnCategory Category => ColumnCategory.Diagnoser;
public int PriorityInCategory => 0;

public string GetValue(Summary summary, Benchmark benchmark)
{
if (results.ContainsKey(benchmark))
{
var result = results[benchmark];
var value = generation == 0 ? result.Gen0Collections :
generation == 1 ? result.Gen1Collections : result.Gen2Collections;

if (value == 0)
return "-"; // make zero more obvious
return (value / (double)result.TotalOperations * opsPerGCCount).ToString("N2", HostEnvironmentInfo.MainCultureInfo);
}
return "N/A";
}
}
}
}
24 changes: 22 additions & 2 deletions src/BenchmarkDotNet.Core/Engines/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class Engine : IEngine
private readonly EngineWarmupStage warmupStage;
private readonly EngineTargetStage targetStage;
private bool isJitted, isPreAllocated;
private int forcedFullGarbageCollections;

internal Engine(Action<long> idleAction, Action<long> mainAction, Job targetJob, Action setupAction, Action cleanupAction, long operationsPerInvoke, bool isDiagnoserAttached)
{
Expand Down Expand Up @@ -98,9 +99,17 @@ public RunResults Run()
warmupStage.RunMain(invokeCount, UnrollFactor);
}

// we enable monitoring after pilot & warmup, just to ignore the memory allocated by these runs
EnableMonitoring();
var initialGcStats = GcStats.ReadInitial(IsDiagnoserAttached);

var main = targetStage.RunMain(invokeCount, UnrollFactor);

return new RunResults(idle, main);
var finalGcStats = GcStats.ReadFinal(IsDiagnoserAttached);
var forcedCollections = GcStats.FromForced(forcedFullGarbageCollections);
var workGcHasDone = finalGcStats - forcedCollections - initialGcStats;

return new RunResults(idle, main, workGcHasDone);
}

public Measurement RunIteration(IterationData data)
Expand Down Expand Up @@ -135,11 +144,13 @@ private void GcCollect()
ForceGcCollect();
}

private static void ForceGcCollect()
private void ForceGcCollect()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

forcedFullGarbageCollections += 2;
}

public void WriteLine(string text)
Expand All @@ -156,6 +167,15 @@ public void WriteLine()
Console.WriteLine();
}

private void EnableMonitoring()
{
if(!IsDiagnoserAttached) // it could affect the results, we do this in separate, diagnostics-only run
return;
#if CLASSIC
AppDomain.MonitoringIsEnabled = true;
#endif
}

private void EnsureNothingIsPrintedWhenDiagnoserIsAttached()
{
if (IsDiagnoserAttached)
Expand Down
120 changes: 120 additions & 0 deletions src/BenchmarkDotNet.Core/Engines/GcStats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;

namespace BenchmarkDotNet.Engines
{
public struct GcStats
{
internal const string ResultsLinePrefix = "GC: ";

private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long allocatedBytes, long totalOperations)
{
Gen0Collections = gen0Collections;
Gen1Collections = gen1Collections;
Gen2Collections = gen2Collections;
AllocatedBytes = allocatedBytes;
TotalOperations = totalOperations;
}

// did not use array here just to avoid heap allocation
public int Gen0Collections { get; }
public int Gen1Collections { get; }
public int Gen2Collections { get; }
public long AllocatedBytes { get; }
public long TotalOperations { get; }

public static GcStats operator +(GcStats left, GcStats right)
{
return new GcStats(
left.Gen0Collections + right.Gen0Collections,
left.Gen1Collections + right.Gen1Collections,
left.Gen2Collections + right.Gen2Collections,
left.AllocatedBytes + right.AllocatedBytes,
left.TotalOperations + right.TotalOperations);
}

public static GcStats operator -(GcStats left, GcStats right)
{
return new GcStats(
Math.Max(0, left.Gen0Collections - right.Gen0Collections),
Math.Max(0, left.Gen1Collections - right.Gen1Collections),
Math.Max(0, left.Gen2Collections - right.Gen2Collections),
Math.Max(0, left.AllocatedBytes - right.AllocatedBytes),
Math.Max(0, left.TotalOperations - right.TotalOperations));
}

public GcStats WithTotalOperations(long totalOperationsCount)
=> this + new GcStats(0, 0, 0, 0, totalOperationsCount);

internal static GcStats ReadInitial(bool isDiagnosticsEnabled)
{
// this might force GC.Collect, so we want to do this before collecting collections counts
long allocatedBytes = GetAllocatedBytes(isDiagnosticsEnabled);

return new GcStats(
GC.CollectionCount(0),
GC.CollectionCount(1),
GC.CollectionCount(2),
allocatedBytes,
0);
}

internal static GcStats ReadFinal(bool isDiagnosticsEnabled)
{
return new GcStats(
GC.CollectionCount(0),
GC.CollectionCount(1),
GC.CollectionCount(2),

// this might force GC.Collect, so we want to do this after collecting collections counts
// to exclude this single full forced collection from results
GetAllocatedBytes(isDiagnosticsEnabled),
0);
}

public static GcStats FromForced(int forcedFullGarbageCollections)
=> new GcStats(forcedFullGarbageCollections, forcedFullGarbageCollections, forcedFullGarbageCollections, 0, 0);

private static long GetAllocatedBytes(bool isDiagnosticsEnabled)
{
#if NETCOREAPP11 // when MS releases new version of .NET Runtime to nuget.org
return GC.GetAllocatedBytesForCurrentThread(); // https://github.com/dotnet/corefx/pull/12489
#elif CLASSIC
if (!isDiagnosticsEnabled)
return 0;

// "This instance Int64 property returns the number of bytes that have been allocated by a specific
// AppDomain. The number is accurate as of the last garbage collection." - CLR via C#
// so we enforce GC.Collect here just to make sure we get accurate results
GC.Collect();
return AppDomain.CurrentDomain.MonitoringTotalAllocatedMemorySize;

This comment was marked as spam.

This comment was marked as spam.

#else
return 0; // currently for .NET Core
#endif
}

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

internal static GcStats Parse(string line)
{
if(!line.StartsWith(ResultsLinePrefix))
throw new NotSupportedException($"Line must start with {ResultsLinePrefix}");

int gen0, gen1, gen2;
long allocatedBytes, totalOperationsCount;
var measurementSplit = line.Remove(0, ResultsLinePrefix.Length).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (!int.TryParse(measurementSplit[0], out gen0)
|| !int.TryParse(measurementSplit[1], out gen1)
|| !int.TryParse(measurementSplit[2], out gen2)
|| !long.TryParse(measurementSplit[3], out allocatedBytes)
|| !long.TryParse(measurementSplit[4], out totalOperationsCount))
{
throw new NotSupportedException("Invalid string");
}

return new GcStats(gen0, gen1, gen2, allocatedBytes, totalOperationsCount);
}

public override string ToString() => ToOutputLine();
}
}
9 changes: 8 additions & 1 deletion src/BenchmarkDotNet.Core/Engines/RunResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ public struct RunResults
{
public List<Measurement> Idle { get; }
public List<Measurement> Main { get; }
public GcStats GCStats { get; }

public RunResults(List<Measurement> idle, List<Measurement> main)
public RunResults(List<Measurement> idle, List<Measurement> main, GcStats gcStats)
{
Idle = idle;
Main = main;
GCStats = gcStats;
}

public void Print()
Expand All @@ -23,8 +25,12 @@ public void Print()
// TODO: check if resulted measurements are too small (like < 0.1ns)
double overhead = Idle == null ? 0.0 : new Statistics(Idle.Select(m => m.Nanoseconds)).Mean;
int resultIndex = 0;
long totalOperationsCount = 0;
foreach (var measurement in Main)
{
if (!measurement.IterationMode.IsIdle())
totalOperationsCount += measurement.Operations;

var resultMeasurement = new Measurement(
measurement.LaunchIndex,
IterationMode.Result,
Expand All @@ -33,6 +39,7 @@ public void Print()
Math.Max(0, measurement.Nanoseconds - overhead));
WriteLine(resultMeasurement.ToOutputLine());
}
WriteLine(GCStats.WithTotalOperations(totalOperationsCount).ToOutputLine());
WriteLine();
}

Expand Down
Loading