Skip to content

Commit 61e0d9f

Browse files
committed
Supporting "await using" in Release configuration
The instruction patterns for compiler-generated branches in the "await using" async state machine are different in the Release configuration than in the Debug configuration, so those patterns have been added to SkipAwaitUsingBranches() in CecilSymbolHelper. All unit tests, including the newly-added ones from my previous commit, are now passing in both Debug and Release configurations locally. One interesting wrinkle I turned up was ILSpy was showing a different set of compiler-generated IL than I was getting from the same code on SharpLab. In particular, the compiler behind the scenes on SharpLab was sometimes optimizing stloc.s/ldloc.s instruction pairs to dup instructions instead. So I went ahead and checked for both patterns (i.e., with store-load pairs or with dup instructions), in hopes of being somewhat more future- proof if, for example, this is an optimization arriving in .NET 5.
1 parent d5b731b commit 61e0d9f

File tree

2 files changed

+109
-19
lines changed

2 files changed

+109
-19
lines changed

src/coverlet.core/Symbols/CecilSymbolHelper.cs

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -397,30 +397,36 @@ private bool SkipGeneratedBranchesForExceptionHandlers(MethodDefinition methodDe
397397

398398
private bool SkipAwaitUsingBranches(List<Instruction> instructions, Instruction instruction)
399399
{
400+
/*
401+
There are three kinds of branches we'll want to skip in the compiler-
402+
generated state machine that arises from an "await using" statement.
403+
*/
404+
405+
int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer());
406+
400407
/*
401408
Suppose we have an "await using" statement like this one:
402409
403410
await using (var ms = new MemoryStream(Encoding.ASCII.GetBytes("abc")))
404411
{
405412
}
406413
407-
The asynchronously disposable object is stored in a compiler-generated
408-
field with the name <xxx>5__#, where "xxx" is the name of the variable
409-
and "#" is an increasing index number for compiler-generated variables
410-
(so, in this case, it would be <ms>5__1). Where it increases from 1 is
411-
when "await using" is nested.
412-
413-
There are three kinds of branches that the compiler generates that we
414-
should skip.
415-
*/
414+
In the Debug configuration, the asynchronously disposable object is stored
415+
in a compiler-generated "hoisted local" field (written as ">5__"). The
416+
rest of the name includes the name of the local variable and a sort of
417+
sequence number (so, for example, in the example above, the full name
418+
would be "<ms>5__1").
416419
417-
int currentIndex = instructions.BinarySearch(instruction, new InstructionByOffsetComparer());
420+
In the Release configuration, the object is instead stored in a local
421+
variable in the generated MoveNext() method.
418422
419-
/*
420423
The first kind of branch we want to skip is a check that the variable
421424
holding the asychronously disposable object is null. In the presence
422425
of nested "await using" statements, all of these variables are checked
423-
in various places, but the IL for each one follows the same pattern.
426+
in various places, but the IL for each one follows one of two patterns
427+
(one for the Debug and one for the Release configuration).
428+
429+
For the Debug configuration, we have:
424430
425431
if (<ms>5__1 == null) [<--- we want to skip this]
426432
{
@@ -430,19 +436,41 @@ holding the asychronously disposable object is null. In the presence
430436
IL_0048: ldarg.0
431437
IL_0049: ldfld class [System.Private.CoreLib]System.IO.MemoryStream AwaitUsing/'<AsyncAwait>d__0'::'<ms>5__1'
432438
IL_004e: brfalse.s IL_00b9 [<--- first kind of branch to skip]
439+
440+
For the Release configuration, we have this instead (appearing just after
441+
a generated try/catch, so we'll check for the preceding leave.s instruction,
442+
since the pattern is otherwise so simple and short.
443+
444+
if (generatedLocalVariable == null) [<--- we want to skip this]
445+
{
446+
goto IL_0075;
447+
}
448+
449+
IL_0037: leave.s IL_0039
450+
IL_0039: ldloc.1
451+
IL_003a: brfalse.s IL_0098
433452
*/
434453
if (currentIndex >= 2 &&
435454
instructions[currentIndex - 2].OpCode == OpCodes.Ldarg_0 &&
436455
instructions[currentIndex - 1].OpCode == OpCodes.Ldfld &&
437456
instructions[currentIndex - 1].Operand is FieldReference disposableRef &&
438-
disposableRef.Name.StartsWith("<") && disposableRef.Name.Contains(">5__"))
457+
disposableRef.Name.StartsWith("<") && disposableRef.Name.Contains(">5__") &&
458+
instructions[currentIndex].OpCode == OpCodes.Brfalse_S)
459+
{
460+
return true;
461+
}
462+
else if (currentIndex >= 2 &&
463+
instructions[currentIndex - 2].OpCode == OpCodes.Leave_S &&
464+
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 &&
465+
instructions[currentIndex].OpCode == OpCodes.Brfalse_S)
439466
{
440467
return true;
441468
}
442469
/*
443470
The second and third kinds of branches we want to skip both appear in a
444471
compiler-generated block that checks if an exception has been thrown by a
445-
task and needs to be re-thrown.
472+
task and needs to be re-thrown. This pattern, too, differs between
473+
Debug and Release configurations. In a Debug configuration, we have:
446474
447475
obj2 = <>s__2;
448476
if (obj2 != null) [<--- second kind of branch to skip]
@@ -465,23 +493,84 @@ task and needs to be re-thrown.
465493
IL_00c9: stloc.s 5
466494
IL_00cb: ldloc.s 5
467495
IL_00cd: brtrue.s IL_00d1 [<--- third branch to skip]
496+
497+
Meanwhile, in a Release configuration, we have the same pattern, but
498+
the naming convention used for the compiler-generated field is
499+
different (<>7__ instead of <>s__, i.e., GeneratedNameKind.ReusableHoistedLocalField
500+
instead of GeneratedNameKind.HoistedSynthesizedLocalField), with
501+
different local variables in use.
502+
503+
IL_0098: ldarg.0
504+
IL_0099: ldfld object AwaitUsing/'<AsyncAwait>d__0'::'<>7__wrap1'
505+
IL_009e: stloc.2
506+
IL_009f: ldloc.2
507+
IL_00a0: brfalse.s IL_00b7
508+
IL_00a2: ldloc.2
509+
IL_00a3: isinst [System.Private.CoreLib]System.Exception
510+
IL_00a8: stloc.s 5
511+
IL_00aa: ldloc.s 5
512+
IL_00ac: brtrue.s IL_00af
513+
514+
And one more note: On SharpLab, I see that the stloc.s/ldloc.s pairs
515+
can also be optimized down to dup instructions, so we'll check for
516+
that pattern, as well.
468517
*/
469518
else if (currentIndex >= 3 &&
470519
instructions[currentIndex - 3].OpCode == OpCodes.Ldfld &&
471-
instructions[currentIndex - 3].Operand is FieldReference exceptionRef && exceptionRef.Name.Contains("<>s__") &&
520+
instructions[currentIndex - 3].Operand is FieldReference debugExceptionRef && debugExceptionRef.Name.Contains("<>s__") &&
472521
instructions[currentIndex - 2].OpCode == OpCodes.Stloc_1 &&
473-
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1)
522+
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_1 &&
523+
instructions[currentIndex].OpCode == OpCodes.Brfalse_S)
474524
{
475525
return true;
476526
}
477527
else if (currentIndex >= 4 &&
478528
instructions[currentIndex - 4].OpCode == OpCodes.Ldloc_1 &&
479529
instructions[currentIndex - 3].OpCode == OpCodes.Isinst &&
480-
instructions[currentIndex - 3].Operand is TypeReference exceptionType && exceptionType.FullName == "System.Exception" &&
530+
instructions[currentIndex - 3].Operand is TypeReference debugExceptionType && debugExceptionType.FullName == "System.Exception" &&
481531
instructions[currentIndex - 2].OpCode == OpCodes.Stloc_S &&
482-
instructions[currentIndex - 2].Operand is VariableReference variableStore && variableStore.Index == 5 &&
532+
instructions[currentIndex - 2].Operand is VariableReference debugVariableStore && debugVariableStore.Index == 5 &&
483533
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_S &&
484-
instructions[currentIndex - 1].Operand is VariableReference variableLoad && variableLoad.Index == 5)
534+
instructions[currentIndex - 1].Operand is VariableReference debugVariableLoad && debugVariableLoad.Index == 5 &&
535+
instructions[currentIndex].OpCode == OpCodes.Brtrue_S)
536+
{
537+
return true;
538+
}
539+
else if (currentIndex >= 3 &&
540+
instructions[currentIndex - 3].OpCode == OpCodes.Ldfld &&
541+
instructions[currentIndex - 3].Operand is FieldReference releaseExceptionRef && releaseExceptionRef.Name.Contains("<>7__") &&
542+
instructions[currentIndex - 2].OpCode == OpCodes.Stloc_2 &&
543+
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_2 &&
544+
instructions[currentIndex].OpCode == OpCodes.Brfalse_S)
545+
{
546+
return true;
547+
}
548+
else if (currentIndex >= 2 &&
549+
instructions[currentIndex - 2].OpCode == OpCodes.Ldfld &&
550+
instructions[currentIndex - 2].Operand is FieldReference releaseWithDupExceptionRef && releaseWithDupExceptionRef.Name.Contains("<>7__") &&
551+
instructions[currentIndex - 1].OpCode == OpCodes.Dup &&
552+
instructions[currentIndex].OpCode == OpCodes.Brfalse_S)
553+
{
554+
return true;
555+
}
556+
else if (currentIndex >= 4 &&
557+
instructions[currentIndex - 4].OpCode == OpCodes.Ldloc_2 &&
558+
instructions[currentIndex - 3].OpCode == OpCodes.Isinst &&
559+
instructions[currentIndex - 3].Operand is TypeReference releaseExceptionType && releaseExceptionType.FullName == "System.Exception" &&
560+
instructions[currentIndex - 2].OpCode == OpCodes.Stloc_S &&
561+
instructions[currentIndex - 2].Operand is VariableReference releaseVariableStore && releaseVariableStore.Index == 5 &&
562+
instructions[currentIndex - 1].OpCode == OpCodes.Ldloc_S &&
563+
instructions[currentIndex - 1].Operand is VariableReference releaseVariableLoad && releaseVariableLoad.Index == 5 &&
564+
instructions[currentIndex].OpCode == OpCodes.Brtrue_S)
565+
{
566+
return true;
567+
}
568+
else if (currentIndex >= 3 &&
569+
instructions[currentIndex - 3].OpCode == OpCodes.Ldloc_2 &&
570+
instructions[currentIndex - 2].OpCode == OpCodes.Isinst &&
571+
instructions[currentIndex - 2].Operand is TypeReference releaseWithDupExceptionType && releaseWithDupExceptionType.FullName == "System.Exception" &&
572+
instructions[currentIndex - 1].OpCode == OpCodes.Dup &&
573+
instructions[currentIndex].OpCode == OpCodes.Brtrue_S)
485574
{
486575
return true;
487576
}

test/coverlet.core.tests/Samples/Samples.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ async public ValueTask AsyncAwait()
196196
await default(ValueTask);
197197
}
198198
}
199+
199200
public class AwaitUsing
200201
{
201202
async public ValueTask AsyncAwait()

0 commit comments

Comments
 (0)