Skip to content

Commit

Permalink
[wasm][debugger] Fixing async locals in nested ContinueWith blocks (#…
Browse files Browse the repository at this point in the history
…56911)

* Adding test for #41984

* Adding old @radical tests and fixing it.

* Fixing tests.

* Update src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs

Co-authored-by: Ankit Jain <radical@gmail.com>

* Update src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs

Co-authored-by: Ankit Jain <radical@gmail.com>

* Addressing @radical comments.

* Addressing @radical comments.

* Addressing @radical comments.

Co-authored-by: Ankit Jain <radical@gmail.com>
  • Loading branch information
thaystg and radical authored Aug 6, 2021
1 parent 1a3378d commit c1351d6
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 10 deletions.
66 changes: 58 additions & 8 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ internal class MonoSDBHelper
private static int MINOR_VERSION = 61;
private static int MAJOR_VERSION = 2;
private readonly ILogger logger;
private Regex regexForAsyncLocals = new Regex(@"\<([^)]*)\>", RegexOptions.Singleline);

public MonoSDBHelper(MonoProxy proxy, ILogger logger)
{
Expand Down Expand Up @@ -1710,6 +1711,62 @@ public async Task<bool> IsAsyncMethod(SessionId sessionId, int methodId, Cancell
return retDebuggerCmdReader.ReadByte() == 1 ; //token
}

private bool IsClosureReferenceField (string fieldName)
{
// mcs is "$locvar"
// old mcs is "<>f__ref"
// csc is "CS$<>"
// roslyn is "<>8__"
return fieldName.StartsWith ("CS$<>", StringComparison.Ordinal) ||
fieldName.StartsWith ("<>f__ref", StringComparison.Ordinal) ||
fieldName.StartsWith ("$locvar", StringComparison.Ordinal) ||
fieldName.StartsWith ("<>8__", StringComparison.Ordinal);
}

public async Task<JArray> GetHoistedLocalVariables(SessionId sessionId, int objectId, JArray asyncLocals, CancellationToken token)
{
JArray asyncLocalsFull = new JArray();
List<int> objectsAlreadyRead = new();
objectsAlreadyRead.Add(objectId);
foreach (var asyncLocal in asyncLocals)
{
var fieldName = asyncLocal["name"].Value<string>();
if (fieldName.EndsWith("__this", StringComparison.Ordinal))
{
asyncLocal["name"] = "this";
asyncLocalsFull.Add(asyncLocal);
}
else if (IsClosureReferenceField(fieldName)) //same code that has on debugger-libs
{
if (DotnetObjectId.TryParse(asyncLocal?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId))
{
if (int.TryParse(dotnetObjectId.Value, out int objectIdToGetInfo) && !objectsAlreadyRead.Contains(objectIdToGetInfo))
{
var asyncLocalsFromObject = await GetObjectValues(sessionId, objectIdToGetInfo, true, false, false, false, token);
var hoistedLocalVariable = await GetHoistedLocalVariables(sessionId, objectIdToGetInfo, asyncLocalsFromObject, token);
asyncLocalsFull = new JArray(asyncLocalsFull.Union(hoistedLocalVariable));
}
}
}
else if (fieldName.StartsWith("<>", StringComparison.Ordinal)) //examples: <>t__builder, <>1__state
{
continue;
}
else if (fieldName.StartsWith('<')) //examples: <code>5__2
{
var match = regexForAsyncLocals.Match(fieldName);
if (match.Success)
asyncLocal["name"] = match.Groups[1].Value;
asyncLocalsFull.Add(asyncLocal);
}
else
{
asyncLocalsFull.Add(asyncLocal);
}
}
return asyncLocalsFull;
}

public async Task<JArray> StackFrameGetValues(SessionId sessionId, MethodInfo method, int thread_id, int frame_id, VarInfo[] varIds, CancellationToken token)
{
var commandParams = new MemoryStream();
Expand All @@ -1729,14 +1786,7 @@ public async Task<JArray> StackFrameGetValues(SessionId sessionId, MethodInfo me
retDebuggerCmdReader.ReadByte(); //ignore type
var objectId = retDebuggerCmdReader.ReadInt32();
var asyncLocals = await GetObjectValues(sessionId, objectId, true, false, false, false, token);
asyncLocals = new JArray(asyncLocals.Where( asyncLocal => !asyncLocal["name"].Value<string>().Contains("<>") || asyncLocal["name"].Value<string>().EndsWith("__this")));
foreach (var asyncLocal in asyncLocals)
{
if (asyncLocal["name"].Value<string>().EndsWith("__this"))
asyncLocal["name"] = "this";
else if (asyncLocal["name"].Value<string>().Contains('<'))
asyncLocal["name"] = Regex.Match(asyncLocal["name"].Value<string>(), @"\<([^)]*)\>").Groups[1].Value;
}
asyncLocals = await GetHoistedLocalVariables(sessionId, objectId, asyncLocals, token);
return asyncLocals;
}

Expand Down
82 changes: 82 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.WebAssembly.Diagnostics;
using Newtonsoft.Json.Linq;
using Xunit;

namespace DebuggerTests
{
public class AsyncTests : DebuggerTestBase
{

// FIXME: method with multiple async blocks - so that we have two separate classes for that method!
// FIXME: nested blocks
// FIXME: Confirm the actual bp location
// FIXME: check object properties..

//FIXME: function name
[Theory]
[InlineData("ContinueWithStaticAsync", "<ContinueWithStaticAsync>b__3_0")]
[InlineData("ContinueWithInstanceAsync", "<ContinueWithInstanceAsync>b__5_0")]
public async Task AsyncLocalsInContinueWith(string method_name, string expected_method_name) => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", method_name, 5, expected_method_name,
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c"),
dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8))
}, "locals");

var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");
});

[Fact]
public async Task AsyncLocalsInContinueWithInstanceUsingThisBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithInstanceUsingThisAsync", 5, "<ContinueWithInstanceUsingThisAsync>b__6_0",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8)),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests")
}, "locals");

var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");

res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "this"), "Date");
await CheckValue(res.Value["result"], TDateTime(new DateTime(2510, 1, 2, 3, 4, 5)), "this.Date");
});

[Fact] // NestedContinueWith
public async Task AsyncLocalsInNestedContinueWithStaticBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "NestedContinueWithStaticAsync", 5, "MoveNext",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
await CheckProps(frame_locals, new
{
t = TObject("System.Threading.Tasks.Task.DelayPromise"),
code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
str = TString("foobar"),
@this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c__DisplayClass4_0"),
ncs_dt0 = TDateTime(new DateTime(3412, 4, 6, 8, 0, 2))
}, "locals");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,27 @@ public async Task EvaluateStaticClassInvalidField() => await CheckInspectLocalsA
(_, res) = await EvaluateOnCallFrame(id, "DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", expect_ok: false);
AssertEqual("Failed to resolve member access for DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
});

[Fact]
public async Task AsyncLocalsInContinueWithBlock() => await CheckInspectLocalsAtBreakpointSite(
"DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithStaticAsync", 4, "<ContinueWithStaticAsync>b__3_0",
"window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
wait_for_event_fn: async (pause_location) =>
{
var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();

await EvaluateOnCallFrameAndCheck(id,
($"t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion")),
($" t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"))
);

await EvaluateOnCallFrameFail(id,
("str", "ReferenceError"),
(" str", "ReferenceError")
);
});

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ public async Task GetObjectValueWithInheritance()
{
var pause_location = await EvaluateAndCheck(
"window.setTimeout(function() { invoke_static_method('[debugger-test] TestChild:TestWatchWithInheritance'); }, 1);",
"dotnet://debugger-test.dll/debugger-test2.cs", 125, 8,
"dotnet://debugger-test.dll/debugger-test2.cs", 127, 8,
"TestWatchWithInheritance");
var frame_id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
var frame_locals = await GetProperties(frame_id);
Expand Down
3 changes: 2 additions & 1 deletion src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,12 +830,13 @@ public async Task GetSourceUsingSourceLink()
public async Task InspectTaskAtLocals() => await CheckInspectLocalsAtBreakpointSite(
"InspectTask",
"RunInspectTask",
7,
10,
"<RunInspectTask>b__0" ,
$"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] InspectTask:RunInspectTask'); }}, 1);",
wait_for_event_fn: async (pause_location) =>
{
var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
CheckNumber(locals, "a", 10);

var t_props = await GetObjectOnLocals(locals, "t");
await CheckProps(t_props, new
Expand Down
97 changes: 97 additions & 0 deletions src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

namespace DebuggerTests.AsyncTests
{
public class ContinueWithTests
{
public DateTime Date => new DateTime(2510, 1, 2, 3, 4, 5);

public static async Task RunAsync()
{
await ContinueWithStaticAsync("foobar");
await new ContinueWithTests().ContinueWithInstanceAsync("foobar");

await NestedContinueWithStaticAsync("foobar");
await new ContinueWithTests().NestedContinueWithInstanceAsync("foobar");
await new ContinueWithTests().ContinueWithInstanceUsingThisAsync("foobar");

}

public static async Task ContinueWithStaticAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}"); //t, code, dt
});
Console.WriteLine ($"done with this method");
}

public static async Task NestedContinueWithStaticAsync(string str)
{
await Task.Delay(500).ContinueWith(async t =>
{
var code = t.Status;
var ncs_dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
Console.WriteLine ($"First continueWith: {code}, {ncs_dt0}"); // t, code, str, dt0
await Task.Delay(300).ContinueWith(t2 =>
{
var ncs_dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"t2: {t2.Status}, str: {str}, {ncs_dt1}, {ncs_dt0}");//t2, dt1, str, dt0
});
});
Console.WriteLine ($"done with this method");
}

public async Task ContinueWithInstanceAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}");// t, code, dt
});
Console.WriteLine ($"done with this method");
}

public async Task ContinueWithInstanceUsingThisAsync(string str)
{
await Task.Delay(1000).ContinueWith(t =>
{
var code = t.Status;
var dt = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"First continueWith: {code}, {dt}, {this.Date}");
});
Console.WriteLine ($"done with this method");
}

[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
public async Task NestedContinueWithInstanceAsync(string str)
{
await Task.Delay(500).ContinueWith(async t =>
{
var code = t.Status;
var dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
if (str == "oi")
{
dt0 = new DateTime(3415, 4, 6, 8, 0, 2);
}
Console.WriteLine ($"First continueWith: {code}, {dt0}, {Date}");//this, t, code, str, dt0
await Task.Delay(300).ContinueWith(t2 =>
{
var dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
Console.WriteLine ($"t2: {t2.Status}, str: {str}, {dt1}, {dt0}");//this, t2, dt1, str, dt0
});
});
Console.WriteLine ($"done with this method");
}

}

}
2 changes: 2 additions & 0 deletions src/mono/wasm/debugger/tests/debugger-test/debugger-test2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public static async System.Threading.Tasks.Task RunInspectTask()
{
await getJsonTask.ContinueWith(t =>
{
int a = 10;
Console.WriteLine(a);
if (t.IsCompletedSuccessfully)
forecasts = t.Result;

Expand Down

0 comments on commit c1351d6

Please sign in to comment.