Skip to content

Commit 539bdb0

Browse files
committed
Added optional includeSurvived flag on MemoryDiagnoserAttribute to capture the total application memory after the benchmark is done.
Need some way to measure the memory before a benchmark is run to subtract away the memory that is consumed by the benchmark runner itself.
1 parent 384d479 commit 539bdb0

File tree

13 files changed

+91
-19
lines changed

13 files changed

+91
-19
lines changed

src/BenchmarkDotNet/Attributes/MemoryDiagnoserAttribute.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ public class MemoryDiagnoserAttribute : Attribute, IConfigSource
99
{
1010
public IConfig Config { get; }
1111

12-
public MemoryDiagnoserAttribute()
12+
public MemoryDiagnoserAttribute(bool includeSurvived = false)
1313
{
14-
Config = ManualConfig.CreateEmpty().AddDiagnoser(MemoryDiagnoser.Default);
14+
Config = ManualConfig.CreateEmpty().AddDiagnoser(includeSurvived ? MemoryDiagnoser.WithSurvived : MemoryDiagnoser.Default);
1515
}
1616
}
1717
}

src/BenchmarkDotNet/Code/CodeGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ internal static string Generate(BuildPartition buildPartition)
6363
.Replace("$PassArguments$", passArguments)
6464
.Replace("$EngineFactoryType$", GetEngineFactoryTypeName(benchmark))
6565
.Replace("$MeasureExtraStats$", buildInfo.Config.HasExtraStatsDiagnoser() ? "true" : "false")
66+
.Replace("$MeasureSurvivedMemory$", buildInfo.Config.HasSurvivedMemoryDiagnoser() ? "true" : "false")
6667
.Replace("$DisassemblerEntryMethodName$", DisassemblerConstants.DisassemblerEntryMethodName)
6768
.Replace("$WorkloadMethodCall$", provider.GetWorkloadMethodCall(passArguments)).ToString();
6869

src/BenchmarkDotNet/Configs/ImmutableConfig.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ internal ImmutableConfig(
9292
public IAnalyser GetCompositeAnalyser() => new CompositeAnalyser(analysers);
9393
public IDiagnoser GetCompositeDiagnoser() => new CompositeDiagnoser(diagnosers);
9494

95-
public bool HasMemoryDiagnoser() => diagnosers.Contains(MemoryDiagnoser.Default);
95+
public bool HasMemoryDiagnoser() => diagnosers.Any(diagnoser => diagnoser is MemoryDiagnoser);
96+
97+
public bool HasSurvivedMemoryDiagnoser() => diagnosers.Contains(MemoryDiagnoser.WithSurvived);
9698

9799
public bool HasThreadingDiagnoser() => diagnosers.Contains(ThreadingDiagnoser.Default);
98100

src/BenchmarkDotNet/Diagnosers/MemoryDiagnoser.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ public class MemoryDiagnoser : IDiagnoser
1515
{
1616
private const string DiagnoserId = nameof(MemoryDiagnoser);
1717

18-
public static readonly MemoryDiagnoser Default = new MemoryDiagnoser();
18+
public static readonly MemoryDiagnoser Default = new MemoryDiagnoser(false);
19+
public static readonly MemoryDiagnoser WithSurvived = new MemoryDiagnoser(true);
1920

20-
private MemoryDiagnoser() { } // we want to have only a single instance of MemoryDiagnoser
21+
private MemoryDiagnoser(bool includeSurvived)
22+
{
23+
IncludeSurvived = includeSurvived;
24+
}
25+
26+
public bool IncludeSurvived { get; }
2127

2228
public RunMode GetRunMode(BenchmarkCase benchmarkCase) => RunMode.NoOverhead;
2329

@@ -37,6 +43,10 @@ public IEnumerable<Metric> ProcessResults(DiagnoserResults diagnoserResults)
3743
yield return new Metric(GarbageCollectionsMetricDescriptor.Gen1, diagnoserResults.GcStats.Gen1Collections / (double)diagnoserResults.GcStats.TotalOperations * 1000);
3844
yield return new Metric(GarbageCollectionsMetricDescriptor.Gen2, diagnoserResults.GcStats.Gen2Collections / (double)diagnoserResults.GcStats.TotalOperations * 1000);
3945
yield return new Metric(AllocatedMemoryMetricDescriptor.Instance, diagnoserResults.GcStats.BytesAllocatedPerOperation);
46+
if (IncludeSurvived)
47+
{
48+
yield return new Metric(SurvivedMemoryMetricDescriptor.Instance, diagnoserResults.GcStats.SurvivedBytes);
49+
}
4050
}
4151

4252
private class AllocatedMemoryMetricDescriptor : IMetricDescriptor
@@ -52,6 +62,19 @@ private class AllocatedMemoryMetricDescriptor : IMetricDescriptor
5262
public bool TheGreaterTheBetter => false;
5363
}
5464

65+
private class SurvivedMemoryMetricDescriptor : IMetricDescriptor
66+
{
67+
internal static readonly IMetricDescriptor Instance = new SurvivedMemoryMetricDescriptor();
68+
69+
public string Id => "Survived Memory";
70+
public string DisplayName => "Survived";
71+
public string Legend => "Total application memory consumed after all operations, including Cleanups (managed only, inclusive, 1KB = 1024B)";
72+
public string NumberFormat => "N0";
73+
public UnitType UnitType => UnitType.Size;
74+
public string Unit => SizeUnit.B.Name;
75+
public bool TheGreaterTheBetter => false;
76+
}
77+
5578
private class GarbageCollectionsMetricDescriptor : IMetricDescriptor
5679
{
5780
internal static readonly IMetricDescriptor Gen0 = new GarbageCollectionsMetricDescriptor(0);

src/BenchmarkDotNet/Engines/Engine.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ public class Engine : IEngine
4242
private readonly EnginePilotStage pilotStage;
4343
private readonly EngineWarmupStage warmupStage;
4444
private readonly EngineActualStage actualStage;
45-
private readonly bool includeExtraStats;
45+
private readonly bool includeExtraStats, includeSurvivedMemory;
4646

4747
internal Engine(
4848
IHost host,
4949
IResolver resolver,
5050
Action dummy1Action, Action dummy2Action, Action dummy3Action, Action<long> overheadAction, Action<long> workloadAction, Job targetJob,
5151
Action globalSetupAction, Action globalCleanupAction, Action iterationSetupAction, Action iterationCleanupAction, long operationsPerInvoke,
52-
bool includeExtraStats, string benchmarkName)
52+
bool includeExtraStats, bool includeSurvivedMemory, string benchmarkName)
5353
{
5454

5555
Host = host;
@@ -66,6 +66,7 @@ internal Engine(
6666
OperationsPerInvoke = operationsPerInvoke;
6767
this.includeExtraStats = includeExtraStats;
6868
BenchmarkName = benchmarkName;
69+
this.includeSurvivedMemory = includeSurvivedMemory;
6970

7071
Resolver = resolver;
7172

@@ -135,8 +136,8 @@ public RunResults Run()
135136
EngineEventSource.Log.BenchmarkStop(BenchmarkName);
136137

137138
var outlierMode = TargetJob.ResolveValue(AccuracyMode.OutlierModeCharacteristic, Resolver);
138-
139-
return new RunResults(idle, main, outlierMode, workGcHasDone, threadingStats);
139+
140+
return new RunResults(idle, main, outlierMode, workGcHasDone.WithSurvivedBytes(includeSurvivedMemory), threadingStats);
140141
}
141142

142143
public Measurement RunIteration(IterationData data)

src/BenchmarkDotNet/Engines/EngineFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ private static Engine CreateEngine(EngineParameters engineParameters, Job job, A
119119
engineParameters.IterationCleanupAction,
120120
engineParameters.OperationsPerInvoke,
121121
engineParameters.MeasureExtraStats,
122+
engineParameters.MeasureSurvivedMemory,
122123
engineParameters.BenchmarkName);
123124
}
124125
}

src/BenchmarkDotNet/Engines/EngineParameters.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class EngineParameters
2727
public Action IterationCleanupAction { get; set; }
2828
public bool MeasureExtraStats { get; set; }
2929

30+
public bool MeasureSurvivedMemory { get; set; }
31+
3032
[PublicAPI] public string BenchmarkName { get; set; }
3133

3234
public bool NeedsJitting => TargetJob.ResolveValue(RunMode.RunStrategyCharacteristic, DefaultResolver).NeedsJitting();

src/BenchmarkDotNet/Engines/GcStats.cs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ public struct GcStats : IEquatable<GcStats>
1515
private static readonly Func<long> GetAllocatedBytesForCurrentThreadDelegate = CreateGetAllocatedBytesForCurrentThreadDelegate();
1616
private static readonly Func<bool, long> GetTotalAllocatedBytesDelegate = CreateGetTotalAllocatedBytesDelegate();
1717

18-
public static readonly GcStats Empty = new GcStats(0, 0, 0, 0, 0);
18+
public static readonly GcStats Empty = new GcStats(0, 0, 0, 0, 0, 0);
1919

20-
private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long allocatedBytes, long totalOperations)
20+
private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, long allocatedBytes, long totalOperations, long survivedBytes)
2121
{
2222
Gen0Collections = gen0Collections;
2323
Gen1Collections = gen1Collections;
2424
Gen2Collections = gen2Collections;
2525
AllocatedBytes = allocatedBytes;
2626
TotalOperations = totalOperations;
27+
SurvivedBytes = survivedBytes;
2728
}
2829

2930
// did not use array here just to avoid heap allocation
@@ -37,6 +38,7 @@ private GcStats(int gen0Collections, int gen1Collections, int gen2Collections, l
3738
private long AllocatedBytes { get; }
3839

3940
public long TotalOperations { get; }
41+
public long SurvivedBytes { get; }
4042

4143
public long BytesAllocatedPerOperation
4244
{
@@ -60,7 +62,8 @@ public long BytesAllocatedPerOperation
6062
left.Gen1Collections + right.Gen1Collections,
6163
left.Gen2Collections + right.Gen2Collections,
6264
left.AllocatedBytes + right.AllocatedBytes,
63-
left.TotalOperations + right.TotalOperations);
65+
left.TotalOperations + right.TotalOperations,
66+
left.SurvivedBytes + right.SurvivedBytes);
6467
}
6568

6669
public static GcStats operator -(GcStats left, GcStats right)
@@ -70,11 +73,15 @@ public long BytesAllocatedPerOperation
7073
Math.Max(0, left.Gen1Collections - right.Gen1Collections),
7174
Math.Max(0, left.Gen2Collections - right.Gen2Collections),
7275
Math.Max(0, left.AllocatedBytes - right.AllocatedBytes),
73-
Math.Max(0, left.TotalOperations - right.TotalOperations));
76+
Math.Max(0, left.TotalOperations - right.TotalOperations),
77+
Math.Max(0, left.SurvivedBytes - right.SurvivedBytes));
7478
}
7579

7680
public GcStats WithTotalOperations(long totalOperationsCount)
77-
=> this + new GcStats(0, 0, 0, 0, totalOperationsCount);
81+
=> this + new GcStats(0, 0, 0, 0, totalOperationsCount, 0);
82+
83+
public GcStats WithSurvivedBytes(bool getBytes)
84+
=> this + new GcStats(0, 0, 0, 0, 0, GetTotalBytes(getBytes));
7885

7986
public int GetCollectionsCount(int generation)
8087
{
@@ -112,6 +119,7 @@ public static GcStats ReadInitial()
112119
GC.CollectionCount(1),
113120
GC.CollectionCount(2),
114121
allocatedBytes,
122+
0,
115123
0);
116124
}
117125

@@ -125,12 +133,31 @@ public static GcStats ReadFinal()
125133
// this will force GC.Collect, so we want to do this after collecting collections counts
126134
// to exclude this single full forced collection from results
127135
GetAllocatedBytes(),
136+
0,
128137
0);
129138
}
130139

131140
[PublicAPI]
132141
public static GcStats FromForced(int forcedFullGarbageCollections)
133-
=> new GcStats(forcedFullGarbageCollections, forcedFullGarbageCollections, forcedFullGarbageCollections, 0, 0);
142+
=> new GcStats(forcedFullGarbageCollections, forcedFullGarbageCollections, forcedFullGarbageCollections, 0, 0, 0);
143+
144+
private static long GetTotalBytes(bool actual)
145+
{
146+
if (!actual)
147+
return 0;
148+
149+
if (RuntimeInformation.IsFullFramework) // it can be a .NET app consuming our .NET Standard package
150+
{
151+
AppDomain.MonitoringIsEnabled = true;
152+
153+
// Enforce GC.Collect here just to make sure we get accurate results
154+
GC.Collect();
155+
return AppDomain.CurrentDomain.MonitoringSurvivedMemorySize;
156+
}
157+
158+
GC.Collect();
159+
return GC.GetTotalMemory(true);
160+
}
134161

135162
private static long GetAllocatedBytes()
136163
{
@@ -171,7 +198,7 @@ private static Func<bool, long> CreateGetTotalAllocatedBytesDelegate()
171198
}
172199

173200
public string ToOutputLine()
174-
=> $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes} {TotalOperations}";
201+
=> $"{ResultsLinePrefix} {Gen0Collections} {Gen1Collections} {Gen2Collections} {AllocatedBytes} {TotalOperations} {SurvivedBytes}";
175202

176203
public static GcStats Parse(string line)
177204
{
@@ -183,12 +210,13 @@ public static GcStats Parse(string line)
183210
|| !int.TryParse(measurementSplit[1], out int gen1)
184211
|| !int.TryParse(measurementSplit[2], out int gen2)
185212
|| !long.TryParse(measurementSplit[3], out long allocatedBytes)
186-
|| !long.TryParse(measurementSplit[4], out long totalOperationsCount))
213+
|| !long.TryParse(measurementSplit[4], out long totalOperationsCount)
214+
|| !long.TryParse(measurementSplit[5], out long survivedBytes))
187215
{
188216
throw new NotSupportedException("Invalid string");
189217
}
190218

191-
return new GcStats(gen0, gen1, gen2, allocatedBytes, totalOperationsCount);
219+
return new GcStats(gen0, gen1, gen2, allocatedBytes, totalOperationsCount, survivedBytes);
192220
}
193221

194222
public override string ToString() => ToOutputLine();
@@ -222,7 +250,13 @@ private static long CalculateAllocationQuantumSize()
222250
return result;
223251
}
224252

225-
public bool Equals(GcStats other) => Gen0Collections == other.Gen0Collections && Gen1Collections == other.Gen1Collections && Gen2Collections == other.Gen2Collections && AllocatedBytes == other.AllocatedBytes && TotalOperations == other.TotalOperations;
253+
public bool Equals(GcStats other) =>
254+
Gen0Collections == other.Gen0Collections
255+
&& Gen1Collections == other.Gen1Collections
256+
&& Gen2Collections == other.Gen2Collections
257+
&& AllocatedBytes == other.AllocatedBytes
258+
&& TotalOperations == other.TotalOperations
259+
&& SurvivedBytes == other.SurvivedBytes;
226260

227261
public override bool Equals(object obj) => obj is GcStats other && Equals(other);
228262

src/BenchmarkDotNet/Exporters/Csv/CsvMeasurementsExporter.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ private static MeasurementColumn[] GetColumns(Summary summary)
7171
new MeasurementColumn("Gen_2", (_, report, __) => report.GcStats.Gen2Collections.ToString(summary.GetCultureInfo())),
7272
new MeasurementColumn("Allocated_Bytes", (_, report, __) => report.GcStats.BytesAllocatedPerOperation.ToString(summary.GetCultureInfo()))
7373
};
74+
if (summary.BenchmarksCases.Any(benchmark => benchmark.Config.HasSurvivedMemoryDiagnoser()))
75+
{
76+
columns.Add(new MeasurementColumn("Survived_Bytes", (_, report, __) => report.GcStats.SurvivedBytes.ToString(summary.GetCultureInfo())));
77+
}
7478

7579
return columns.ToArray();
7680
}

src/BenchmarkDotNet/Templates/BenchmarkType.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
TargetJob = job,
3939
OperationsPerInvoke = $OperationsPerInvoke$,
4040
MeasureExtraStats = $MeasureExtraStats$,
41+
MeasureSurvivedMemory = $MeasureSurvivedMemory$,
4142
BenchmarkName = benchmarkName
4243
};
4344

0 commit comments

Comments
 (0)