[Debugger] Added support for stepping over await and out of async met…
authorDavid Karlaš <david.karlas@xamarin.com>
Fri, 27 Jan 2017 10:17:40 +0000 (11:17 +0100)
committerDavid Karlaš <david.karlas@xamarin.com>
Fri, 27 Jan 2017 10:17:57 +0000 (11:17 +0100)
commitfb5bc8d4c539fe82b36f1cd3a3bc89236b39c4aa
tree4eeb5f47f077a71d4eaf79fc408cfb4da09511a2
parentda69fcc7f6942ef78b580f755485c657bb659b47
[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+&lt;ss_await_1&gt;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+&lt;ss_await_1&gt;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
mono/mini/debugger-agent.c
mono/mini/method-to-ir.c
mono/mini/seq-points.c