[wasm][debugger] Improvements in debugging in async methods (#78651)
authorThays Grazia <thaystg@gmail.com>
Tue, 29 Nov 2022 19:47:17 +0000 (16:47 -0300)
committerGitHub <noreply@github.com>
Tue, 29 Nov 2022 19:47:17 +0000 (16:47 -0300)
* Fixing 2 variables with same names in different scopes in a async method.

* Implementing support to see local variables in async methods in VB code, and also fixing 2 variables with same name in differente scopes, and also fixing method name for async methods in VB.

* Adding Test case as suggested by @radical

* Refactor Scope class

* slight test changes

* Split the tests, to be more readable in code, and in test results

* cleanup

* addressing radical comments

* addressing radical comments, fix test case

* addressing radical comments

* addressing @radical comments

Co-authored-by: Ankit Jain <radical@gmail.com>
src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs
src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vb [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vbproj [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-test.csproj

index e07fdd7..b2abc22 100644 (file)
@@ -350,6 +350,7 @@ namespace Microsoft.WebAssembly.Diagnostics
         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)
         {
@@ -361,6 +362,7 @@ namespace Microsoft.WebAssembly.Diagnostics
             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)
@@ -447,7 +449,34 @@ namespace Microsoft.WebAssembly.Diagnostics
                 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()
@@ -617,6 +646,8 @@ namespace Microsoft.WebAssembly.Diagnostics
                 return loc.Source.Id;
             }
         }
+
+        private record struct AsyncScopeDebugInformation(int StartOffset, int EndOffset);
     }
 
     internal sealed class ParameterInfo
index 45c772a..b47698e 100644 (file)
@@ -1406,7 +1406,7 @@ namespace Microsoft.WebAssembly.Diagnostics
 
                 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)
index f5d9dc0..4b61432 100644 (file)
@@ -804,7 +804,9 @@ namespace Microsoft.WebAssembly.Diagnostics
         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);
@@ -1285,6 +1287,14 @@ namespace Microsoft.WebAssembly.Diagnostics
                             if (anonymousMethodId.LastIndexOf('_') >= 0)
                                 anonymousMethodId = klassName.Substring(klassName.LastIndexOf('_') + 1);
                         }
+                        else if (klassName.StartsWith("VB$"))
+                        {
+                            var match = regexForVBAsyncMethodName.Match(klassName);
+                            if (match.Success)
+                                ret = ret.Insert(0, match.Groups[2].Value);
+                            else
+                                ret = ret.Insert(0, klassName);
+                        }
                         else
                         {
                             var matchOnClassName = regexForNestedLeftRightAngleBrackets.Match(klassName);
@@ -1965,7 +1975,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                         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();
@@ -1976,7 +1986,6 @@ namespace Microsoft.WebAssembly.Diagnostics
                 if (fieldName.EndsWith("__this", StringComparison.Ordinal))
                 {
                     asyncLocal["name"] = "this";
-                    asyncLocalsFull.Add(asyncLocal);
                 }
                 else if (IsClosureReferenceField(fieldName)) //same code that has on debugger-libs
                 {
@@ -1986,10 +1995,11 @@ namespace Microsoft.WebAssembly.Diagnostics
                         {
                             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));
                         }
                     }
+                    continue;
                 }
                 else if (fieldName.StartsWith("<>", StringComparison.Ordinal)) //examples: <>t__builder, <>1__state
                 {
@@ -1999,18 +2009,37 @@ namespace Microsoft.WebAssembly.Diagnostics
                 {
                     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);
+                    }
                 }
-                else
+                //VB language
+                else if (fieldName.StartsWith("$VB$Local_", StringComparison.Ordinal))
+                {
+                    asyncLocal["name"] = fieldName.Remove(0, 10);
+                }
+                else if (fieldName.StartsWith("$VB$ResumableLocal_", StringComparison.Ordinal))
                 {
-                    asyncLocalsFull.Add(asyncLocal);
+                    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;
+                    }
+                }
+                else if (fieldName.StartsWith("$"))
+                {
+                    continue;
                 }
+                asyncLocalsFull.Add(asyncLocal);
             }
             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);
@@ -2027,7 +2056,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                 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;
             }
 
index 17d29d6..96dc8f2 100644 (file)
@@ -81,5 +81,77 @@ namespace DebuggerTests
                      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);
+                }
+            );
+        }
     }
 }
diff --git a/src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vb b/src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vb
new file mode 100644 (file)
index 0000000..4ca35c3
--- /dev/null
@@ -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
diff --git a/src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vbproj b/src/mono/wasm/debugger/tests/debugger-test-vb/debugger-test-vb.vbproj
new file mode 100644 (file)
index 0000000..c54512c
--- /dev/null
@@ -0,0 +1,8 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <RootNamespace>DebuggerTestVB</RootNamespace>
+    <TargetFramework>net7.0</TargetFramework>
+  </PropertyGroup>
+
+</Project>
index da30a31..3c103d3 100644 (file)
@@ -230,4 +230,118 @@ namespace DebuggerTests.AsyncTests
         }
     }
 
+    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;
+            }
+        }
+    }
+
 }
index ea37306..099f5da 100644 (file)
@@ -28,6 +28,7 @@
     <ProjectReference Include="..\debugger-test-with-source-link\debugger-test-with-source-link.csproj" ReferenceOutputAssembly="false" Private="true" />
     <ProjectReference Include="..\debugger-test-without-debug-symbols-to-load\debugger-test-without-debug-symbols-to-load.csproj" Private="true" />
     <ProjectReference Include="..\debugger-test-with-non-user-code-class\debugger-test-with-non-user-code-class.csproj" Private="true" />
+    <ProjectReference Include="..\debugger-test-vb\debugger-test-vb.vbproj" Private="true" />
     <!-- loaded by *tests*, and not the test app -->
     <ProjectReference Include="..\lazy-debugger-test-embedded\lazy-debugger-test-embedded.csproj" ReferenceOutputAssembly="false" Private="true" />
 
@@ -62,6 +63,7 @@
       <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-with-source-link.dll" />
       <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-without-debug-symbols-to-load.dll" />
       <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-with-non-user-code-class.dll" />
+      <WasmAssembliesToBundle Include="$(OutDir)\debugger-test-vb.dll" />
       <WasmAssembliesToBundle Include="$(MicrosoftNetCoreAppRuntimePackRidDir)\lib\$(NetCoreappCurrent)\System.Runtime.InteropServices.JavaScript.dll" />
 
       <!-- Assemblies only dynamically loaded -->