[mono][debugger] Support debug inline array (#90026)
authorThays Grazia <thaystg@gmail.com>
Wed, 9 Aug 2023 12:56:55 +0000 (09:56 -0300)
committerGitHub <noreply@github.com>
Wed, 9 Aug 2023 12:56:55 +0000 (09:56 -0300)
* Support debug inline array for wasm

* Removing extra tab

* Changing behavior to use the same protocol version on wasm and on debugger-libs.

* Revert last changes

* Adding changes for debugger-libs support

* Change where we send the information about inline array.

* fix wasm compilation

* Addressing @radical comments
Fixing other tests.

* Fix wrong change.

* Adding more tests as suggested by @radical

src/mono/mono/component/debugger-agent.c
src/mono/mono/component/debugger-protocol.h
src/mono/wasm/debugger/BrowserDebugProxy/JObjectValueCreator.cs
src/mono/wasm/debugger/BrowserDebugProxy/MemberReferenceResolver.cs
src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-array-test.cs

index 91a2ee0..f6e62bf 100644 (file)
@@ -5269,6 +5269,13 @@ buffer_add_value_full (Buffer *buf, MonoType *t, void *addr, MonoDomain *domain,
                                continue;
                        nfields ++;
                }
+               if (CHECK_PROTOCOL_VERSION(2, 65)) {
+                       if (m_class_is_inlinearray (klass) && nfields == 1)
+                               buffer_add_int (buf, m_class_inlinearray_value (klass));
+                       else
+                               buffer_add_int (buf, -1);
+               }
+
                buffer_add_int (buf, nfields);
 
                iter = NULL;
@@ -5290,6 +5297,14 @@ buffer_add_value_full (Buffer *buf, MonoType *t, void *addr, MonoDomain *domain,
                        }
 
                        buffer_add_value_full (buf, f->type, mono_vtype_get_field_addr (addr, f), domain, FALSE, parent_vtypes, len_fixed_array != 1 ? len_fixed_array : isFixedSizeArray(f));
+                       if (CHECK_PROTOCOL_VERSION(2, 65)) {
+                               if (m_class_is_inlinearray (klass) && nfields == 1) {
+                                       int element_size = mono_class_instance_size (mono_class_from_mono_type_internal (f->type)) - MONO_ABI_SIZEOF (MonoObject);
+                                       int array_size = m_class_inlinearray_value (klass);
+                                       for (int i = 1; i < array_size; i++)
+                                               buffer_add_value_full (buf, f->type, ((char*)mono_vtype_get_field_addr (addr, f)) + (i*element_size), domain, FALSE, parent_vtypes, len_fixed_array != 1 ? len_fixed_array : isFixedSizeArray(f));
+                               }
+                       }
                }
 
                if (boxed_vtype) {
@@ -5349,6 +5364,8 @@ decode_vtype (MonoType *t, MonoDomain *domain, gpointer void_addr, gpointer void
        if (CHECK_PROTOCOL_VERSION(2, 61))
                decode_byte (buf, &buf, limit);
        klass = decode_typeid (buf, &buf, limit, &d, &err);
+       if (CHECK_PROTOCOL_VERSION(2, 65))
+               decode_int (buf, &buf, limit); //ignore inline array
        if (err != ERR_NONE)
                return err;
 
@@ -5453,6 +5470,8 @@ decode_vtype_compute_size (MonoType *t, MonoDomain *domain, gpointer void_buf, g
        if (CHECK_PROTOCOL_VERSION(2, 61))
                decode_byte (buf, &buf, limit);
        klass = decode_typeid (buf, &buf, limit, &d, &err);
+       if (CHECK_PROTOCOL_VERSION(2, 65))
+               decode_int (buf, &buf, limit); //ignore inline array
        if (err != ERR_NONE)
                goto end;
 
@@ -5580,6 +5599,8 @@ decode_value_compute_size (MonoType *t, int type, MonoDomain *domain, guint8 *bu
                                decode_byte (buf, &buf, limit);
                                if (CHECK_PROTOCOL_VERSION(2, 61))
                                        decode_byte (buf, &buf, limit); //ignore is boxed
+                               if (CHECK_PROTOCOL_VERSION(2, 65))
+                                       decode_int (buf, &buf, limit); //ignore inline array
                                decode_typeid (buf, &buf, limit, &d, &err);
                                ret += decode_vtype_compute_size (NULL, domain, buf, &buf, limit, from_by_ref_value_type);
                        } else {
@@ -5757,6 +5778,8 @@ decode_value_internal (MonoType *t, int type, MonoDomain *domain, guint8 *addr,
                                if (CHECK_PROTOCOL_VERSION(2, 61))
                                        decode_byte (buf, &buf, limit); //ignore is boxed
                                klass = decode_typeid (buf, &buf, limit, &d, &err);
+                               if (CHECK_PROTOCOL_VERSION(2, 65))
+                                       decode_int (buf, &buf, limit); //ignore inline array
                                if (err != ERR_NONE)
                                        return err;
 
@@ -9155,6 +9178,11 @@ method_commands_internal (int command, MonoMethod *method, MonoDomain *domain, g
                                        buffer_add_int (buf, header->code_size);
                                }
                        }
+                       if (CHECK_PROTOCOL_VERSION (2, 65)) {
+                               for (int i = 0; i < num_locals; ++i) {
+                                       buffer_add_int (buf, locals->locals [i].index);
+                               }
+                       }
                }
                mono_metadata_free_mh (header);
 
@@ -9914,7 +9942,7 @@ frame_commands (int command, guint8 *p, guint8 *end, Buffer *buf)
                                pos = - pos - 1;
                                cmd_stack_frame_get_parameter (frame, sig, pos, buf, jit);
                        } else {
-                               if (!CHECK_PROTOCOL_VERSION (2, 59)) { //from newer protocol versions it's sent the pdb index
+                               if (!CHECK_PROTOCOL_VERSION (2, 59)) { //from older protocol versions it's sent the pdb index
                                        MonoDebugLocalsInfo *locals;
                                        locals = mono_debug_lookup_locals (frame->de.method);
                                        if (locals) {
@@ -9970,7 +9998,7 @@ frame_commands (int command, guint8 *p, guint8 *end, Buffer *buf)
                                var = &jit->params [pos];
                                is_arg = TRUE;
                        } else {
-                               if (!CHECK_PROTOCOL_VERSION (2, 59)) { //from newer protocol versions it's sent the pdb index
+                               if (!CHECK_PROTOCOL_VERSION (2, 59)) { //from older protocol versions it's sent the pdb index
                                        MonoDebugLocalsInfo *locals;
                                        locals = mono_debug_lookup_locals (frame->de.method);
                                        if (locals) {
index 9a8cc5e..9fe3c69 100644 (file)
@@ -11,7 +11,7 @@
  */
 
 #define MAJOR_VERSION 2
-#define MINOR_VERSION 64
+#define MINOR_VERSION 65
 
 typedef enum {
        MDBGPROT_CMD_COMPOSITE = 100
index 3df1c9b..0a9f53c 100644 (file)
@@ -317,6 +317,10 @@ internal sealed class JObjectValueCreator
         var isBoxed = retDebuggerCmdReader.ReadByte() == 1;
         var typeId = retDebuggerCmdReader.ReadInt32();
         var className = await _sdbAgent.GetTypeName(typeId, token);
+        var inlineArraySize = -1;
+        (int MajorVersion, int MinorVersion) = await _sdbAgent.GetVMVersion(token);
+        if (MajorVersion == 2 && MinorVersion >= 65)
+            inlineArraySize = retDebuggerCmdReader.ReadInt32();
         var numValues = retDebuggerCmdReader.ReadInt32();
 
         if (className.StartsWith("System.Nullable<", StringComparison.Ordinal)) //should we call something on debugger-agent to check???
@@ -347,6 +351,7 @@ internal sealed class JObjectValueCreator
                                                     typeId,
                                                     isEnum,
                                                     includeStatic,
+                                                    inlineArraySize,
                                                     token);
         _valueTypes[valueType.Id.Value] = valueType;
         return await valueType.ToJObject(_sdbAgent, forDebuggerDisplayAttribute, token);
index 73bc464..5205ed5 100644 (file)
@@ -395,6 +395,15 @@ namespace Microsoft.WebAssembly.Diagnostics
 
                 switch (objectId.Scheme)
                 {
+                    case "valuetype": //can be an inlined array
+                    {
+                        if (!context.SdbAgent.ValueCreator.TryGetValueTypeById(objectId.Value, out ValueTypeClass valueType))
+                            throw new InvalidOperationException($"Cannot apply indexing with [] to an expression of scheme '{objectId.Scheme}'");
+                        var typeInfo = await context.SdbAgent.GetTypeInfo(valueType.TypeId, token);
+                        if (int.TryParse(elementIdxInfo.ElementIdxStr, out elementIdx) && elementIdx >= 0 && elementIdx < valueType.InlineArray.Count)
+                            return (JObject)valueType.InlineArray[elementIdx]["value"];
+                        throw new InvalidOperationException($"Index is outside the bounds of the inline array");
+                    }
                     case "array":
                         rootObject["value"] = await context.SdbAgent.GetArrayValues(objectId.Value, token);
                         if (!elementIdxInfo.IsMultidimensional)
index f802395..b714233 100644 (file)
@@ -818,7 +818,7 @@ namespace Microsoft.WebAssembly.Diagnostics
 
         private static int debuggerObjectId;
         private static int cmdId = 1; //cmdId == 0 is used by events which come from runtime
-        private const int MINOR_VERSION = 62;
+        private const int MINOR_VERSION = 65;
         private const int MAJOR_VERSION = 2;
 
         private int VmMinorVersion { get; set; }
index 0faca91..3aefe23 100644 (file)
@@ -24,13 +24,13 @@ namespace BrowserDebugProxy
         private bool fieldsExpanded;
         private readonly string className;
         private JArray fields;
-
+        public List<JObject> InlineArray { get; init; }
         public DotnetObjectId Id { get; init; }
         public byte[] Buffer { get; init; }
         public int TypeId { get; init; }
         public bool IsEnum { get; init; }
 
-        public ValueTypeClass(byte[] buffer, string className, JArray fields, int typeId, bool isEnum)
+        public ValueTypeClass(byte[] buffer, string className, JArray fields, int typeId, bool isEnum, List<JObject> inlineArray = null)
         {
             var valueTypeId = MonoSDBHelper.GetNewObjectId();
             var objectId = new DotnetObjectId("valuetype", valueTypeId);
@@ -42,6 +42,7 @@ namespace BrowserDebugProxy
             autoExpand = ShouldAutoExpand(className);
             Id = objectId;
             IsEnum = isEnum;
+            InlineArray = inlineArray;
         }
 
         public override string ToString() => $"{{ ValueTypeClass: typeId: {TypeId}, Id: {Id}, Id: {Id}, fields: {fields} }}";
@@ -54,6 +55,7 @@ namespace BrowserDebugProxy
                                                 int typeId,
                                                 bool isEnum,
                                                 bool includeStatic,
+                                                int inlineArraySize,
                                                 CancellationToken token)
         {
             var typeInfo = await sdbAgent.GetTypeInfo(typeId, token);
@@ -63,6 +65,8 @@ namespace BrowserDebugProxy
             IReadOnlyList<FieldTypeClass> fieldTypes = await sdbAgent.GetTypeFields(typeId, token);
 
             JArray fields = new();
+            List<JObject> inlineArray = null;
+            JObject lastWritableFieldValue = null;
             if (includeStatic)
             {
                 IEnumerable<FieldTypeClass> staticFields =
@@ -77,20 +81,30 @@ namespace BrowserDebugProxy
             IEnumerable<FieldTypeClass> writableFields = fieldTypes
                 .Where(f => !f.Attributes.HasFlag(FieldAttributes.Literal)
                     && !f.Attributes.HasFlag(FieldAttributes.Static));
-
             foreach (var field in writableFields)
             {
-                var fieldValue = await sdbAgent.ValueCreator.ReadAsVariableValue(cmdReader, field.Name, token, true, field.TypeId, false);
-                fields.Add(GetFieldWithMetadata(field, fieldValue, isStatic: false));
+                lastWritableFieldValue = await sdbAgent.ValueCreator.ReadAsVariableValue(cmdReader, field.Name, token, isOwn: true, field.TypeId, forDebuggerDisplayAttribute: false);
+                fields.Add(GetFieldWithMetadata(field, lastWritableFieldValue, isStatic: false));
+            }
+            if (inlineArraySize > 0)
+            {
+                inlineArray = new(inlineArraySize+1);
+                inlineArray.Add(lastWritableFieldValue);
+                var firstFieldtypeId = writableFields.First().TypeId;
+                for (int i = 1; i < inlineArraySize; i++)
+                {
+                    //the valuetype has a single instance field in inline-arrays
+                    var inlineArrayItem = await sdbAgent.ValueCreator.ReadAsVariableValue(cmdReader, $"{i}", token, isOwn: true, firstFieldtypeId, forDebuggerDisplayAttribute: false);
+                    inlineArray.Add(inlineArrayItem);
+                }
             }
-
             long endPos = cmdReader.BaseStream.Position;
             cmdReader.BaseStream.Position = initialPos;
             byte[] valueTypeBuffer = new byte[endPos - initialPos];
             cmdReader.Read(valueTypeBuffer, 0, (int)(endPos - initialPos));
             cmdReader.BaseStream.Position = endPos;
 
-            return new ValueTypeClass(valueTypeBuffer, className, fields, typeId, isEnum);
+            return new ValueTypeClass(valueTypeBuffer, className, fields, typeId, isEnum, inlineArray);
 
             JObject GetFieldWithMetadata(FieldTypeClass field, JObject fieldValue, bool isStatic)
             {
index ff0712e..f94be60 100644 (file)
@@ -670,5 +670,100 @@ namespace DebuggerTests
             CheckNumber(int_arr_3, "1, 2, 1", 121);
             CheckNumber(int_arr_3, "1, 2, 2", 122);
         }
+
+        [ConditionalTheory(nameof(RunningOnChrome))]
+        [InlineData("s")]
+        [InlineData("classWithInlineArrayField.myInlineArray")]
+        [InlineData("classWithInlineArrayField.InlineArrayProp")]
+        [InlineData("classWithInlineArrayField.myStructWithInlineArray.myInlineArray")]
+        public async Task InspectInlineArray(string varName)
+        {
+            var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+            var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+                $"'[debugger-test] DebuggerTests.InlineArray:run'" +
+                "); }, 1);";
+
+            var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, 441, 12, "DebuggerTests.InlineArray.run");
+            var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+            await EvaluateOnCallFrameAndCheck(id,
+                   ($"{varName}[0]", TObject("DebuggerTests.InlineArray.E")),
+                   ($"{varName}[a]", TObject("DebuggerTests.InlineArray.E")),
+                   ($"{varName}[b]", TObject("DebuggerTests.InlineArray.E")),
+                   ($"{varName}[1]", TObject("DebuggerTests.InlineArray.E")));
+            var (_, res) = await EvaluateOnCallFrame(id,$"{varName}[43]", expect_ok: false);
+            Assert.Equal($"Unable to evaluate element access '{varName}[43]': Index is outside the bounds of the inline array", res.Error["result"]?["description"]?.Value<string>());
+
+            var (s_zero, _) = await EvaluateOnCallFrame(id, $"{varName}[0]");
+            await CheckValue(s_zero, TObject("DebuggerTests.InlineArray.E"), nameof(s_zero));
+            var s_zero_props = await GetProperties(s_zero["objectId"]?.Value<string>());
+            await CheckProps(s_zero_props, new
+            {
+                x = TNumber(1),
+                y = TNumber(2),
+                o = TObject("DebuggerTests.InlineArray.One")
+            }, "s_zero_props#1");
+
+            var (s_one, _) = await EvaluateOnCallFrame(id, $"{varName}[1]");
+            await CheckValue(s_one, TObject("DebuggerTests.InlineArray.E"), nameof(s_one));
+            var ss_one_props = await GetProperties(s_one["objectId"]?.Value<string>());
+            await CheckProps(ss_one_props, new
+            {
+                x = TNumber(3),
+                y = TNumber(4),
+                o = TObject("DebuggerTests.InlineArray.Two")
+            }, "s_one_props#1");
+        }
+        [Fact]
+        public async Task InspectInlineArray2()
+        {
+            var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+            var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+                $"'[debugger-test] DebuggerTests.InlineArray:run2'" +
+                "); }, 1);";
+
+            var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, 459, 12, "DebuggerTests.InlineArray.run2");
+            var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+            await EvaluateOnCallFrameAndCheck(id,
+                   ($"a2[0]", TNumber(1)),
+                   ($"a3[0]", TNumber(2)),
+                   ($"a4[0]", TObject("DebuggerTests.InlineArray.E")),
+                   ($"Arr4.myStaticField", TNumber(50))
+                   );
+
+            var (_, res) = await EvaluateOnCallFrame(id,$"a3[1]", expect_ok: false);
+            Assert.Equal($"Unable to evaluate element access 'a3[1]': Index is outside the bounds of the inline array", res.Error["result"]?["description"]?.Value<string>());
+
+            var (s_zero, _) = await EvaluateOnCallFrame(id, $"a4[0]");
+            await CheckValue(s_zero, TObject("DebuggerTests.InlineArray.E"), nameof(s_zero));
+            var s_zero_props = await GetProperties(s_zero["objectId"]?.Value<string>());
+            await CheckProps(s_zero_props, new
+            {
+                x = TNumber(1),
+                y = TNumber(2),
+                o = TObject("DebuggerTests.InlineArray.One")
+            }, "s_zero_props#1");
+
+            var (s_one, _) = await EvaluateOnCallFrame(id, $"a4[1]");
+            await CheckValue(s_one, TObject("DebuggerTests.InlineArray.E"), nameof(s_one));
+            var ss_one_props = await GetProperties(s_one["objectId"]?.Value<string>());
+            await CheckProps(ss_one_props, new
+            {
+                x = TNumber(3),
+                y = TNumber(4),
+                o = TObject("DebuggerTests.InlineArray.Two")
+            }, "s_one_props#1");
+
+            var (s_two, _) = await EvaluateOnCallFrame(id, $"a4");
+            await CheckValue(s_two, TObject("DebuggerTests.InlineArray.Arr4"), nameof(s_two));
+            var s_two_props = await GetProperties(s_two["objectId"]?.Value<string>());
+            await CheckProps(s_two_props, new
+            {
+                myStaticField = TNumber(50),
+                e = TObject("DebuggerTests.InlineArray.E"),
+                Length = TNumber(42)
+            }, "s_two_props#1");
+        }
     }
 }
index f1a2b41..c05329b 100644 (file)
@@ -345,4 +345,119 @@ namespace DebuggerTests
             Console.WriteLine($"int_arr: {int_arr_3.Length}");
         }
     }
+
+    public class InlineArray
+    {
+        struct StructWithInlineArray
+        {
+            public Arr1 myInlineArray;
+        }
+        class ClassWithInlineArrayField
+        {
+            public Arr1 myInlineArray;
+            public Arr1 InlineArrayProp => myInlineArray;
+            public StructWithInlineArray myStructWithInlineArray;
+        }
+        class One {}
+        class Two {}
+        class Three {}
+        class Four {}
+        struct E
+        {
+            public int x;
+            public int y;
+            public object o;
+        }
+
+        [System.Runtime.CompilerServices.InlineArray(Length)]
+        struct Arr1
+        {
+            public const int Length = 42;
+            public E e;
+        }
+
+        [System.Runtime.CompilerServices.InlineArray(Length)]
+        struct Arr2
+        {
+            public const int Length = 42;
+            public int e;
+        }
+        
+        [System.Runtime.CompilerServices.InlineArray(1)]
+        struct Arr3
+        {
+            public int e;
+        }
+
+        [System.Runtime.CompilerServices.InlineArray(Length)]
+        struct Arr4
+        {
+            public static int myStaticField = 50;
+            public const int Length = 42;
+            public E e;
+        }
+
+        private static Arr1 Initialize(Arr1 s)
+        {
+            s[0].o = new One();
+            s[0].x = 1;
+            s[0].y = 2;
+            s[1].o = new Two();
+            s[1].x = 3;
+            s[1].y = 4;
+            s[2].o = new Three();
+            s[2].x = 5;
+            s[2].y = 6;
+            s[3].o = new Four();
+            s[3].x = 7;
+            s[3].y = 8;
+            return s;
+        }
+        public static void run()
+        {
+            int a = 0;
+            int b = 1;
+            Arr1 s = default;
+            s = Initialize(s);
+            ClassWithInlineArrayField classWithInlineArrayField = new ();
+            s = Initialize(classWithInlineArrayField.myInlineArray);
+            classWithInlineArrayField.myInlineArray[0].o = new One();
+            classWithInlineArrayField.myInlineArray[0].x = 1;
+            classWithInlineArrayField.myInlineArray[0].y = 2;
+            classWithInlineArrayField.myInlineArray[1].o = new Two();
+            classWithInlineArrayField.myInlineArray[1].x = 3;
+            classWithInlineArrayField.myInlineArray[1].y = 4;            
+            //classWithInlineArrayField.InlineArrayProp[0].o = new One();
+            //classWithInlineArrayField.InlineArrayProp[0].x = 1;
+            //classWithInlineArrayField.InlineArrayProp[0].y = 2;
+            //classWithInlineArrayField.InlineArrayProp[1].o = new Two();
+            //classWithInlineArrayField.InlineArrayProp[1].x = 3;
+            //classWithInlineArrayField.InlineArrayProp[1].y = 4;
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[0].o = new One();
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[0].x = 1;
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[0].y = 2;
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[1].o = new Two();
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[1].x = 3;
+            classWithInlineArrayField.myStructWithInlineArray.myInlineArray[1].y = 4;
+            System.Diagnostics.Debugger.Break();
+            Console.WriteLine(s[0].o.GetType().Name);
+        }
+
+        public static void run2()
+        {
+            Arr2 a2 = default; //test with primitive type
+            Arr3 a3 = default; //test with length==1
+            Arr4 a4 = default; //test with static field
+            a2[0] = 1; 
+            a3[0] = 2;
+            a4[0].o = new One();
+            a4[0].x = 1;
+            a4[0].y = 2;
+            a4[1].o = new Two();
+            a4[1].x = 3;
+            a4[1].y = 4;
+            Console.WriteLine($"olha thays - {Arr4.myStaticField}");
+            System.Diagnostics.Debugger.Break();
+        }
+    }
 }