Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm][debugger] Improvements in debugging in async methods #78651

Merged
merged 12 commits into from
Nov 29, 2022
31 changes: 31 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ internal sealed class MethodInfo
private ParameterInfo[] _parametersInfo;
public int KickOffMethod { get; }
internal bool IsCompilerGenerated { get; }
private readonly AsyncScopeDebugInformation[] _asyncScopes;

public MethodInfo(AssemblyInfo assembly, string methodName, int methodToken, TypeInfo type, MethodAttributes attrs)
{
Expand All @@ -361,6 +362,7 @@ public MethodInfo(AssemblyInfo assembly, string methodName, int methodToken, Typ
this.TypeInfo = type;
TypeInfo.Methods.Add(this);
assembly.Methods[methodToken] = this;
_asyncScopes = Array.Empty<AsyncScopeDebugInformation>();
}

public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle, int token, SourceFile source, TypeInfo type, MetadataReader asmMetadataReader, MetadataReader pdbMetadataReader)
Expand Down Expand Up @@ -447,7 +449,34 @@ public MethodInfo(AssemblyInfo assembly, MethodDefinitionHandle methodDefHandle,
DebuggerAttrInfo.ClearInsignificantAttrFlags();
}
if (pdbMetadataReader != null)
{
localScopes = pdbMetadataReader.GetLocalScopes(methodDefHandle);
byte[] scopeDebugInformation =
(from cdiHandle in pdbMetadataReader.GetCustomDebugInformation(methodDefHandle)
let cdi = pdbMetadataReader.GetCustomDebugInformation(cdiHandle)
where pdbMetadataReader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.StateMachineHoistedLocalScopes
select pdbMetadataReader.GetBlobBytes(cdi.Value)).FirstOrDefault();

if (scopeDebugInformation != null)
{
_asyncScopes = new AsyncScopeDebugInformation[scopeDebugInformation.Length / 8];
for (int i = 0; i < _asyncScopes.Length; i++)
{
int scopeOffset = BitConverter.ToInt32(scopeDebugInformation, i * 8);
int scopeLen = BitConverter.ToInt32(scopeDebugInformation, (i * 8) + 4);
_asyncScopes[i] = new AsyncScopeDebugInformation(scopeOffset, scopeOffset + scopeLen);
}
}

_asyncScopes ??= Array.Empty<AsyncScopeDebugInformation>();
}
}

public bool ContainsAsyncScope(int oneBasedIdx, int offset)
{
int arrIdx = oneBasedIdx - 1;
return arrIdx >= 0 && arrIdx < _asyncScopes.Length &&
offset >= _asyncScopes[arrIdx].StartOffset && offset <= _asyncScopes[arrIdx].EndOffset;
}

public ParameterInfo[] GetParametersInfo()
Expand Down Expand Up @@ -617,6 +646,8 @@ public override int GetHashCode(MethodInfo loc)
return loc.Source.Id;
}
}

private record struct AsyncScopeDebugInformation(int StartOffset, int EndOffset);
}

internal sealed class ParameterInfo
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1406,7 +1406,7 @@ internal async Task<GetMembersResult> GetScopeProperties(SessionId msg_id, int s

VarInfo[] varIds = scope.Method.Info.GetLiveVarsAt(scope.Location.IlLocation.Offset);

var values = await context.SdbAgent.StackFrameGetValues(scope.Method, context.ThreadId, scopeId, varIds, token);
var values = await context.SdbAgent.StackFrameGetValues(scope.Method, context.ThreadId, scopeId, varIds, scope.Location.IlLocation.Offset, token);
if (values != null)
{
if (values == null || values.Count == 0)
Expand Down
42 changes: 37 additions & 5 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,9 @@ internal sealed class MonoSDBHelper
private SessionId sessionId;

internal readonly ILogger logger;
private static readonly Regex regexForAsyncLocals = new (@"\<([^)]*)\>", RegexOptions.Singleline);
private static readonly Regex regexForAsyncLocals = new(@"\<([^)]*)\>([^)]*)([_][_])([0-9]*)", RegexOptions.Singleline); //<testCSharpScope>5__1
private static readonly Regex regexForVBAsyncLocals = new(@"\$VB\$ResumableLocal_([^)]*)\$([0-9]*)", RegexOptions.Singleline); //$VB$ResumableLocal_testVbScope$2
private static readonly Regex regexForVBAsyncMethodName = new(@"VB\$StateMachine_([0-9]*)_([^)]*)", RegexOptions.Singleline); //VB$StateMachine_2_RunVBScope
private static readonly Regex regexForAsyncMethodName = new (@"\<([^>]*)\>([d][_][_])([0-9]*)", RegexOptions.Compiled);
private static readonly Regex regexForGenericArgs = new (@"[`][0-9]+", RegexOptions.Compiled);
private static readonly Regex regexForNestedLeftRightAngleBrackets = new ("^(((?'Open'<)[^<>]*)+((?'Close-Open'>)[^<>]*)+)*(?(Open)(?!))[^<>]*", RegexOptions.Compiled);
Expand Down Expand Up @@ -1285,6 +1287,11 @@ public async Task<string> GetPrettyMethodName(int methodId, bool isAnonymous, Ca
if (anonymousMethodId.LastIndexOf('_') >= 0)
anonymousMethodId = klassName.Substring(klassName.LastIndexOf('_') + 1);
}
else if (klassName.StartsWith("VB$"))
{
var match = regexForVBAsyncMethodName.Match(klassName);
ret = ret.Insert(0, match.Groups[2].Value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check match.Success .

}
else
{
var matchOnClassName = regexForNestedLeftRightAngleBrackets.Match(klassName);
Expand Down Expand Up @@ -1965,7 +1972,7 @@ private static bool IsClosureReferenceField (string fieldName)
fieldName.StartsWith ("<>8__", StringComparison.Ordinal);
}

public async Task<JArray> GetHoistedLocalVariables(int objectId, IEnumerable<JToken> asyncLocals, CancellationToken token)
public async Task<JArray> GetHoistedLocalVariables(MethodInfoWithDebugInformation method, int objectId, IEnumerable<JToken> asyncLocals, int offset, CancellationToken token)
{
JArray asyncLocalsFull = new JArray();
List<int> objectsAlreadyRead = new();
Expand All @@ -1986,7 +1993,7 @@ public async Task<JArray> GetHoistedLocalVariables(int objectId, IEnumerable<JTo
{
var asyncProxyMembersFromObject = await MemberObjectsExplorer.GetObjectMemberValues(
this, dotnetObjectId.Value, GetObjectCommandOptions.WithProperties, token);
var hoistedLocalVariable = await GetHoistedLocalVariables(dotnetObjectId.Value, asyncProxyMembersFromObject.Flatten(), token);
var hoistedLocalVariable = await GetHoistedLocalVariables(method, dotnetObjectId.Value, asyncProxyMembersFromObject.Flatten(), offset, token);
asyncLocalsFull = new JArray(asyncLocalsFull.Union(hoistedLocalVariable));
}
}
Expand All @@ -1999,9 +2006,34 @@ public async Task<JArray> GetHoistedLocalVariables(int objectId, IEnumerable<JTo
{
var match = regexForAsyncLocals.Match(fieldName);
if (match.Success)
{
if (!method.Info.ContainsAsyncScope(Convert.ToInt32(match.Groups[4].Value), offset))
continue;
asyncLocal["name"] = match.Groups[1].Value;
}
asyncLocalsFull.Add(asyncLocal);
}
//VB language
else if (fieldName.StartsWith("$VB$Local_", StringComparison.Ordinal))
{
asyncLocal["name"] = fieldName.Remove(0, 10);
asyncLocalsFull.Add(asyncLocal);
}
else if (fieldName.StartsWith("$VB$ResumableLocal_", StringComparison.Ordinal))
{
var match = regexForVBAsyncLocals.Match(fieldName);
if (match.Success)
{
if (!method.Info.ContainsAsyncScope(Convert.ToInt32(match.Groups[2].Value) + 1, offset))
continue;
asyncLocal["name"] = match.Groups[1].Value;
}
asyncLocalsFull.Add(asyncLocal);
}
else if (fieldName.StartsWith("$"))
{
continue;
}
else
{
asyncLocalsFull.Add(asyncLocal);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be unconditional? It seems to be common, for the cases we don't continue earlier.

Expand All @@ -2010,7 +2042,7 @@ public async Task<JArray> GetHoistedLocalVariables(int objectId, IEnumerable<JTo
return asyncLocalsFull;
}

public async Task<JArray> StackFrameGetValues(MethodInfoWithDebugInformation method, int thread_id, int frame_id, VarInfo[] varIds, CancellationToken token)
public async Task<JArray> StackFrameGetValues(MethodInfoWithDebugInformation method, int thread_id, int frame_id, VarInfo[] varIds, int offset, CancellationToken token)
{
using var commandParamsWriter = new MonoBinaryWriter();
commandParamsWriter.Write(thread_id);
Expand All @@ -2027,7 +2059,7 @@ public async Task<JArray> StackFrameGetValues(MethodInfoWithDebugInformation met
retDebuggerCmdReader.ReadByte(); //ignore type
var objectId = retDebuggerCmdReader.ReadInt32();
GetMembersResult asyncProxyMembers = await MemberObjectsExplorer.GetObjectMemberValues(this, objectId, GetObjectCommandOptions.WithProperties, token, includeStatic: true);
var asyncLocals = await GetHoistedLocalVariables(objectId, asyncProxyMembers.Flatten(), token);
var asyncLocals = await GetHoistedLocalVariables(method, objectId, asyncProxyMembers.Flatten(), offset, token);
return asyncLocals;
}

Expand Down
72 changes: 72 additions & 0 deletions src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,77 @@ public async Task AsyncLocalsInNestedContinueWithStaticBlock() => await CheckIns
ncs_dt0 = TDateTime(new DateTime(3412, 4, 6, 8, 0, 2))
}, "locals");
});

[Theory]
[InlineData("Run", 246, 16, 252, 16, "RunCSharpScope")]
[InlineData("RunContinueWith", 277, 20, 283, 20, "RunContinueWithSameVariableName")]
[InlineData("RunNestedContinueWith", 309, 24, 315, 24, "RunNestedContinueWithSameVariableName.AnonymousMethod__1")]
[InlineData("RunNonAsyncMethod", 334, 16, 340, 16, "RunNonAsyncMethodSameVariableName")]
public async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod_CSharp(string method_to_run, int line1, int col1, int line2, int col2, string func_to_pause)
=> await InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(
$"[debugger-test] DebuggerTests.AsyncTests.VariablesWithSameNameDifferentScopes:{method_to_run}",
"dotnet://debugger-test.dll/debugger-async-test.cs",
line1,
col1,
line2,
col2,
$"DebuggerTests.AsyncTests.VariablesWithSameNameDifferentScopes.{func_to_pause}",
"testCSharpScope");

[Theory]
[InlineData("[debugger-test-vb] DebuggerTestVB.TestVbScope:Run", 14, 12, 22, 12, "DebuggerTestVB.TestVbScope.RunVBScope", "testVbScope")]
public async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod_VB(string method_to_run, int line1, int col1, int line2, int col2, string func_to_pause, string variable_to_inspect)
=> await InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(
method_to_run,
"dotnet://debugger-test-vb.dll/debugger-test-vb.vb",
line1,
col1,
line2,
col2,
func_to_pause,
variable_to_inspect);

private async Task InspectLocalsWithSameNameInDifferentScopesInAsyncMethod(string method_to_run, string source_to_pause, int line1, int col1, int line2, int col2, string func_to_pause, string variable_to_inspect)
{
var expression = $"{{ invoke_static_method('{method_to_run}'); }}";

await EvaluateAndCheck(
"window.setTimeout(function() {" + expression + "; }, 1);",
source_to_pause, line1, col1,
func_to_pause,
locals_fn: async (locals) =>
{
await CheckString(locals, variable_to_inspect, "hello");
await CheckString(locals, "onlyInFirstScope", "only-in-first-scope");
Assert.False(locals.Any(jt => jt["name"]?.Value<string>() == "onlyInSecondScope"));
}
);
await StepAndCheck(StepKind.Resume, source_to_pause, line2, col2, func_to_pause,
locals_fn: async (locals) =>
{
await CheckString(locals, variable_to_inspect, "hi");
await CheckString(locals, "onlyInSecondScope", "only-in-second-scope");
Assert.False(locals.Any(jt => jt["name"]?.Value<string>() == "onlyInFirstScope"));
}
);
}

[Fact]
public async Task InspectLocalsInAsyncVBMethod()
{
var expression = $"{{ invoke_static_method('[debugger-test-vb] DebuggerTestVB.TestVbScope:Run'); }}";

await EvaluateAndCheck(
"window.setTimeout(function() {" + expression + "; }, 1);",
"dotnet://debugger-test-vb.dll/debugger-test-vb.vb", 14, 12,
"DebuggerTestVB.TestVbScope.RunVBScope",
locals_fn: async (locals) =>
{
await CheckString(locals, "testVbScope", "hello");
CheckNumber(locals, "a", 10);
CheckNumber(locals, "data", 10);
}
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Public Class TestVbScope
Public Shared Async Function Run() As Task
Await RunVBScope(10)
Await RunVBScope(1000)
End Function

Public Shared Async Function RunVBScope(data As Integer) As Task(Of Integer)
Dim a As Integer
a = 10
If data < 999 Then
Dim testVbScope As String
Dim onlyInFirstScope As String
testVbScope = "hello"
onlyInFirstScope = "only-in-first-scope"
System.Diagnostics.Debugger.Break()
Await Task.Delay(1)
Return data
Else
Dim testVbScope As String
Dim onlyInSecondScope As String
testVbScope = "hi"
onlyInSecondScope = "only-in-second-scope"
System.Diagnostics.Debugger.Break()
Await Task.Delay(1)
Return data
End If

End Function

End Class
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<RootNamespace>DebuggerTestVB</RootNamespace>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>

</Project>
114 changes: 114 additions & 0 deletions src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,118 @@ await Task.Delay(300).ContinueWith(t2 =>
}
}

public class VariablesWithSameNameDifferentScopes
{
public static async Task Run()
{
await RunCSharpScope(10);
await RunCSharpScope(1000);
}

public static async Task<string> RunCSharpScope(int number)
{
await Task.Delay(1);
if (number < 999)
{
string testCSharpScope = "hello"; string onlyInFirstScope = "only-in-first-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
else
{
string testCSharpScope = "hi"; string onlyInSecondScope = "only-in-second-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
}

public static async Task RunContinueWith()
{
await RunContinueWithSameVariableName(10);
await RunContinueWithSameVariableName(1000);
}

public static async Task RunNestedContinueWith()
{
await RunNestedContinueWithSameVariableName(10);
await RunNestedContinueWithSameVariableName(1000);
}

public static async Task RunContinueWithSameVariableName(int number)
{
await Task.Delay(500).ContinueWith(async t =>
{
await Task.Delay(1);
if (number < 999)
{
var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
else
{
var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
});
Console.WriteLine ($"done with this method");
}

public static async Task RunNestedContinueWithSameVariableName(int number)
{
await Task.Delay(500).ContinueWith(async t =>
{
if (number < 999)
{
var testCSharpScope = new String("hello_out"); string onlyInFirstScope = "only-in-first-scope_out";
Console.WriteLine(testCSharpScope);
}
else
{
var testCSharpScope = new String("hi_out"); string onlyInSecondScope = "only-in-second-scope_out";
Console.WriteLine(testCSharpScope);
}
await Task.Delay(300).ContinueWith(t2 =>
{
if (number < 999)
{
var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
else
{
var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
});
});
Console.WriteLine ($"done with this method");
}

public static void RunNonAsyncMethod()
{
RunNonAsyncMethodSameVariableName(10);
RunNonAsyncMethodSameVariableName(1000);
}

public static string RunNonAsyncMethodSameVariableName(int number)
{
if (number < 999)
{
var testCSharpScope = new String("hello"); string onlyInFirstScope = "only-in-first-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
else
{
var testCSharpScope = new String("hi"); string onlyInSecondScope = "only-in-second-scope";
System.Diagnostics.Debugger.Break();
return testCSharpScope;
}
}
}

}
Loading