Skip to content

Commit 35948a1

Browse files
committed
Fix async GlobalSetup/GlobalCleanup not being awaited with InProcessEmit toolchain.
1 parent 68e4459 commit 35948a1

File tree

3 files changed

+179
-28
lines changed

3 files changed

+179
-28
lines changed

src/BenchmarkDotNet/Toolchains/InProcess.Emit.Implementation/Emitters/RunnableEmitter.cs

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ private static void EmitNoArgsMethodCallPopReturn(
245245
private TypeBuilder runnableBuilder;
246246
private ConsumableTypeInfo consumableInfo;
247247
private ConsumeEmitter consumeEmitter;
248+
private ConsumableTypeInfo globalSetupReturnInfo;
249+
private ConsumableTypeInfo globalCleanupReturnInfo;
250+
private ConsumableTypeInfo iterationSetupReturnInfo;
251+
private ConsumableTypeInfo iterationCleanupReturnInfo;
248252

249253
private FieldBuilder globalSetupActionField;
250254
private FieldBuilder globalCleanupActionField;
@@ -356,13 +360,22 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark)
356360

357361
consumableInfo = new ConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.WorkloadMethod.ReturnType);
358362
consumeEmitter = ConsumeEmitter.GetConsumeEmitter(consumableInfo);
363+
globalSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalSetupMethod?.ReturnType);
364+
globalCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.GlobalCleanupMethod?.ReturnType);
365+
iterationSetupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationSetupMethod?.ReturnType);
366+
iterationCleanupReturnInfo = GetConsumableTypeInfo(benchmark.BenchmarkCase.Descriptor.IterationCleanupMethod?.ReturnType);
359367

360368
// Init types
361369
runnableBuilder = DefineRunnableTypeBuilder(benchmark, moduleBuilder);
362370
overheadDelegateType = EmitOverheadDelegateType();
363371
workloadDelegateType = EmitWorkloadDelegateType();
364372
}
365373

374+
private static ConsumableTypeInfo GetConsumableTypeInfo(Type methodReturnType)
375+
{
376+
return methodReturnType == null ? null : new ConsumableTypeInfo(methodReturnType);
377+
}
378+
366379
private Type EmitOverheadDelegateType()
367380
{
368381
// .class public auto ansi sealed BenchmarkDotNet.Autogenerated.Runnable_0OverheadDelegate
@@ -887,34 +900,84 @@ private void EmitSetupCleanupMethods()
887900
{
888901
// Emit Setup/Cleanup methods
889902
// We emit empty method instead of EmptyAction = "() => { }"
890-
globalSetupMethod = EmitWrapperMethod(
891-
GlobalSetupMethodName,
892-
Descriptor.GlobalSetupMethod);
893-
globalCleanupMethod = EmitWrapperMethod(
894-
GlobalCleanupMethodName,
895-
Descriptor.GlobalCleanupMethod);
896-
iterationSetupMethod = EmitWrapperMethod(
897-
IterationSetupMethodName,
898-
Descriptor.IterationSetupMethod);
899-
iterationCleanupMethod = EmitWrapperMethod(
900-
IterationCleanupMethodName,
901-
Descriptor.IterationCleanupMethod);
903+
globalSetupMethod = EmitWrapperMethod(GlobalSetupMethodName, Descriptor.GlobalSetupMethod, globalSetupReturnInfo);
904+
globalCleanupMethod = EmitWrapperMethod(GlobalCleanupMethodName, Descriptor.GlobalCleanupMethod, globalCleanupReturnInfo);
905+
iterationSetupMethod = EmitWrapperMethod(IterationSetupMethodName, Descriptor.IterationSetupMethod, iterationSetupReturnInfo);
906+
iterationCleanupMethod = EmitWrapperMethod(IterationCleanupMethodName, Descriptor.IterationCleanupMethod, iterationCleanupReturnInfo);
902907
}
903908

904-
private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod)
909+
private MethodBuilder EmitWrapperMethod(string methodName, MethodInfo optionalTargetMethod, ConsumableTypeInfo returnTypeInfo)
905910
{
906911
var methodBuilder = runnableBuilder.DefinePrivateVoidInstanceMethod(methodName);
907912

908913
var ilBuilder = methodBuilder.GetILGenerator();
909914

910915
if (optionalTargetMethod != null)
911-
EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true);
916+
{
917+
if (returnTypeInfo?.IsAwaitable == true)
918+
{
919+
EmitAwaitableSetupTeardown(methodBuilder, optionalTargetMethod, ilBuilder, returnTypeInfo);
920+
}
921+
else
922+
{
923+
EmitNoArgsMethodCallPopReturn(methodBuilder, optionalTargetMethod, ilBuilder, forceDirectCall: true);
924+
}
925+
}
912926

913927
ilBuilder.EmitVoidReturn(methodBuilder);
914928

915929
return methodBuilder;
916930
}
917931

932+
private void EmitAwaitableSetupTeardown(
933+
MethodBuilder methodBuilder,
934+
MethodInfo targetMethod,
935+
ILGenerator ilBuilder,
936+
ConsumableTypeInfo returnTypeInfo)
937+
{
938+
if (targetMethod == null)
939+
throw new ArgumentNullException(nameof(targetMethod));
940+
941+
if (returnTypeInfo.WorkloadMethodReturnType == typeof(void))
942+
{
943+
ilBuilder.Emit(OpCodes.Ldarg_0);
944+
}
945+
/*
946+
// call for instance
947+
// GlobalSetup();
948+
IL_0006: ldarg.0
949+
IL_0007: call instance void [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalSetup()
950+
*/
951+
/*
952+
// call for static
953+
// GlobalSetup();
954+
IL_0006: call string [BenchmarkDotNet]BenchmarkDotNet.Samples.SampleBenchmark::GlobalCleanup()
955+
*/
956+
if (targetMethod.IsStatic)
957+
{
958+
ilBuilder.Emit(OpCodes.Call, targetMethod);
959+
960+
}
961+
else if (methodBuilder.IsStatic)
962+
{
963+
throw new InvalidOperationException(
964+
$"[BUG] Static method {methodBuilder.Name} tries to call instance member {targetMethod.Name}");
965+
}
966+
else
967+
{
968+
ilBuilder.Emit(OpCodes.Ldarg_0);
969+
ilBuilder.Emit(OpCodes.Call, targetMethod);
970+
}
971+
972+
/*
973+
// BenchmarkDotNet.Helpers.AwaitHelper.GetResult(...);
974+
IL_000e: call !!0 BenchmarkDotNet.Helpers.AwaitHelper::GetResult<int32>(valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1<!!0>)
975+
*/
976+
977+
ilBuilder.Emit(OpCodes.Call, returnTypeInfo.GetResultMethod);
978+
ilBuilder.Emit(OpCodes.Pop);
979+
}
980+
918981
private void EmitCtorBody()
919982
{
920983
var ilBuilder = ctorMethod.GetILGenerator();

tests/BenchmarkDotNet.IntegrationTests/BenchmarkTestExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public Reports.Summary CanExecute<TBenchmark>(IConfig config = null, bool fullVa
4949
/// <param name="config">Optional custom config to be used instead of the default</param>
5050
/// <param name="fullValidation">Optional: disable validation (default = true/enabled)</param>
5151
/// <returns>The summary from the benchmark run</returns>
52-
protected Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true)
52+
public Reports.Summary CanExecute(Type type, IConfig config = null, bool fullValidation = true)
5353
{
5454
// Add logging, so the Benchmark execution is in the TestRunner output (makes Debugging easier)
5555
if (config == null)

tests/BenchmarkDotNet.IntegrationTests/InProcessEmitTest.cs

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -239,35 +239,123 @@ public static ValueTask<decimal> InvokeOnceStaticValueTaskOfT()
239239
}
240240
}
241241

242-
[Fact]
243-
public void InProcessEmitToolchainSupportsIterationSetupAndCleanup()
242+
[Theory]
243+
[InlineData(typeof(IterationSetupCleanup))]
244+
[InlineData(typeof(GlobalSetupCleanupTask))]
245+
[InlineData(typeof(GlobalSetupCleanupValueTask))]
246+
[InlineData(typeof(GlobalSetupCleanupValueTaskSource))]
247+
public void InProcessEmitToolchainSupportsSetupAndCleanup(Type benchmarkType)
244248
{
245249
var logger = new OutputLogger(Output);
246250
var config = CreateInProcessConfig(logger);
247251

248-
WithIterationSetupAndCleanup.SetupCounter = 0;
249-
WithIterationSetupAndCleanup.BenchmarkCounter = 0;
250-
WithIterationSetupAndCleanup.CleanupCounter = 0;
252+
Counters.SetupCounter = 0;
253+
Counters.BenchmarkCounter = 0;
254+
Counters.CleanupCounter = 0;
251255

252-
var summary = CanExecute<WithIterationSetupAndCleanup>(config);
256+
var summary = CanExecute(benchmarkType, config);
253257

254-
Assert.Equal(1, WithIterationSetupAndCleanup.SetupCounter);
255-
Assert.Equal(16, WithIterationSetupAndCleanup.BenchmarkCounter);
256-
Assert.Equal(1, WithIterationSetupAndCleanup.CleanupCounter);
258+
Assert.Equal(1, Counters.SetupCounter);
259+
Assert.Equal(16, Counters.BenchmarkCounter);
260+
Assert.Equal(1, Counters.CleanupCounter);
257261
}
258262

259-
public class WithIterationSetupAndCleanup
263+
private static class Counters
260264
{
261265
public static int SetupCounter, BenchmarkCounter, CleanupCounter;
266+
}
262267

268+
public class IterationSetupCleanup
269+
{
263270
[IterationSetup]
264-
public void Setup() => Interlocked.Increment(ref SetupCounter);
271+
public void Setup() => Interlocked.Increment(ref Counters.SetupCounter);
265272

266273
[Benchmark]
267-
public void Benchmark() => Interlocked.Increment(ref BenchmarkCounter);
274+
public void Benchmark() => Interlocked.Increment(ref Counters.BenchmarkCounter);
268275

269276
[IterationCleanup]
270-
public void Cleanup() => Interlocked.Increment(ref CleanupCounter);
277+
public void Cleanup() => Interlocked.Increment(ref Counters.CleanupCounter);
278+
}
279+
280+
public class GlobalSetupCleanupTask
281+
{
282+
[GlobalSetup]
283+
public static async Task GlobalSetup()
284+
{
285+
await Task.Yield();
286+
Interlocked.Increment(ref Counters.SetupCounter);
287+
}
288+
289+
[GlobalCleanup]
290+
public async Task GlobalCleanup()
291+
{
292+
await Task.Yield();
293+
Interlocked.Increment(ref Counters.CleanupCounter);
294+
}
295+
296+
[Benchmark]
297+
public void InvokeOnceVoid()
298+
{
299+
Interlocked.Increment(ref Counters.BenchmarkCounter);
300+
}
301+
}
302+
303+
public class GlobalSetupCleanupValueTask
304+
{
305+
[GlobalSetup]
306+
public static async ValueTask GlobalSetup()
307+
{
308+
await Task.Yield();
309+
Interlocked.Increment(ref Counters.SetupCounter);
310+
}
311+
312+
[GlobalCleanup]
313+
public async ValueTask GlobalCleanup()
314+
{
315+
await Task.Yield();
316+
Interlocked.Increment(ref Counters.CleanupCounter);
317+
}
318+
319+
[Benchmark]
320+
public void InvokeOnceVoid()
321+
{
322+
Interlocked.Increment(ref Counters.BenchmarkCounter);
323+
}
324+
}
325+
326+
public class GlobalSetupCleanupValueTaskSource
327+
{
328+
private readonly static ValueTaskSource<int> valueTaskSource = new ();
329+
330+
[GlobalSetup]
331+
public static ValueTask GlobalSetup()
332+
{
333+
valueTaskSource.Reset();
334+
Task.Delay(1).ContinueWith(_ =>
335+
{
336+
Interlocked.Increment(ref Counters.SetupCounter);
337+
valueTaskSource.SetResult(42);
338+
});
339+
return new ValueTask(valueTaskSource, valueTaskSource.Token);
340+
}
341+
342+
[GlobalCleanup]
343+
public ValueTask GlobalCleanup()
344+
{
345+
valueTaskSource.Reset();
346+
Task.Delay(1).ContinueWith(_ =>
347+
{
348+
Interlocked.Increment(ref Counters.CleanupCounter);
349+
valueTaskSource.SetResult(42);
350+
});
351+
return new ValueTask(valueTaskSource, valueTaskSource.Token);
352+
}
353+
354+
[Benchmark]
355+
public void InvokeOnceVoid()
356+
{
357+
Interlocked.Increment(ref Counters.BenchmarkCounter);
358+
}
271359
}
272360
}
273361
}

0 commit comments

Comments
 (0)