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] View multidimensional array when debugging #60983

Merged
merged 9 commits into from
Nov 16, 2021
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 @@ -555,7 +555,7 @@ private async Task<bool> CallOnFunction(MessageId id, JObject args, Cancellation
args["details"] = await SdbHelper.GetPointerContent(id, int.Parse(objectId.Value), token);
break;
case "array":
args["details"] = await SdbHelper.GetArrayValues(id, int.Parse(objectId.Value), token);
args["details"] = await SdbHelper.GetArrayValuesProxy(id, int.Parse(objectId.Value), token);
break;
case "cfo_res":
{
Expand Down
90 changes: 81 additions & 9 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,55 @@ internal enum StepSize
Line
}

internal class ArrayDimensions
{
public int Rank { get; }
public List<int> Bounds { get; }

public ArrayDimensions(int rank)
{
Rank = rank;
Bounds = new List<int>(rank);
}
public int TotalLength()
{
int ret = 1;
for (int i = 0 ; i < Rank ; i++)
{
ret *= Bounds[i];
}
return ret;
}
public override string ToString()
{
return $"{string.Join(", ", Bounds.ToArray())}";
}
public string GetArrayIndexString(int idx)
{
int boundLimit = 1;
int lastBoundLimit = 1;
int[] arrayStr = new int[Rank];
int rankStart = 0;
Copy link
Member

Choose a reason for hiding this comment

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

Check for idx < 0, and idx >= TotalLength

while (idx > 0)
{
boundLimit = 1;
for (int i = Rank - 1; i >= rankStart; i--)
{
lastBoundLimit = boundLimit;
boundLimit *= Bounds[i];
if (idx < boundLimit)
{
arrayStr[i] = (int)(idx / lastBoundLimit);
idx -= arrayStr[i] * lastBoundLimit;
rankStart = i;
break;
}
}
}
return $"{string.Join(", ", arrayStr)}";
}
}

internal record MethodInfoWithDebugInformation(MethodInfo Info, int DebugId, string Name);

internal class TypeInfoWithDebugInformation
Expand Down Expand Up @@ -1400,7 +1449,8 @@ public async Task<string> GetTypeName(SessionId sessionId, int typeId, Cancellat
string className = await GetTypeNameOriginal(sessionId, typeId, token);
className = className.Replace("+", ".");
className = Regex.Replace(className, @"`\d+", "");
className = className.Replace("[]", "__SQUARED_BRACKETS__");
className = Regex.Replace(className, @"[[, ]+]", "__SQUARED_BRACKETS__");
//className = className.Replace("[]", "__SQUARED_BRACKETS__");
className = className.Replace("[", "<");
className = className.Replace("]", ">");
className = className.Replace("__SQUARED_BRACKETS__", "[]");
Expand Down Expand Up @@ -1451,15 +1501,20 @@ public async Task<string> GetStringValue(SessionId sessionId, int string_id, Can
}
return null;
}
public async Task<int> GetArrayLength(SessionId sessionId, int object_id, CancellationToken token)
public async Task<ArrayDimensions> GetArrayLength(SessionId sessionId, int object_id, CancellationToken token)
Copy link
Member

Choose a reason for hiding this comment

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

rename to GetArrayDimensions

{
var commandParams = new MemoryStream();
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(object_id);
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdArray>(sessionId, CmdArray.GetLength, commandParams, token);
var length = retDebuggerCmdReader.ReadInt32();
length = retDebuggerCmdReader.ReadInt32();
return length;
var arrDimensions = new ArrayDimensions(length);
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 var rank = new int[length] then new ArrayDimensions(rank) and forgo the resizing?

Copy link
Member

Choose a reason for hiding this comment

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

And populate rank, before new ArrayDimensions, so TotalLength can be precomputed too. And ArrayDimensions can be a record.

Copy link
Member Author

Choose a reason for hiding this comment

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

done.

for (int i = 0 ; i < length; i++)
{
arrDimensions.Bounds.Add(retDebuggerCmdReader.ReadInt32());
retDebuggerCmdReader.ReadInt32();
Copy link
Member

Choose a reason for hiding this comment

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

What is this reading? comment would be helpful

}
return arrDimensions;
}
public async Task<List<int>> GetTypeIdFromObject(SessionId sessionId, int object_id, bool withParents, CancellationToken token)
{
Expand Down Expand Up @@ -1778,9 +1833,14 @@ public async Task<JObject> CreateJObjectForString(SessionId sessionId, MonoBinar
public async Task<JObject> CreateJObjectForArray(SessionId sessionId, MonoBinaryReader retDebuggerCmdReader, CancellationToken token)
{
var objectId = retDebuggerCmdReader.ReadInt32();
var value = await GetClassNameFromObject(sessionId, objectId, token);
var className = await GetClassNameFromObject(sessionId, objectId, token);
var arrayType = className.ToString();
var length = await GetArrayLength(sessionId, objectId, token);
return CreateJObject<string>(null, "object", $"{value.ToString()}({length})", false, value.ToString(), "dotnet:array:" + objectId, null, "array");
if (arrayType.LastIndexOf('[') > 0)
arrayType = arrayType.Insert(arrayType.LastIndexOf('[')+1, length.ToString());
if (className.LastIndexOf('[') > 0)
className = className.Insert(arrayType.LastIndexOf('[')+1, new string(',', length.Rank-1));
return CreateJObject<string>(null, "object", arrayType, false, className.ToString(), "dotnet:array:" + objectId, null, length.Rank == 1 ? "array" : null);
Copy link
Member

Choose a reason for hiding this comment

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

nit: param names, a bit confusing to read otherwise

}

public async Task<JObject> CreateJObjectForObject(SessionId sessionId, MonoBinaryReader retDebuggerCmdReader, int typeIdFromAttribute, bool forDebuggerDisplayAttribute, CancellationToken token)
Expand Down Expand Up @@ -2211,16 +2271,28 @@ public async Task<JArray> GetArrayValues(SessionId sessionId, int arrayId, Cance
var commandParamsWriter = new MonoBinaryWriter(commandParams);
commandParamsWriter.Write(arrayId);
commandParamsWriter.Write(0);
commandParamsWriter.Write(length);
commandParamsWriter.Write(length.TotalLength());
var retDebuggerCmdReader = await SendDebuggerAgentCommand<CmdArray>(sessionId, CmdArray.GetValues, commandParams, token);
JArray array = new JArray();
for (int i = 0 ; i < length ; i++)
for (int i = 0 ; i < length.TotalLength(); i++)
{
var var_json = await CreateJObjectForVariableValue(sessionId, retDebuggerCmdReader, i.ToString(), false, -1, false, token);
var var_json = await CreateJObjectForVariableValue(sessionId, retDebuggerCmdReader, length.GetArrayIndexString(i), false, -1, false, token);
Copy link
Member

Choose a reason for hiding this comment

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

nit: param name for the bools at least

array.Add(var_json);
}
return array;
}

public async Task<JObject> GetArrayValuesProxy(SessionId sessionId, int arrayId, CancellationToken token)
{
var length = await GetArrayLength(sessionId, arrayId, token);
var arrayProxy = JObject.FromObject(new
{
items = await GetArrayValues(sessionId, arrayId, token),
Copy link
Member

Choose a reason for hiding this comment

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

GetArrayValues also calls GetArrayLength. Instead maybe GetArrayValues can return (JArray, ArrayDimensions), or it can take a `ArrayDimensions argument?

dimensionsDetails = length.Bounds.ToArray()
});
return arrayProxy;
}

public async Task<bool> EnableExceptions(SessionId sessionId, PauseOnExceptionsKind state, CancellationToken token)
{
if (state == PauseOnExceptionsKind.Unset)
Expand Down
107 changes: 80 additions & 27 deletions src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,24 @@ public async Task InspectGenericValueTypeArrayLocals2(int line, int col, string
frame_idx: frame_idx,
use_cfo: use_cfo);

async Task<JToken> GetObjectWithCFO(string objectId, JObject fn_args = null)
{
var fn_decl = "function () { return this; }";
var cfo_args = JObject.FromObject(new
{
functionDeclaration = fn_decl,
objectId = objectId
});

if (fn_args != null)
cfo_args["arguments"] = fn_args;

// callFunctionOn
var result = await cli.SendCommand("Runtime.callFunctionOn", cfo_args, token);

return await GetProperties(result.Value["result"]["objectId"]?.Value<string>(), fn_args);
}

async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, string method_name, string etype_name,
string local_var_name_prefix, object[] array, object[] array_elem_props,
bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false)
Expand All @@ -215,8 +233,8 @@ async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, st

var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
Assert.Equal(4, locals.Count());
CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]", array?.Length ?? 0);
CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]", 0);
CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]", $"{etype_name}[{array?.Length ?? 0}]");
CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]", $"{etype_name}[0]");
CheckObject(locals, $"{local_var_name_prefix}_arr_null", $"{etype_name}[]", is_null: true);
CheckBool(locals, "call_other", test_prev_frame);

Expand Down Expand Up @@ -264,24 +282,6 @@ async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, st

var props = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], $"{local_var_name_prefix}_arr_empty");
await CheckProps(props, new object[0], "${local_var_name_prefix}_arr_empty");

async Task<JToken> GetObjectWithCFO(string objectId, JObject fn_args = null)
{
var fn_decl = "function () { return this; }";
var cfo_args = JObject.FromObject(new
{
functionDeclaration = fn_decl,
objectId = objectId
});

if (fn_args != null)
cfo_args["arguments"] = fn_args;

// callFunctionOn
var result = await cli.SendCommand("Runtime.callFunctionOn", cfo_args, token);

return await GetProperties(result.Value["result"]["objectId"]?.Value<string>(), fn_args);
}
}

[Theory]
Expand Down Expand Up @@ -313,10 +313,10 @@ public async Task InspectObjectArrayMembers(bool use_cfo)
await CheckProps(c_props, new
{
id = TString("c#id"),
ClassArrayProperty = TArray("DebuggerTests.SimpleClass[]", 3),
ClassArrayField = TArray("DebuggerTests.SimpleClass[]", 3),
PointsProperty = TArray("DebuggerTests.Point[]", 2),
PointsField = TArray("DebuggerTests.Point[]", 2)
ClassArrayProperty = TArray("DebuggerTests.SimpleClass[]", "DebuggerTests.SimpleClass[3]"),
ClassArrayField = TArray("DebuggerTests.SimpleClass[]", "DebuggerTests.SimpleClass[3]"),
PointsProperty = TArray("DebuggerTests.Point[]", "DebuggerTests.Point[2]"),
PointsField = TArray("DebuggerTests.Point[]", "DebuggerTests.Point[2]")
},
"c"
);
Expand Down Expand Up @@ -382,8 +382,8 @@ public async Task InspectValueTypeArrayLocalsStaticAsync(bool use_cfo)
await CheckProps(frame_locals, new
{
call_other = TBool(false),
gvclass_arr = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", 2),
gvclass_arr_empty = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]"),
gvclass_arr = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[2]"),
gvclass_arr_empty = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[0]"),
gvclass_arr_null = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", is_null: true),
gvclass = TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
// BUG: this shouldn't be null!
Expand Down Expand Up @@ -448,7 +448,7 @@ public async Task InspectValueTypeArrayLocalsInstanceAsync(bool use_cfo)
{
t1 = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
@this = TObject("DebuggerTests.ArrayTestsClass"),
point_arr = TArray("DebuggerTests.Point[]", 2),
point_arr = TArray("DebuggerTests.Point[]", "DebuggerTests.Point[2]"),
point = TValueType("DebuggerTests.Point")
}, "InspectValueTypeArrayLocalsInstanceAsync#locals");

Expand Down Expand Up @@ -642,6 +642,59 @@ public async Task InvalidAccessors() => await CheckInspectLocalsAtBreakpointSite
AssertEqual("undefined", res.Value["result"]?["type"]?.ToString(), "Expected to get undefined result for non-existant accessor");
}
});

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task InspectPrimitiveTypeMultiArrayLocals(bool use_cfo)
{
var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";

var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
$"'[debugger-test] DebuggerTests.MultiDimensionalArray:run'" +
"); }, 1);";

var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, 343, 12, "run");

var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
Assert.Equal(3, locals.Count());
var int_arr_1 = !use_cfo ?
await GetProperties(locals[0]["value"]["objectId"].Value<string>()) :
await GetObjectWithCFO((locals[0]["value"]["objectId"].Value<string>()));

CheckNumber(int_arr_1, "0", 0);
CheckNumber(int_arr_1, "1", 1);
var int_arr_2 = !use_cfo ?
await GetProperties(locals[1]["value"]["objectId"].Value<string>()) :
await GetObjectWithCFO((locals[1]["value"]["objectId"].Value<string>()));
CheckNumber(int_arr_2, "0, 0", 0);
CheckNumber(int_arr_2, "0, 1", 1);
CheckNumber(int_arr_2, "0, 2", 2);
CheckNumber(int_arr_2, "1, 0", 10);
CheckNumber(int_arr_2, "1, 1", 11);
CheckNumber(int_arr_2, "1, 2", 12);

var int_arr_3 = !use_cfo ?
await GetProperties(locals[2]["value"]["objectId"].Value<string>()) :
await GetObjectWithCFO((locals[2]["value"]["objectId"].Value<string>()));
CheckNumber(int_arr_3, "0, 0, 0", 0);
CheckNumber(int_arr_3, "0, 0, 1", 1);
CheckNumber(int_arr_3, "0, 0, 2", 2);
CheckNumber(int_arr_3, "0, 1, 0", 10);
CheckNumber(int_arr_3, "0, 1, 1", 11);
CheckNumber(int_arr_3, "0, 1, 2", 12);
CheckNumber(int_arr_3, "0, 2, 0", 20);
CheckNumber(int_arr_3, "0, 2, 1", 21);
CheckNumber(int_arr_3, "0, 2, 2", 22);
CheckNumber(int_arr_3, "1, 0, 0", 100);
CheckNumber(int_arr_3, "1, 0, 1", 101);
CheckNumber(int_arr_3, "1, 0, 2", 102);
CheckNumber(int_arr_3, "1, 1, 0", 110);
CheckNumber(int_arr_3, "1, 1, 1", 111);
CheckNumber(int_arr_3, "1, 1, 2", 112);
CheckNumber(int_arr_3, "1, 2, 0", 120);
CheckNumber(int_arr_3, "1, 2, 1", 121);
CheckNumber(int_arr_3, "1, 2, 2", 122);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class AssignmentTests : DebuggerTestBase
{ "MONO_TYPE_CHAR", TSymbol("0 '\u0000'"), TSymbol("97 'a'") },
{ "MONO_TYPE_STRING", TString(default), TString("hello") },
{ "MONO_TYPE_ENUM", TEnum("DebuggerTests.RGB", "Red"), TEnum("DebuggerTests.RGB", "Blue") },
{ "MONO_TYPE_ARRAY", TObject("byte[]", is_null: true), TArray("byte[]", 2) },
{ "MONO_TYPE_ARRAY", TObject("byte[]", is_null: true), TArray("byte[]", "byte[2]") },
{ "MONO_TYPE_VALUETYPE", TValueType("DebuggerTests.Point"), TValueType("DebuggerTests.Point") },
{ "MONO_TYPE_VALUETYPE2", TValueType("System.Decimal","0"), TValueType("System.Decimal", "1.1") },
{ "MONO_TYPE_GENERICINST", TObject("System.Func<int>", is_null: true), TDelegate("System.Func<int>", "int Prepare ()") },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ public async Task RunOnJSObject(bool roundtrip) => await RunCallFunctionOn(
await CheckProps(obj_own_val, new
{
a_obj = TObject("Object"),
b_arr = TArray("Array", 2)
b_arr = TArray("Array", "Array(2)")
}, "obj_own");
});

Expand Down Expand Up @@ -641,7 +641,7 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li
// Check arrays through getters

res = await InvokeGetter(obj, get_args_fn(new[] { "IntArray" }), cfo_fn);
await CheckValue(res.Value["result"], TArray("int[]", 2), $"{local_name}.IntArray");
await CheckValue(res.Value["result"], TArray("int[]", "int[2]"), $"{local_name}.IntArray");
{
var arr_elems = await GetProperties(res.Value["result"]?["objectId"]?.Value<string>());
var exp_elems = new[]
Expand All @@ -654,7 +654,7 @@ public async Task PropertyGettersTest(string eval_fn, string method_name, int li
}

res = await InvokeGetter(obj, get_args_fn(new[] { "DTArray" }), cfo_fn);
await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), $"{local_name}.DTArray");
await CheckValue(res.Value["result"], TArray("System.DateTime[]", "System.DateTime[2]"), $"{local_name}.DTArray");
{
var dt0 = new DateTime(6, 7, 8, 9, 10, 11);
var dt1 = new DateTime(1, 2, 3, 4, 5, 6);
Expand Down Expand Up @@ -944,7 +944,7 @@ async Task CheckCFOResult(Result result)
if (res_array_len < 0)
await CheckValue(result.Value["result"], TObject("Object"), $"cfo-res");
else
await CheckValue(result.Value["result"], TArray("Array", res_array_len), $"cfo-res");
await CheckValue(result.Value["result"], TArray("Array", $"Array({res_array_len})"), $"cfo-res");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public async Task UsingDebuggerTypeProxy()
var props = await GetObjectOnFrame(frame, "myList");
Assert.Equal(1, props.Count());

CheckArray(props, "Items", "int[]", 4);
CheckArray(props, "Items", "int[]", "int[4]");

CheckObject(locals, "b", "DebuggerTests.WithProxy", description:"DebuggerTests.WithProxy");
props = await GetObjectOnFrame(frame, "b");
Expand Down
Loading