-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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] Improvement of memory consumption while Evaluating Expressions #61470
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
72ad108
Fix memory consumption.
thaystg d65373b
Merge branch 'dotnet:main' into thays_fix_memory_evaluation
thaystg 9755954
Fix debugger-tests
thaystg 3e832bf
Fix compilation.
thaystg 498e2c1
Addressing @lewing PR.
thaystg d505d40
Address @lewing comment
thaystg 69ad647
Addressing @radical comment.
thaystg d8e3e57
Addressing comments.
thaystg 0128938
Addressing @radical comments.
thaystg 194ee4f
missing return.
thaystg 36e7e56
Addressing @radical comments
thaystg 0a686ec
Adding test case
thaystg 97b98ce
Fixing tests.
thaystg bd552fd
Adding another test case. Thanks @lewing.
thaystg 351f897
Reuse the script.
thaystg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,17 +12,28 @@ | |
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Scripting; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Emit; | ||
using Microsoft.CodeAnalysis.Scripting; | ||
using Newtonsoft.Json; | ||
using Newtonsoft.Json.Linq; | ||
using System.Text.RegularExpressions; | ||
|
||
namespace Microsoft.WebAssembly.Diagnostics | ||
{ | ||
internal static class EvaluateExpression | ||
{ | ||
internal static Script<object> script = CSharpScript.Create( | ||
"", | ||
ScriptOptions.Default.WithReferences( | ||
typeof(object).Assembly, | ||
typeof(Enumerable).Assembly, | ||
typeof(JObject).Assembly | ||
)); | ||
private class FindVariableNMethodCall : CSharpSyntaxWalker | ||
{ | ||
private static Regex regexForReplaceVarName = new Regex(@"[^A-Za-z0-9_]", RegexOptions.Singleline); | ||
public List<IdentifierNameSyntax> identifiers = new List<IdentifierNameSyntax>(); | ||
public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>(); | ||
public List<MemberAccessExpressionSyntax> memberAccesses = new List<MemberAccessExpressionSyntax>(); | ||
|
@@ -32,6 +43,7 @@ private class FindVariableNMethodCall : CSharpSyntaxWalker | |
private int visitCount; | ||
public bool hasMethodCalls; | ||
public bool hasElementAccesses; | ||
internal List<string> variableDefinitions = new List<string>(); | ||
|
||
public void VisitInternal(SyntaxNode node) | ||
{ | ||
|
@@ -97,7 +109,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
{ | ||
// Generate a random suffix | ||
string suffix = Guid.NewGuid().ToString().Substring(0, 5); | ||
string prefix = ma_str.Trim().Replace(".", "_"); | ||
string prefix = regexForReplaceVarName.Replace(ma_str, "_"); | ||
id_name = $"{prefix}_{suffix}"; | ||
|
||
memberAccessToParamName[ma_str] = id_name; | ||
|
@@ -114,7 +126,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
{ | ||
// Generate a random suffix | ||
string suffix = Guid.NewGuid().ToString().Substring(0, 5); | ||
string prefix = iesStr.Trim().Replace(".", "_").Replace("(", "_").Replace(")", "_"); | ||
string prefix = regexForReplaceVarName.Replace(iesStr, "_"); | ||
id_name = $"{prefix}_{suffix}"; | ||
methodCallToParamName[iesStr] = id_name; | ||
} | ||
|
@@ -138,7 +150,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
return SyntaxFactory.IdentifierName(id_name); | ||
}); | ||
|
||
var paramsSet = new HashSet<string>(); | ||
var localsSet = new HashSet<string>(); | ||
|
||
// 2. For every unique member ref, add a corresponding method param | ||
if (ma_values != null) | ||
|
@@ -151,15 +163,15 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); | ||
} | ||
memberAccessValues[id_name] = value; | ||
root = UpdateWithNewMethodParam(root, id_name, value); | ||
AddLocalVariableWithValue(id_name, value); | ||
} | ||
} | ||
|
||
if (id_values != null) | ||
{ | ||
foreach ((IdentifierNameSyntax idns, JObject value) in identifiers.Zip(id_values)) | ||
{ | ||
root = UpdateWithNewMethodParam(root, idns.Identifier.Text, value); | ||
AddLocalVariableWithValue(idns.Identifier.Text, value); | ||
} | ||
} | ||
|
||
|
@@ -172,7 +184,7 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
{ | ||
throw new Exception($"BUG: Expected to find an id name for the member access string: {node_str}"); | ||
} | ||
root = UpdateWithNewMethodParam(root, id_name, value); | ||
AddLocalVariableWithValue(id_name, value); | ||
} | ||
} | ||
|
||
|
@@ -185,96 +197,73 @@ public SyntaxTree ReplaceVars(SyntaxTree syntaxTree, IEnumerable<JObject> ma_val | |
{ | ||
throw new Exception($"BUG: Expected to find an id name for the element access string: {node_str}"); | ||
} | ||
root = UpdateWithNewMethodParam(root, id_name, value); | ||
AddLocalVariableWithValue(id_name, value); | ||
} | ||
} | ||
|
||
return syntaxTree.WithRootAndOptions(root, syntaxTree.Options); | ||
|
||
CompilationUnitSyntax UpdateWithNewMethodParam(CompilationUnitSyntax root, string id_name, JObject value) | ||
void AddLocalVariableWithValue(string idName, JObject value) | ||
{ | ||
var classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; | ||
var method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; | ||
|
||
if (paramsSet.Contains(id_name)) | ||
{ | ||
// repeated member access expression | ||
// eg. this.a + this.a | ||
return root; | ||
} | ||
|
||
argValues.Add(ConvertJSToCSharpType(value)); | ||
|
||
MethodDeclarationSyntax updatedMethod = method.AddParameterListParameters( | ||
SyntaxFactory.Parameter( | ||
SyntaxFactory.Identifier(id_name)) | ||
.WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value)))); | ||
|
||
paramsSet.Add(id_name); | ||
root = root.ReplaceNode(method, updatedMethod); | ||
|
||
return root; | ||
if (localsSet.Contains(idName)) | ||
return; | ||
localsSet.Add(idName); | ||
variableDefinitions.Add(ConvertJSToCSharpLocalVariableAssignment(idName, value)); | ||
} | ||
} | ||
|
||
private object ConvertJSToCSharpType(JToken variable) | ||
private string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) | ||
{ | ||
string typeRet; | ||
object valueRet; | ||
JToken value = variable["value"]; | ||
string type = variable["type"].Value<string>(); | ||
string subType = variable["subtype"]?.Value<string>(); | ||
|
||
string objectId = variable["objectId"]?.Value<string>(); | ||
switch (type) | ||
radical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
case "string": | ||
return value?.Value<string>(); | ||
{ | ||
var str = value?.Value<string>(); | ||
str = str.Replace("\"", "\\\""); | ||
valueRet = $"\"{str}\""; | ||
typeRet = "string"; | ||
break; | ||
} | ||
case "number": | ||
return value?.Value<double>(); | ||
valueRet = value?.Value<double>(); | ||
typeRet = "double"; | ||
break; | ||
case "boolean": | ||
return value?.Value<bool>(); | ||
valueRet = value?.Value<string>().ToLower(); | ||
typeRet = "bool"; | ||
break; | ||
case "object": | ||
return variable; | ||
valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" | ||
+ $"type = \"{type}\"" | ||
+ $", description = \"{variable["description"].Value<string>()}\"" | ||
+ $", className = \"{variable["className"].Value<string>()}\"" | ||
+ (subType != null ? $", subtype = \"{subType}\"" : "") | ||
+ (objectId != null ? $", objectId = \"{objectId}\"" : "") | ||
+ "})"; | ||
typeRet = "object"; | ||
break; | ||
case "void": | ||
return null; | ||
} | ||
throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); | ||
} | ||
|
||
private string GetTypeFullName(JToken variable) | ||
{ | ||
string type = variable["type"].ToString(); | ||
string subType = variable["subtype"]?.Value<string>(); | ||
object value = ConvertJSToCSharpType(variable); | ||
|
||
switch (type) | ||
{ | ||
case "object": | ||
{ | ||
if (subType == "null") | ||
lewing marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return variable["className"].Value<string>(); | ||
else | ||
return "object"; | ||
} | ||
case "void": | ||
return "object"; | ||
valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" | ||
+ $"type = \"object\"," | ||
+ $"description = \"object\"," | ||
+ $"className = \"object\"," | ||
+ $"subtype = \"null\"" | ||
+ "})"; | ||
typeRet = "object"; | ||
break; | ||
Comment on lines
+242
to
+259
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand these two.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
default: | ||
return value.GetType().FullName; | ||
throw new Exception($"Evaluate of this datatype {type} not implemented yet");//, "Unsupported"); | ||
} | ||
throw new ReturnAsErrorException($"GetTypefullName: Evaluate of this datatype {type} not implemented yet", "Unsupported"); | ||
return $"{typeRet} {idName} = {valueRet};"; | ||
} | ||
} | ||
|
||
private static SyntaxNode GetExpressionFromSyntaxTree(SyntaxTree syntaxTree) | ||
{ | ||
CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot(); | ||
ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax; | ||
MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax; | ||
BlockSyntax blockValue = methodDeclaration.Body; | ||
ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax; | ||
ParenthesizedExpressionSyntax expressionParenthesized = returnValue.Expression as ParenthesizedExpressionSyntax; | ||
|
||
return expressionParenthesized?.Expression; | ||
} | ||
|
||
private static async Task<IList<JObject>> ResolveMemberAccessExpressions(IEnumerable<MemberAccessExpressionSyntax> member_accesses, | ||
MemberReferenceResolver resolver, CancellationToken token) | ||
{ | ||
|
@@ -335,32 +324,20 @@ private static async Task<IList<JObject>> ResolveElementAccess(IEnumerable<Eleme | |
return values; | ||
} | ||
|
||
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", | ||
Justification = "Suppressing the warning until gets fixed, see https://github.com/dotnet/runtime/issues/51202")] | ||
internal static async Task<JObject> CompileAndRunTheExpression(string expression, MemberReferenceResolver resolver, CancellationToken token) | ||
{ | ||
expression = expression.Trim(); | ||
if (!expression.StartsWith('(')) | ||
{ | ||
expression = "(" + expression + ")"; | ||
} | ||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@" | ||
using System; | ||
public class CompileAndRunTheExpression | ||
{ | ||
public static object Evaluate() | ||
{ | ||
return " + expression + @"; | ||
} | ||
}", cancellationToken: token); | ||
SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(expression + @";", cancellationToken: token); | ||
|
||
SyntaxNode expressionTree = GetExpressionFromSyntaxTree(syntaxTree); | ||
SyntaxNode expressionTree = syntaxTree.GetCompilationUnitRoot(token); | ||
if (expressionTree == null) | ||
throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); | ||
|
||
FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall(); | ||
findVarNMethodCall.VisitInternal(expressionTree); | ||
|
||
// this fails with `"a)"` | ||
// because the code becomes: return (a)); | ||
// and the returned expression from GetExpressionFromSyntaxTree is `a`! | ||
|
@@ -388,7 +365,7 @@ public static object Evaluate() | |
|
||
if (findVarNMethodCall.hasMethodCalls) | ||
{ | ||
expressionTree = GetExpressionFromSyntaxTree(syntaxTree); | ||
expressionTree = syntaxTree.GetCompilationUnitRoot(token); | ||
|
||
findVarNMethodCall.VisitInternal(expressionTree); | ||
|
||
|
@@ -400,7 +377,7 @@ public static object Evaluate() | |
// eg. "elements[0]" | ||
if (findVarNMethodCall.hasElementAccesses) | ||
{ | ||
expressionTree = GetExpressionFromSyntaxTree(syntaxTree); | ||
expressionTree = syntaxTree.GetCompilationUnitRoot(token); | ||
|
||
findVarNMethodCall.VisitInternal(expressionTree); | ||
|
||
|
@@ -409,48 +386,21 @@ public static object Evaluate() | |
syntaxTree = findVarNMethodCall.ReplaceVars(syntaxTree, null, null, null, elementAccessValues); | ||
} | ||
|
||
expressionTree = GetExpressionFromSyntaxTree(syntaxTree); | ||
expressionTree = syntaxTree.GetCompilationUnitRoot(token); | ||
if (expressionTree == null) | ||
throw new Exception($"BUG: Unable to evaluate {expression}, could not get expression from the syntax tree"); | ||
|
||
MetadataReference[] references = new MetadataReference[] | ||
{ | ||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location), | ||
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location) | ||
}; | ||
|
||
CSharpCompilation compilation = CSharpCompilation.Create( | ||
"compileAndRunTheExpression", | ||
syntaxTrees: new[] { syntaxTree }, | ||
references: references, | ||
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); | ||
try { | ||
var newScript = script.ContinueWith( | ||
radical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
string.Join("\n", findVarNMethodCall.variableDefinitions) + "\nreturn " + syntaxTree.ToString()); | ||
|
||
SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); | ||
CodeAnalysis.TypeInfo TypeInfo = semanticModel.GetTypeInfo(expressionTree, cancellationToken: token); | ||
var state = await newScript.RunAsync(cancellationToken: token); | ||
|
||
using (var ms = new MemoryStream()) | ||
return JObject.FromObject(ConvertCSharpToJSType(state.ReturnValue, state.ReturnValue.GetType())); | ||
radical marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
catch (Exception) | ||
{ | ||
EmitResult result = compilation.Emit(ms, cancellationToken: token); | ||
if (!result.Success) | ||
{ | ||
var sb = new StringBuilder(); | ||
foreach (Diagnostic d in result.Diagnostics) | ||
sb.Append(d.ToString()); | ||
|
||
throw new ReturnAsErrorException(sb.ToString(), "CompilationError"); | ||
} | ||
|
||
ms.Seek(0, SeekOrigin.Begin); | ||
Assembly assembly = Assembly.Load(ms.ToArray()); | ||
Type type = assembly.GetType("CompileAndRunTheExpression"); | ||
|
||
object ret = type.InvokeMember("Evaluate", | ||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, | ||
null, | ||
null, | ||
findVarNMethodCall.argValues.ToArray()); | ||
|
||
return JObject.FromObject(ConvertCSharpToJSType(ret, TypeInfo.Type)); | ||
throw new ReturnAsErrorException($"Cannot evaluate '{expression}'.", "CompilationError"); | ||
} | ||
} | ||
|
||
|
@@ -462,7 +412,7 @@ public static object Evaluate() | |
typeof(float), typeof(double) | ||
}; | ||
|
||
private static object ConvertCSharpToJSType(object v, ITypeSymbol type) | ||
private static object ConvertCSharpToJSType(object v, Type type) | ||
{ | ||
if (v == null) | ||
return new { type = "object", subtype = "null", className = type.ToString(), description = type.ToString() }; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For future PR, we should consider using the common version (more than one!) property for this, being used in other places in the repo.