Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@

using System;
using Newtonsoft.Json.Linq;
using BrowserDebugProxy;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.WebAssembly.Diagnostics;

internal static class HelperExtensions
{
private const int MaxLogMessageLineLength = 65536;
private static readonly bool TruncateLogMessages = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WASM_DONT_TRUNCATE_LOG_MESSAGES"));
private static Dictionary<ProxyInternalUseProperty, string> proxyInternalUsePropNames = new Dictionary<ProxyInternalUseProperty, string>() {
{ ProxyInternalUseProperty.Hidden, "__hidden" },
{ ProxyInternalUseProperty.State, "__state" },
{ ProxyInternalUseProperty.Section, "__section" },
{ ProxyInternalUseProperty.Owner, "__owner" },
{ ProxyInternalUseProperty.IsStatic, "__isStatic" },
{ ProxyInternalUseProperty.IsNewSlot, "__isNewSlot" },
{ ProxyInternalUseProperty.IsBackingField, "__isBackingField" },
{ ProxyInternalUseProperty.ParentTypeId, "__parentTypeId" }
};
private static Dictionary<string, ProxyInternalUseProperty> proxyInternalUsePropNamesInverse = proxyInternalUsePropNames.ToDictionary((i) => i.Value, (i) => i.Key);

public static string Truncate(this string message, int maxLen, string suffix = "")

Expand All @@ -29,6 +43,9 @@ public static void AddRange(this JArray arr, JArray addedArr)
arr.Add(item);
}

public static string ToUnderscoredString(this ProxyInternalUseProperty key) => proxyInternalUsePropNames[key];
public static bool TryConvertToProxyInternalUseProperty(this string key) => proxyInternalUsePropNamesInverse.TryGetValue(key, out _);

public static bool IsNullValuedObject(this JObject obj)
=> obj != null && obj["type"]?.Value<string>() == "object" && obj["subtype"]?.Value<string>() == "null";
}
82 changes: 59 additions & 23 deletions src/mono/wasm/debugger/BrowserDebugProxy/MemberObjectsExplorer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,17 @@ private static async Task<JObject> ReadFieldValue(
// for backing fields, we are getting it from the properties
typePropertiesBrowsableInfo.TryGetValue(field.Name, out state);
}
fieldValue["__state"] = state?.ToString();
fieldValue["__section"] = field.Attributes.HasFlag(FieldAttributes.Private)
fieldValue[ProxyInternalUseProperty.State.ToUnderscoredString()] = state?.ToString();
fieldValue[ProxyInternalUseProperty.Section.ToUnderscoredString()] = field.Attributes.HasFlag(FieldAttributes.Private)
? "private" : "result";

if (field.IsBackingField)
{
fieldValue["__isBackingField"] = true;
fieldValue["__parentTypeId"] = parentTypeId;
fieldValue[ProxyInternalUseProperty.IsBackingField.ToUnderscoredString()] = true;
fieldValue[ProxyInternalUseProperty.ParentTypeId.ToUnderscoredString()] = parentTypeId;
}
if (field.Attributes.HasFlag(FieldAttributes.Static))
fieldValue["__isStatic"] = true;
fieldValue[ProxyInternalUseProperty.IsStatic.ToUnderscoredString()] = true;

if (getObjectOptions.HasFlag(GetObjectCommandOptions.WithSetter))
{
Expand Down Expand Up @@ -125,6 +125,8 @@ private static async Task<JArray> GetRootHiddenChildren(
if (rootValue?["type"]?.Value<string>() != "object")
return new JArray();

int? maxSize = null;

// unpack object/valuetype
if (rootObjectId.Scheme is "object" or "valuetype")
{
Expand All @@ -150,8 +152,9 @@ private static async Task<JArray> GetRootHiddenChildren(
{
// a collection - expose elements to be of array scheme
var memberNamedItems = members
.Where(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items")
.FirstOrDefault();
.FirstOrDefault(m => m["name"]?.Value<string>() == "Items" || m["name"]?.Value<string>() == "_items");
// sometimes items have dummy empty elements added after real items
maxSize = members.FirstOrDefault(m => m["name"]?.Value<string>() == "_size")?["value"]?["value"]?.Value<int>();
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 doing? And who adds _size, and _items?

Copy link
Member

Choose a reason for hiding this comment

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

Ok, so these are private fields for List<T>. We shouldn't depend on a type's private members like this. What do the "dummy empty elements" look like?

The error condition can be recreated by a = new List<int>(capacity: 20); a.Add(1); -> now the internal array _items will be of size 20, but it will have only 1 real value.

Copy link
Member

Choose a reason for hiding this comment

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

Does VS show all the elements in _items?

Copy link
Member

Choose a reason for hiding this comment

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

btw, you can use list.Capacity to know the size of _items.

Copy link
Member Author

@ilonatommy ilonatommy Oct 12, 2022

Choose a reason for hiding this comment

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

We have 2 behaviors in DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden. The data:

[InlineData("EvaluateBrowsableClass", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)]
[InlineData("EvaluateBrowsableClass", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 10)]
[InlineData("EvaluateBrowsableStruct", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 10)]
[InlineData("EvaluateBrowsableClassStatic", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)]
[InlineData("EvaluateBrowsableClassStatic", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 10)]
[InlineData("EvaluateBrowsableStructStatic", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)]
[InlineData("EvaluateBrowsableStructStatic", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 10)]
[InlineData("EvaluateBrowsableNonAutoPropertiesClass", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 5)]
[InlineData("EvaluateBrowsableNonAutoPropertiesClassStatic", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 5)]

result in producing the information you are talking about. On calling GetObjectMemberValues for listRootHidden in GetRootHiddenChildren we get a publicly available object that looks like this:

  "value": {
    "type": "object",
    "value": null,
    "description": "int[2]",
    "className": "int[]",
    "objectId": "dotnet:array:22",
    "subtype": "array"
  },
  "writable": false,
  "name": "Items",
  "isOwn": true,
  "__section__": "result",
  "__state__": null,
  "__parentTypeId__": 2,
  "__isNewSlot__": false
}

and it does not cause any problems: array has only two elements, it's public and always named Items, so after calling
JArray resultValue = await sdbHelper.GetArrayValues(rootObjectId.Value, token); on it, we will get the items we should expose.
However for these cases:

[InlineData("EvaluateBrowsableStruct", "TestEvaluateFieldsRootHidden", "testFieldsRootHidden", 10)]
[InlineData("EvaluateBrowsableNonAutoPropertiesStruct", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 5)]
[InlineData("EvaluateBrowsableNonAutoPropertiesStructStatic", "TestEvaluatePropertiesRootHidden", "testPropertiesRootHidden", 5)]

we are getting only following private objects:

{
  "value": {
    "type": "object",
    "value": null,
    "description": "int[4]",
    "className": "int[]",
    "objectId": "dotnet:array:46",
    "subtype": "array"
  },
  "writable": false,
  "isOwn": true,
  "name": "_items",
  "__state__": null,
  "__section__": "private"
}
{
  "value": {
    "type": "number",
    "value": 2,
    "description": "2"
  },
  "writable": true,
  "isOwn": true,
  "name": "_size",
  "__state__": null,
  "__section__": "private"
}
{
  "value": {
    "type": "number",
    "value": 2,
    "description": "2"
  },
  "writable": true,
  "isOwn": true,
  "name": "_version",
  "__state__": null,
  "__section__": "private"
}
{
  "value": {
    "type": "object",
    "value": null,
    "description": "int[0]",
    "className": "int[]",
    "objectId": "dotnet:array:66",
    "subtype": "array"
  },
  "writable": false,
  "isOwn": true,
  "name": "s_emptyArray",
  "__state__": null,
  "__section__": "private",
  "__isStatic__": true
}

The only way to get access to children of the hidden root is to call GetArrayValues on private _items object. As you can see, it has 4 elements even though the size of the list == 2, they are like this (empty elements always in the end):

{
  "value": {
    "type": "number",
    "value": 1,
    "description": "1"
  },
  "writable": true,
  "name": "0"
}
{
  "value": {
    "type": "number",
    "value": 2,
    "description": "2"
  },
  "writable": true,
  "name": "1"
}
{
  "value": {
    "type": "number",
    "value": 0,
    "description": "0"
  },
  "writable": true,
  "name": "2"
}
{
  "value": {
    "type": "number",
    "value": 0,
    "description": "0"
  },
  "writable": true,
  "name": "3"
}

In Console Application we see the same data when we expand private members.
image

Copy link
Member Author

@ilonatommy ilonatommy Oct 12, 2022

Choose a reason for hiding this comment

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

btw, you can use list.Capacity to know the size of _items.

In the screenshot above we can se it would not work, capacity is 4 even though our array has only 2 elements. Count is fine but we don't have it in available members.

Copy link
Member Author

@ilonatommy ilonatommy Oct 14, 2022

Choose a reason for hiding this comment

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

ToDo: check what is failing without this change and why it was not failing before clearing the __ fields.

Edit: answer is - EvaluateBrowsableRootHidden is failing on main:

  Failed DebuggerTests.EvaluateOnCallFrameTests.EvaluateBrowsableRootHidden(outerClassName: "EvaluateBrowsableClassStatic", className: "TestEvaluatePropertiesRootHidden", localVarName: "testPropertiesRootHidden", breakLine: 10) [993 ms]
  Error Message:
   Assert.Equal() Failure
Expected: 10
Actual:   8
Failed:     1, Passed:    11, Skipped:     0, Total:    12

Reason:
refListElementsProp has 4 elements while it should have only 2 (it has 2 empty reserved places in memory as discussed before and they are being returned along with legible list members).

What now:
I will make a separate PR for it and revert the changes here. We will merge the other PR first.

if (memberNamedItems is not null &&
(DotnetObjectId.TryParse(memberNamedItems["value"]?["objectId"]?.Value<string>(), out DotnetObjectId itemsObjectId)) &&
itemsObjectId.Scheme == "array")
Expand All @@ -164,6 +167,8 @@ private static async Task<JArray> GetRootHiddenChildren(
if (rootObjectId.Scheme == "array")
{
JArray resultValue = await sdbHelper.GetArrayValues(rootObjectId.Value, token);
if (maxSize is not null)
resultValue = new JArray(resultValue.Take(maxSize.Value));

// root hidden item name has to be unique, so we concatenate the root's name to it
foreach (var item in resultValue)
Expand Down Expand Up @@ -282,6 +287,8 @@ public static async Task<JArray> GetExpandedMemberValues(
{
if (state is DebuggerBrowsableState.RootHidden)
{
if (namePrefix == "valueTypeEnumRootHidden")
Console.WriteLine($"namePrefix = {namePrefix}; typeName = {typeName}");
if (MonoSDBHelper.IsPrimitiveType(typeName))
return GetHiddenElement();

Expand Down Expand Up @@ -361,14 +368,14 @@ public static async Task<Dictionary<string, JObject>> ExpandPropertyValues(
continue;
}

bool isExistingMemberABackingField = existingMember["__isBackingField"]?.Value<bool>() == true;
bool isExistingMemberABackingField = existingMember[ProxyInternalUseProperty.IsBackingField.ToUnderscoredString()]?.Value<bool>() == true;
if (isOwn && !isExistingMemberABackingField)
{
// repeated propname on the same type! cannot happen
throw new Exception($"Internal Error: should not happen. propName: {propName}. Existing all members: {string.Join(",", allMembers.Keys)}");
}

bool isExistingMemberABackingFieldOwnedByThisType = isExistingMemberABackingField && existingMember["__owner"]?.Value<string>() == typeName;
bool isExistingMemberABackingFieldOwnedByThisType = isExistingMemberABackingField && existingMember[ProxyInternalUseProperty.Owner.ToUnderscoredString()]?.Value<string>() == typeName;
if (isExistingMemberABackingField && (isOwn || isExistingMemberABackingFieldOwnedByThisType))
{
// this is the property corresponding to the backing field in *this* type
Expand All @@ -382,8 +389,8 @@ public static async Task<Dictionary<string, JObject>> ExpandPropertyValues(
{
// this has `new` keyword if it is newSlot but direct child was not a newSlot:
var child = allMembers.FirstOrDefault(
kvp => (kvp.Key == propName || kvp.Key.StartsWith($"{propName} (")) && kvp.Value["__parentTypeId"]?.Value<int>() == typeId).Value;
bool wasOverriddenByDerivedType = child != null && child["__isNewSlot"]?.Value<bool>() != true;
kvp => (kvp.Key == propName || kvp.Key.StartsWith($"{propName} (")) && kvp.Value[ProxyInternalUseProperty.ParentTypeId.ToUnderscoredString()]?.Value<int>() == typeId).Value;
bool wasOverriddenByDerivedType = child != null && child[ProxyInternalUseProperty.IsNewSlot.ToUnderscoredString()]?.Value<bool>() != true;
if (wasOverriddenByDerivedType)
{
/*
Expand All @@ -409,7 +416,7 @@ public static async Task<Dictionary<string, JObject>> ExpandPropertyValues(
*/

JObject backingFieldForHiddenProp = allMembers.GetValueOrDefault(overriddenOrHiddenPropName);
if (backingFieldForHiddenProp is null || backingFieldForHiddenProp["__isBackingField"]?.Value<bool>() != true)
if (backingFieldForHiddenProp is null || backingFieldForHiddenProp[ProxyInternalUseProperty.IsBackingField.ToUnderscoredString()]?.Value<bool>() != true)
{
// hiding with a non-auto property, so nothing to adjust
// add the new property
Expand All @@ -424,12 +431,12 @@ public static async Task<Dictionary<string, JObject>> ExpandPropertyValues(

async Task UpdateBackingFieldWithPropertyAttributes(JObject backingField, string autoPropName, MethodAttributes getterMemberAccessAttrs, DebuggerBrowsableState? state)
{
backingField["__section"] = getterMemberAccessAttrs switch
backingField[ProxyInternalUseProperty.Section.ToUnderscoredString()] = getterMemberAccessAttrs switch
{
MethodAttributes.Private => "private",
_ => "result"
};
backingField["__state"] = state?.ToString();
backingField[ProxyInternalUseProperty.State.ToUnderscoredString()] = state?.ToString();

if (state is null)
return;
Expand Down Expand Up @@ -472,16 +479,16 @@ async Task AddProperty(
}

propRet["isOwn"] = isOwn;
propRet["__section"] = getterAttrs switch
propRet[ProxyInternalUseProperty.Section.ToUnderscoredString()] = getterAttrs switch
{
MethodAttributes.Private => "private",
_ => "result"
};
propRet["__state"] = state?.ToString();
propRet[ProxyInternalUseProperty.State.ToUnderscoredString()] = state?.ToString();
if (parentTypeId != -1)
{
propRet["__parentTypeId"] = parentTypeId;
propRet["__isNewSlot"] = isNewSlot;
propRet[ProxyInternalUseProperty.ParentTypeId.ToUnderscoredString()] = parentTypeId;
propRet[ProxyInternalUseProperty.IsNewSlot.ToUnderscoredString()] = isNewSlot;
}

string namePrefix = GetNamePrefixForValues(propNameWithSufix, typeName, isOwn, state);
Expand Down Expand Up @@ -586,7 +593,7 @@ public static async Task<GetMembersResult> GetObjectMemberValues(
if (getCommandType.HasFlag(GetObjectCommandOptions.AccessorPropertiesOnly))
{
foreach (var f in allFields)
f["__hidden"] = true;
f[ProxyInternalUseProperty.Hidden.ToUnderscoredString()] = true;
}
AddOnlyNewFieldValuesByNameTo(allFields, allMembers, typeName, isOwn);
}
Expand Down Expand Up @@ -632,8 +639,8 @@ static void AddOnlyNewFieldValuesByNameTo(JArray namedValues, IDictionary<string
if (valuesDict.TryAdd(name, item as JObject))
{
// new member
if (item["__isBackingField"]?.Value<bool>() == true)
item["__owner"] = typeName;
if (item[ProxyInternalUseProperty.IsBackingField.ToUnderscoredString()]?.Value<bool>() == true)
item[ProxyInternalUseProperty.Owner.ToUnderscoredString()] = typeName;
continue;
}

Expand All @@ -650,6 +657,18 @@ static void AddOnlyNewFieldValuesByNameTo(JArray namedValues, IDictionary<string

}

public enum ProxyInternalUseProperty
{
Hidden,
State,
Section,
Owner,
IsStatic,
IsNewSlot,
IsBackingField,
ParentTypeId
}

internal sealed class GetMembersResult
{
// public / protected / internal:
Expand All @@ -676,6 +695,23 @@ public GetMembersResult(JArray value, bool sortByAccessLevel)
PrivateMembers = t.PrivateMembers;
}

public void CleanUp()
{
CleanUpJArray(Result);
CleanUpJArray(PrivateMembers);
static void CleanUpJArray(JArray arr)
{
foreach(var item in arr)
{
item.Children().Where(x =>
x is JProperty p &&
p.Name.TryConvertToProxyInternalUseProperty())
.ToList()
.ForEach(x => x.Remove());
}
}
}

public static GetMembersResult FromValues(IEnumerable<JToken> values, bool splitMembersByAccessLevel = false) =>
FromValues(new JArray(values), splitMembersByAccessLevel);

Expand All @@ -694,10 +730,10 @@ public static GetMembersResult FromValues(JArray values, bool splitMembersByAcce

private void Split(JToken member)
{
if (member["__hidden"]?.Value<bool>() == true)
if (member[ProxyInternalUseProperty.Hidden.ToUnderscoredString()]?.Value<bool>() == true)
return;

if (member["__section"]?.Value<string>() is not string section)
if (member[ProxyInternalUseProperty.Section.ToUnderscoredString()]?.Value<string>() is not string section)
{
Result.Add(member);
return;
Expand Down
2 changes: 2 additions & 0 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ internal async Task<ValueOrError<GetMembersResult>> RuntimeGetObjectMembers(Sess
case "valuetype":
var resValue = await MemberObjectsExplorer.GetValueTypeMemberValues(
context.SdbAgent, objectId.Value, getObjectOptions, token, sortByAccessLevel, includeStatic: true);
resValue?.CleanUp();
return resValue switch
{
null => ValueOrError<GetMembersResult>.WithError($"Could not get properties for {objectId}"),
Expand All @@ -779,6 +780,7 @@ internal async Task<ValueOrError<GetMembersResult>> RuntimeGetObjectMembers(Sess
case "object":
var resObj = await MemberObjectsExplorer.GetObjectMemberValues(
context.SdbAgent, objectId.Value, getObjectOptions, token, sortByAccessLevel, includeStatic: true);
resObj.CleanUp();
return ValueOrError<GetMembersResult>.WithValue(resObj);
case "pointer":
var resPointer = new JArray { await context.SdbAgent.GetPointerContent(objectId.Value, token) };
Expand Down
6 changes: 3 additions & 3 deletions src/mono/wasm/debugger/BrowserDebugProxy/ValueTypeClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ JObject GetFieldWithMetadata(FieldTypeClass field, JObject fieldValue, bool isSt
if (isStatic)
fieldValue["name"] = field.Name;
FieldAttributes attr = field.Attributes & FieldAttributes.FieldAccessMask;
fieldValue["__section"] = attr == FieldAttributes.Private ? "private" : "result";
fieldValue[ProxyInternalUseProperty.Section.ToUnderscoredString()] = attr == FieldAttributes.Private ? "private" : "result";

if (field.IsBackingField)
{
fieldValue["__isBackingField"] = true;
fieldValue[ProxyInternalUseProperty.IsBackingField.ToUnderscoredString()] = true;
return fieldValue;
}
typeFieldsBrowsableInfo.TryGetValue(field.Name, out DebuggerBrowsableState? state);
fieldValue["__state"] = state?.ToString();
fieldValue[ProxyInternalUseProperty.State.ToUnderscoredString()] = state?.ToString();
return fieldValue;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1181,9 +1181,11 @@ public async Task EvaluateBrowsableRootHidden(
var (refList, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.list");
var refListProp = await GetProperties(refList["objectId"]?.Value<string>());
var list = refListProp
.Where(v => v["name"]?.Value<string>() == "Items" || v["name"]?.Value<string>() == "_items")
.FirstOrDefault();
.FirstOrDefault(v => v["name"]?.Value<string>() == "Items" || v["name"]?.Value<string>() == "_items");
var refListElementsProp = await GetProperties(list["value"]["objectId"]?.Value<string>());
int? listMaxSize = refListProp.FirstOrDefault(m => m["name"]?.Value<string>() == "_size")?["value"]?["value"]?.Value<int>();
if (listMaxSize is not null)
refListElementsProp = new JArray(refListElementsProp.Take(listMaxSize.Value));

var (refArray, _) = await EvaluateOnCallFrame(id, "testPropertiesNone.array");
var refArrayProp = await GetProperties(refArray["objectId"]?.Value<string>());
Expand Down