99using BenchmarkDotNet . Columns ;
1010using BenchmarkDotNet . Configs ;
1111using BenchmarkDotNet . Diagnosers ;
12+ using BenchmarkDotNet . Engines ;
1213using BenchmarkDotNet . Extensions ;
1314using BenchmarkDotNet . IntegrationTests . Xunit ;
1415using BenchmarkDotNet . Jobs ;
1920using BenchmarkDotNet . Tests . XUnit ;
2021using BenchmarkDotNet . Toolchains ;
2122using BenchmarkDotNet . Toolchains . CoreRt ;
23+ using BenchmarkDotNet . Toolchains . CsProj ;
2224using BenchmarkDotNet . Toolchains . InProcess . Emit ;
2325using Xunit ;
2426using Xunit . Abstractions ;
@@ -69,6 +71,64 @@ public void MemoryDiagnoserIsAccurate(IToolchain toolchain)
6971 } ) ;
7072 }
7173
74+ public class AccurateSurvived
75+ {
76+ [ Benchmark ] public byte [ ] EightBytesArray ( ) => new byte [ 8 ] ;
77+ [ Benchmark ] public byte [ ] SixtyFourBytesArray ( ) => new byte [ 64 ] ;
78+ [ Benchmark ] public Task < int > AllocateTask ( ) => Task . FromResult ( default ( int ) ) ;
79+
80+
81+ public byte [ ] bytes8 ;
82+ public byte [ ] bytes64 ;
83+ public Task < int > task ;
84+
85+ [ GlobalSetup ( Targets = new string [ ] { nameof ( EightBytesArrayNoAllocate ) , nameof ( SixtyFourBytesArrayNoAllocate ) } ) ]
86+ public void SetupNoAllocate ( )
87+ {
88+ bytes8 = new byte [ 8 ] ;
89+ bytes64 = new byte [ 64 ] ;
90+ }
91+
92+ [ Benchmark ] public byte [ ] EightBytesArrayNoAllocate ( ) => bytes8 ;
93+ [ Benchmark ] public byte [ ] SixtyFourBytesArrayNoAllocate ( ) => bytes64 ;
94+
95+
96+ [ Benchmark ] public void EightBytesArraySurvive ( ) => bytes8 = new byte [ 8 ] ;
97+ [ Benchmark ] public void SixtyFourBytesArraySurvive ( ) => bytes64 = new byte [ 64 ] ;
98+ [ Benchmark ] public void AllocateTaskSurvive ( ) => task = Task . FromResult ( default ( int ) ) ;
99+
100+
101+ [ Benchmark ] public void EightBytesArrayAllocateNoSurvive ( ) => DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( new byte [ 8 ] ) ;
102+ [ Benchmark ] public void SixtyFourBytesArrayAllocateNoSurvive ( ) => DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( new byte [ 64 ] ) ;
103+ [ Benchmark ] public void TaskAllocateNoSurvive ( ) => DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( Task . FromResult ( default ( int ) ) ) ;
104+ }
105+
106+ [ Theory , MemberData ( nameof ( GetToolchains ) ) ]
107+ [ Trait ( Constants . Category , Constants . BackwardCompatibilityCategory ) ]
108+ public void MemoryDiagnoserSurvivedIsAccurate ( IToolchain toolchain )
109+ {
110+ long objectAllocationOverhead = IntPtr . Size * 2 ; // pointer to method table + object header word
111+ long arraySizeOverhead = IntPtr . Size ; // array length
112+
113+ AssertSurvived ( toolchain , typeof ( AccurateSurvived ) , new Dictionary < string , long >
114+ {
115+ { nameof ( AccurateSurvived . EightBytesArray ) , 0 } ,
116+ { nameof ( AccurateSurvived . SixtyFourBytesArray ) , 0 } ,
117+ { nameof ( AccurateSurvived . AllocateTask ) , 0 } ,
118+
119+ { nameof ( AccurateSurvived . EightBytesArrayNoAllocate ) , 0 } ,
120+ { nameof ( AccurateSurvived . SixtyFourBytesArrayNoAllocate ) , 0 } ,
121+
122+ { nameof ( AccurateSurvived . EightBytesArraySurvive ) , 8 + objectAllocationOverhead + arraySizeOverhead } ,
123+ { nameof ( AccurateSurvived . SixtyFourBytesArraySurvive ) , 64 + objectAllocationOverhead + arraySizeOverhead } ,
124+ { nameof ( AccurateSurvived . AllocateTaskSurvive ) , CalculateRequiredSpace < Task < int > > ( ) } ,
125+
126+ { nameof ( AccurateSurvived . EightBytesArrayAllocateNoSurvive ) , 0 } ,
127+ { nameof ( AccurateSurvived . SixtyFourBytesArrayAllocateNoSurvive ) , 0 } ,
128+ { nameof ( AccurateSurvived . TaskAllocateNoSurvive ) , 0 } ,
129+ } ) ;
130+ }
131+
72132 public class AllocatingGlobalSetupAndCleanup
73133 {
74134 private List < int > list ;
@@ -102,6 +162,16 @@ public void MemoryDiagnoserDoesNotIncludeAllocationsFromSetupAndCleanup(IToolcha
102162 } ) ;
103163 }
104164
165+ [ Theory , MemberData ( nameof ( GetToolchains ) ) ]
166+ [ Trait ( Constants . Category , Constants . BackwardCompatibilityCategory ) ]
167+ public void MemoryDiagnoserDoesNotIncludeSurvivedFromSetupAndCleanup ( IToolchain toolchain )
168+ {
169+ AssertSurvived ( toolchain , typeof ( AllocatingGlobalSetupAndCleanup ) , new Dictionary < string , long >
170+ {
171+ { nameof ( AllocatingGlobalSetupAndCleanup . AllocateNothing ) , 0 }
172+ } ) ;
173+ }
174+
105175 public class NoAllocationsAtAll
106176 {
107177 [ Benchmark ] public void EmptyMethod ( ) { }
@@ -117,6 +187,16 @@ public void EngineShouldNotInterfereAllocationResults(IToolchain toolchain)
117187 } ) ;
118188 }
119189
190+ [ Theory , MemberData ( nameof ( GetToolchains ) ) ]
191+ [ Trait ( Constants . Category , Constants . BackwardCompatibilityCategory ) ]
192+ public void EngineShouldNotInterfereSurvivedResults ( IToolchain toolchain )
193+ {
194+ AssertSurvived ( toolchain , typeof ( NoAllocationsAtAll ) , new Dictionary < string , long >
195+ {
196+ { nameof ( NoAllocationsAtAll . EmptyMethod ) , 0 }
197+ } ) ;
198+ }
199+
120200 public class NoBoxing
121201 {
122202 [ Benchmark ] public ValueTuple < int > ReturnsValueType ( ) => new ValueTuple < int > ( 0 ) ;
@@ -132,10 +212,29 @@ public void EngineShouldNotIntroduceBoxing(IToolchain toolchain)
132212 } ) ;
133213 }
134214
215+ [ Theory , MemberData ( nameof ( GetToolchains ) ) ]
216+ [ Trait ( Constants . Category , Constants . BackwardCompatibilityCategory ) ]
217+ public void EngineShouldNotIntroduceBoxingSurvived ( IToolchain toolchain )
218+ {
219+ AssertSurvived ( toolchain , typeof ( NoBoxing ) , new Dictionary < string , long >
220+ {
221+ { nameof ( NoBoxing . ReturnsValueType ) , 0 }
222+ } ) ;
223+ }
224+
135225 public class NonAllocatingAsynchronousBenchmarks
136226 {
137227 private readonly Task < int > completedTaskOfT = Task . FromResult ( default ( int ) ) ; // we store it in the field, because Task<T> is reference type so creating it allocates heap memory
138228
229+ [ GlobalSetup ]
230+ public void Setup ( )
231+ {
232+ // Run once to set static memory.
233+ DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( CompletedTask ( ) ) ;
234+ DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( CompletedTaskOfT ( ) ) ;
235+ DeadCodeEliminationHelper . KeepAliveWithoutBoxing ( CompletedValueTaskOfT ( ) ) ;
236+ }
237+
139238 [ Benchmark ] public Task CompletedTask ( ) => Task . CompletedTask ;
140239
141240 [ Benchmark ] public Task < int > CompletedTaskOfT ( ) => completedTaskOfT ;
@@ -155,6 +254,18 @@ public void AwaitingTasksShouldNotInterfereAllocationResults(IToolchain toolchai
155254 } ) ;
156255 }
157256
257+ [ Theory , MemberData ( nameof ( GetToolchains ) ) ]
258+ [ Trait ( Constants . Category , Constants . BackwardCompatibilityCategory ) ]
259+ public void AwaitingTasksShouldNotInterfereSurvivedResults ( IToolchain toolchain )
260+ {
261+ AssertSurvived ( toolchain , typeof ( NonAllocatingAsynchronousBenchmarks ) , new Dictionary < string , long >
262+ {
263+ { nameof ( NonAllocatingAsynchronousBenchmarks . CompletedTask ) , 0 } ,
264+ { nameof ( NonAllocatingAsynchronousBenchmarks . CompletedTaskOfT ) , 0 } ,
265+ { nameof ( NonAllocatingAsynchronousBenchmarks . CompletedValueTaskOfT ) , 0 }
266+ } ) ;
267+ }
268+
158269 public class WithOperationsPerInvokeBenchmarks
159270 {
160271 [ Benchmark ( OperationsPerInvoke = 4 ) ]
@@ -257,7 +368,7 @@ public void MemoryDiagnoserIsAccurateForMultiThreadedBenchmarks(IToolchain toolc
257368
258369 private void AssertAllocations ( IToolchain toolchain , Type benchmarkType , Dictionary < string , long > benchmarksAllocationsValidators )
259370 {
260- var config = CreateConfig ( toolchain ) ;
371+ var config = CreateConfig ( toolchain , MemoryDiagnoser . Default ) ;
261372 var benchmarks = BenchmarkConverter . TypeToBenchmarks ( benchmarkType , config ) ;
262373
263374 var summary = BenchmarkRunner . Run ( benchmarks ) ;
@@ -285,7 +396,37 @@ private void AssertAllocations(IToolchain toolchain, Type benchmarkType, Diction
285396 }
286397 }
287398
288- private IConfig CreateConfig ( IToolchain toolchain )
399+ private void AssertSurvived ( IToolchain toolchain , Type benchmarkType , Dictionary < string , long > benchmarkSurvivedValidators )
400+ {
401+ // Core has survived memory measurement problems.
402+ // See https://github.com/dotnet/runtime/issues/45446
403+ if ( toolchain is CsProjCoreToolchain || toolchain is CoreRtToolchain ) // CoreRt actually does measure accurately in a normal benchmark run, but doesn't with the specific version used in these tests.
404+ return ;
405+
406+ var config = CreateConfig ( toolchain , MemoryDiagnoser . WithSurvived ) ;
407+ var benchmarks = BenchmarkConverter . TypeToBenchmarks ( benchmarkType , config ) ;
408+
409+ var summary = BenchmarkRunner . Run ( benchmarks ) ;
410+
411+ foreach ( var benchmarkSurvivedValidator in benchmarkSurvivedValidators )
412+ {
413+ // CoreRT is missing some of the CoreCLR threading/task related perf improvements, so sizeof(Task<int>) calculated for CoreCLR < sizeof(Task<int>) on CoreRT
414+ // see https://github.com/dotnet/corert/issues/5705 for more
415+ if ( benchmarkSurvivedValidator . Key == nameof ( AccurateSurvived . AllocateTaskSurvive ) && toolchain is CoreRtToolchain )
416+ continue ;
417+
418+ var survivedBenchmarks = benchmarks . BenchmarksCases . Where ( benchmark => benchmark . Descriptor . WorkloadMethodDisplayInfo == benchmarkSurvivedValidator . Key ) . ToArray ( ) ;
419+
420+ foreach ( var benchmark in survivedBenchmarks )
421+ {
422+ var benchmarkReport = summary . Reports . Single ( report => report . BenchmarkCase == benchmark ) ;
423+
424+ Assert . Equal ( benchmarkSurvivedValidator . Value , benchmarkReport . GcStats . SurvivedBytes ) ;
425+ }
426+ }
427+ }
428+
429+ private IConfig CreateConfig ( IToolchain toolchain , MemoryDiagnoser memoryDiagnoser )
289430 => ManualConfig . CreateEmpty ( )
290431 . AddJob ( Job . ShortRun
291432 . WithEvaluateOverhead ( false ) // no need to run idle for this test
@@ -294,7 +435,7 @@ private IConfig CreateConfig(IToolchain toolchain)
294435 . WithGcForce ( false )
295436 . WithToolchain ( toolchain ) )
296437 . AddColumnProvider ( DefaultColumnProviders . Instance )
297- . AddDiagnoser ( MemoryDiagnoser . Default )
438+ . AddDiagnoser ( memoryDiagnoser )
298439 . AddLogger ( toolchain . IsInProcess ? ConsoleLogger . Default : new OutputLogger ( output ) ) ; // we can't use OutputLogger for the InProcess toolchains because it allocates memory on the same thread
299440
300441 // note: don't copy, never use in production systems (it should work but I am not 100% sure)
0 commit comments