Skip to content

Commit

Permalink
[wasm][debugger] Support deep member accesses for EvaluteOnCallFrame (#…
Browse files Browse the repository at this point in the history
…40836)

* [wasm][debugger] Support deep member accesses for EvaluteOnCallFrame

Eg. `obj.Property.X`, or `obj.Y + obj.Z.p`

Each of the member access expressions (like `a.b.c`) must be of only
primitive types. Though if the expression is a single member access (and
nothing else, like `"a.b.c"`), then that can be a non-primitive type.

This works by sending the expression to the browser, where it gets
resolved by `library_mono.js`. And that takes an easy route for doing
this, essentially just fetching the list of locals/properties, and using
that.

There are better ways to do this, that will be explored later.

* [wasm][debugger][tests] Remove some debug spew

* [wasm][debugger] fix formatting with dotnet-format
  • Loading branch information
radical authored Aug 18, 2020
1 parent d0efddd commit 907f7da
Show file tree
Hide file tree
Showing 10 changed files with 1,090 additions and 352 deletions.
25 changes: 23 additions & 2 deletions src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ public static MonoCommands GetScopeVariables(int scopeId, params VarInfo[] vars)
return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject(var_ids)})");
}

public static MonoCommands EvaluateMemberAccess(int scopeId, string expr, params VarInfo[] vars)
{
var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray();
return new MonoCommands($"MONO.mono_wasm_eval_member_access({scopeId}, {JsonConvert.SerializeObject(var_ids)}, '', '{expr}')");
}

public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");

public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
Expand Down Expand Up @@ -285,7 +291,7 @@ internal class ExecutionContext
internal DebugStore store;
public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore>();

public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken>();
Dictionary<int, PerScopeCache> perScopeCaches { get; } = new Dictionary<int, PerScopeCache>();

public DebugStore Store
{
Expand All @@ -298,11 +304,26 @@ public DebugStore Store
}
}

public PerScopeCache GetCacheForScope(int scope_id)
{
if (perScopeCaches.TryGetValue(scope_id, out var cache))
return cache;

cache = new PerScopeCache();
perScopeCaches[scope_id] = cache;
return cache;
}

public void ClearState()
{
CallStack = null;
LocalsCache.Clear();
perScopeCaches.Clear();
}
}

internal class PerScopeCache
{
public Dictionary<string, JObject> Locals { get; } = new Dictionary<string, JObject>();
public Dictionary<string, JObject> MemberReferences { get; } = new Dictionary<string, JObject>();
}
}
300 changes: 219 additions & 81 deletions src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.WebAssembly.Diagnostics
{
internal class MemberReferenceResolver
{
private MessageId messageId;
private int scopeId;
private MonoProxy proxy;
private ExecutionContext ctx;
private PerScopeCache scopeCache;
private VarInfo[] varIds;
private ILogger logger;
private bool locals_fetched = false;

public MemberReferenceResolver(MonoProxy proxy, ExecutionContext ctx, MessageId msg_id, int scope_id, ILogger logger)
{
messageId = msg_id;
scopeId = scope_id;
this.proxy = proxy;
this.ctx = ctx;
this.logger = logger;
scopeCache = ctx.GetCacheForScope(scope_id);
}

// Checks Locals, followed by `this`
public async Task<JObject> Resolve(string var_name, CancellationToken token)
{
if (scopeCache.Locals.Count == 0 && !locals_fetched)
{
var scope_res = await proxy.GetScopeProperties(messageId, scopeId, token);
if (scope_res.IsErr)
throw new Exception($"BUG: Unable to get properties for scope: {scopeId}. {scope_res}");
locals_fetched = true;
}

if (scopeCache.Locals.TryGetValue(var_name, out var obj))
{
return obj["value"]?.Value<JObject>();
}

if (scopeCache.MemberReferences.TryGetValue(var_name, out var ret))
return ret;

if (varIds == null)
{
var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scopeId);
varIds = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
}

var res = await proxy.SendMonoCommand(messageId, MonoCommands.EvaluateMemberAccess(scopeId, var_name, varIds), token);
if (res.IsOk)
{
ret = res.Value?["result"]?["value"]?["value"]?.Value<JObject>();
scopeCache.MemberReferences[var_name] = ret;
}
else
{
logger.LogDebug(res.Error.ToString());
}

return ret;
}

}
}
102 changes: 27 additions & 75 deletions src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,9 @@ protected override async Task<bool> AcceptCommand(MessageId id, string method, J
async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
{
if (objectId.Scheme == "scope")
{
return await GetScopeProperties(id, int.Parse(objectId.Value), token);
}

var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token);
if (res.IsErr)
Expand Down Expand Up @@ -673,64 +675,6 @@ async Task<bool> Step(MessageId msg_id, StepKind kind, CancellationToken token)
return true;
}

internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
{
if (ctx.LocalsCache.TryGetValue(expression, out obj))
{
if (only_search_on_this && obj["fromThis"] == null)
return false;
return true;
}
return false;
}

internal async Task<JToken> TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
{
JToken thisValue = null;
var context = GetContext(msg_id);
if (context.CallStack == null)
return null;

if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
return obj;

var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id);
var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
//get_this
var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token);

var scope_values = res.Value?["result"]?["value"]?.Values<JObject>()?.ToArray();
thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value<string>() == "this");

if (!only_search_on_this)
{
if (thisValue != null && expression == "this")
return thisValue;

var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value<string>() == expression);
if (value != null)
return value;
}

//search in scope
if (thisValue != null)
{
if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId))
return null;

res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token);
scope_values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray();
var foundValue = scope_values.FirstOrDefault(v => v["name"].Value<string>() == expression);
if (foundValue != null)
{
foundValue["fromThis"] = true;
context.LocalsCache[foundValue["name"].Value<string>()] = foundValue;
return foundValue;
}
}
return null;
}

async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
{
try
Expand All @@ -739,35 +683,40 @@ async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string ex
if (context.CallStack == null)
return false;

var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token);
var resolver = new MemberReferenceResolver(this, context, msg_id, scope_id, logger);

if (varValue != null)
JObject retValue = await resolver.Resolve(expression, token);
if (retValue == null)
{
retValue = await EvaluateExpression.CompileAndRunTheExpression(expression, resolver, token);
}

if (retValue != null)
{
SendResponse(msg_id, Result.OkFromObject(new
{
result = varValue["value"]
result = retValue
}), token);
return true;
}

string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token);
SendResponse(msg_id, Result.OkFromObject(new
else
{
result = new
{
value = retValue
}
}), token);
return true;
SendResponse(msg_id, Result.Err($"Unable to evaluate {expression}"), token);
}
}
catch (ReturnAsErrorException ree)
{
SendResponse(msg_id, ree.Error, token);
}
catch (Exception e)
{
logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
logger.LogDebug($"Error in EvaluateOnCallFrame for expression '{expression}' with '{e}.");
SendResponse(msg_id, Result.Exception(e), token);
}
return false;

return true;
}

async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
internal async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
{
try
{
Expand All @@ -788,8 +737,11 @@ async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, Cancellati
if (values == null || values.Length == 0)
return Result.OkFromObject(new { result = Array.Empty<object>() });

var frameCache = ctx.GetCacheForScope(scope_id);
foreach (var value in values)
ctx.LocalsCache[value["name"]?.Value<string>()] = value;
{
frameCache.Locals[value["name"]?.Value<string>()] = value;
}

return Result.OkFromObject(new { result = values });
}
Expand Down
Loading

0 comments on commit 907f7da

Please sign in to comment.