Skip to content

Commit ac7210d

Browse files
committed
Fix async GlobalSetup/GlobalCleanup not being awaited with InProcessEmit toolchain.
1 parent f0acf70 commit ac7210d

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;
@@ -357,13 +361,22 @@ private void InitForEmitRunnable(BenchmarkBuildInfo newBenchmark)
357361

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

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

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

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

909914
var ilBuilder = methodBuilder.GetILGenerator();
910915

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

914928
ilBuilder.EmitVoidReturn(methodBuilder);
915929

916930
return methodBuilder;
917931
}
918932

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

238-
[Fact]
239-
public void InProcessEmitToolchainSupportsIterationSetupAndCleanup()
238+
[Theory]
239+
[InlineData(typeof(IterationSetupCleanup))]
240+
[InlineData(typeof(GlobalSetupCleanupTask))]
241+
[InlineData(typeof(GlobalSetupCleanupValueTask))]
242+
[InlineData(typeof(GlobalSetupCleanupValueTaskSource))]
243+
public void InProcessEmitToolchainSupportsSetupAndCleanup(Type benchmarkType)
240244
{
241245
var logger = new OutputLogger(Output);
242246
var config = CreateInProcessConfig(logger);
243247

244-
WithIterationSetupAndCleanup.SetupCounter = 0;
245-
WithIterationSetupAndCleanup.BenchmarkCounter = 0;
246-
WithIterationSetupAndCleanup.CleanupCounter = 0;
248+
Counters.SetupCounter = 0;
249+
Counters.BenchmarkCounter = 0;
250+
Counters.CleanupCounter = 0;
247251

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

250-
Assert.Equal(1, WithIterationSetupAndCleanup.SetupCounter);
251-
Assert.Equal(16, WithIterationSetupAndCleanup.BenchmarkCounter);
252-
Assert.Equal(1, WithIterationSetupAndCleanup.CleanupCounter);
254+
Assert.Equal(1, Counters.SetupCounter);
255+
Assert.Equal(16, Counters.BenchmarkCounter);
256+
Assert.Equal(1, Counters.CleanupCounter);
253257
}
254258

255-
public class WithIterationSetupAndCleanup
259+
private static class Counters
256260
{
257261
public static int SetupCounter, BenchmarkCounter, CleanupCounter;
262+
}
258263

264+
public class IterationSetupCleanup
265+
{
259266
[IterationSetup]
260-
public void Setup() => Interlocked.Increment(ref SetupCounter);
267+
public void Setup() => Interlocked.Increment(ref Counters.SetupCounter);
261268

262269
[Benchmark]
263-
public void Benchmark() => Interlocked.Increment(ref BenchmarkCounter);
270+
public void Benchmark() => Interlocked.Increment(ref Counters.BenchmarkCounter);
264271

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

0 commit comments

Comments
 (0)