[Debugger] Added support for stepping over await and out of async met…
…hods
method-to-ir.c: Added sequence points on IL offsets yieldOffsets and resumeOffsets since we need stepping to stop yieldOffset and ability to set breakpoint on resumeOffset
seq-points.c: This change is needed since we need to know if we are at last non-empty SeqPoint in method, but without this change is_last_non_empty method in debugger-agent.c would loop forever since blocks point between each other in cycles
doest-app.cs and dtest.cs: unit test
debugger-agent.c: I will explain how stepping over `await` and stepping out of `async` method works in commit message, code itself should be self explaining with comments
Step-In and Step-Out case: inside `async` method we do everything same except two things:
1. At end of method we switch to step-out logic
This is important because normal stepping-in/over would step out into “thread pool” calling code. Which is not what we want, we want to continue stepping where .Wait, .Result or `await` is waiting for our Task to finish. `is_last_non_empty` is needed because last non-empty SeqPoint is placed before SetResult(see line 33 in decompiled method below) so we can do StepOut logic before SetResult method is called. I will explain how step-out works below.
2. When stepping is finished we check if we stepped on yieldOffset(this happens when user is about to step over `await` call) and Task has IsCompleted false(didn’t finish immediately)(line 11 in decompiled method). If we stopped on yieldOffset we put breakpoint on resumeOffset SeqPoint(line 19 in decompiled method) and resume execution so when our AsyncStateMachine is called back after Task(the one that our `await` call triggered) is finished we hit breakpoint and user is just after `await` call. It’s important that we set `async_id` before resuming at yieldOffset so we can check when breakpoint is hit if this is our Task or some other that is executing at same time, since we can’t check threads since threads can change before and after `await` call.
Step-Out case: When user requests step-out or user requests step-in/over and we are at end of method, we set breakpoint in special .Net framework method called “NotifyDebuggerOfWaitCompletion” and call “SetNotificationForWaitCompletion” method so after Task is finished(when we call .SetResult, line 33 in decompiled method).
Method that called our `async` method and got our Task(on which we called “SetNotificationForWaitCompletion”) returned. When it calls .Wait(), .Result, or `await` on our task it will call “NotifyDebuggerOfWaitCompletion”(framework does this). At that point our breakpoint(inside “NotifyDebuggerOfWaitCompletion”) is hit and we do step-out until we get to where user called .Wait(), .Result, and `await`.(Of course if ProjectCodeOnly is enabled, otherwise it’s in 1 method above “NotifyDebuggerOfWaitCompletion”).
I added also ppdb dump and monodis if anyone wants more data:
C# code:
public static async Task<int> ss_await_1 () {
var a = 1;
await Task.Delay (20);
return a + 2;
}
C# decompiled:
1: void IAsyncStateMachine.MoveNext ()
2: {
3: int num = this.<>1__state;
4: int result;
5: try {
6: TaskAwaiter taskAwaiter;
7: if (num != 0) {
8: this.<a>5__1 = 1;
9: taskAwaiter = Task.Delay (20).GetAwaiter ();
10: if (!taskAwaiter.get_IsCompleted ()) {
11: this.<>1__state = 0;
12: this.<>u__1 = taskAwaiter;
13: Tests.<ss_await_1>d__90 <ss_await_1>d__ = this;
14: this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Tests.<ss_await_1>d__90> (ref taskAwaiter, ref <ss_await_1>d__);
15: return;
16: }
17: }
18: else {
19: taskAwaiter = this.<>u__1;
20: this.<>u__1 = default(TaskAwaiter);
21: this.<>1__state = -1;
22: }
23: taskAwaiter.GetResult ();
24: taskAwaiter = default(TaskAwaiter);
25: result = this.<a>5__1 + 2;
26: }
27: catch (Exception exception) {
28: this.<>1__state = -2;
29: this.<>t__builder.SetException (exception);
30: return;
31: }
32: this.<>1__state = -2;
33: this.<>t__builder.SetResult (result);
34:}
ppdb dump for this method:
<method containingType="Tests+<ss_await_1>d__90" name="MoveNext">
<sequencePoints>
<entry offset="0x0" hidden="true" document="1" />
<entry offset="0x7" hidden="true" document="1" />
<entry offset="0xe" startLine="744" startColumn="46" endLine="744" endColumn="47" document="1" />
<entry offset="0xf" startLine="745" startColumn="3" endLine="745" endColumn="13" document="1" />
<entry offset="0x16" startLine="746" startColumn="3" endLine="746" endColumn="25" document="1" />
<entry offset="0x23" hidden="true" document="1" />
<entry offset="0x7c" startLine="747" startColumn="3" endLine="747" endColumn="16" document="1" />
<entry offset="0x87" hidden="true" document="1" />
<entry offset="0xa1" startLine="748" startColumn="2" endLine="748" endColumn="3" document="1" />
<entry offset="0xa9" hidden="true" document="1" />
</sequencePoints>
<scope startOffset="0x0" endOffset="0xb7" />
<asyncInfo>
<kickoffMethod declaringType="Tests" methodName="ss_await_1" />
<await yield="0x35" resume="0x50" declaringType="Tests+<ss_await_1>d__90" methodName="MoveNext" />
</asyncInfo>
</method>
monodis:
.method private final virtual hidebysig newslot
instance default void MoveNext () cil managed
{
// Method begins at RVA 0x41f0
.override Could not decode method override class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext due to (null)
// Code size 183 (0xb7)
.maxstack 3
.locals init (
int32 V_0,
int32 V_1,
valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiterV_2,
class Tests/'<ss_await_1>d__90' V_3,
class [mscorlib]System.Exception V_4)
IL_0000: ldarg.0
IL_0001: ldfld int32 Tests/'<ss_await_1>d__90'::'<>1__state'
IL_0006: stloc.0
.try { // 0
IL_0007: ldloc.0
IL_0008: brfalse.s IL_000c
IL_000a: br.s IL_000e
IL_000c: br.s IL_0050
IL_000e: nop
IL_000f: ldarg.0
IL_0010: ldc.i4.1
IL_0011: stfld int32 Tests/'<ss_await_1>d__90'::'<a>5__1'
IL_0016: ldc.i4.s 0x14
IL_0018: call class [mscorlib]System.Threading.Tasks.Task class [mscorlib]System.Threading.Tasks.Task::Delay(int32)
IL_001d: callvirt instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter class [mscorlib]System.Threading.Tasks.Task::GetAwaiter()
IL_0022: stloc.2
IL_0023: ldloca.s 2
IL_0025: call instance bool valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::get_IsCompleted()
IL_002a: brtrue.s IL_006c
IL_002c: ldarg.0
IL_002d: ldc.i4.0
IL_002e: dup
IL_002f: stloc.0
IL_0030: stfld int32 Tests/'<ss_await_1>d__90'::'<>1__state'
IL_0035: ldarg.0
IL_0036: ldloc.2
IL_0037: stfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Tests/'<ss_await_1>d__90'::'<>u__1'
IL_003c: ldarg.0
IL_003d: stloc.3
IL_003e: ldarg.0
IL_003f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Tests/'<ss_await_1>d__90'::'<>t__builder'
IL_0044: ldloca.s 2
IL_0046: ldloca.s 3
IL_0048: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter, class Tests/'<ss_await_1>d__90'> ([out] !!0&, [out] !!1&)
IL_004d: nop
IL_004e: leave.s IL_00b6
IL_0050: ldarg.0
IL_0051: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Tests/'<ss_await_1>d__90'::'<>u__1'
IL_0056: stloc.2
IL_0057: ldarg.0
IL_0058: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter Tests/'<ss_await_1>d__90'::'<>u__1'
IL_005d: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
IL_0063: ldarg.0
IL_0064: ldc.i4.m1
IL_0065: dup
IL_0066: stloc.0
IL_0067: stfld int32 Tests/'<ss_await_1>d__90'::'<>1__state'
IL_006c: ldloca.s 2
IL_006e: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter::GetResult()
IL_0073: nop
IL_0074: ldloca.s 2
IL_0076: initobj [mscorlib]System.Runtime.CompilerServices.TaskAwaiter
IL_007c: ldarg.0
IL_007d: ldfld int32 Tests/'<ss_await_1>d__90'::'<a>5__1'
IL_0082: ldc.i4.2
IL_0083: add
IL_0084: stloc.1
IL_0085: leave.s IL_00a1
} // end .try 0
catch class [mscorlib]System.Exception { // 0
IL_0087: stloc.s 4
IL_0089: ldarg.0
IL_008a: ldc.i4.s 0xfffffffe
IL_008c: stfld int32 Tests/'<ss_await_1>d__90'::'<>1__state'
IL_0091: ldarg.0
IL_0092: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Tests/'<ss_await_1>d__90'::'<>t__builder'
IL_0097: ldloc.s 4
IL_0099: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetException(class [mscorlib]System.Exception)
IL_009e: nop
IL_009f: leave.s IL_00b6
} // end handler 0
IL_00a1: ldarg.0
IL_00a2: ldc.i4.s 0xfffffffe
IL_00a4: stfld int32 Tests/'<ss_await_1>d__90'::'<>1__state'
IL_00a9: ldarg.0
IL_00aa: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> Tests/'<ss_await_1>d__90'::'<>t__builder'
IL_00af: ldloc.1
IL_00b0: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::SetResult(!0)
IL_00b5: nop
IL_00b6: ret
} // end of method <ss_await_1>d__90::MoveNext