diff --git a/source/Runtime/Internal/Flow.cs b/source/Runtime/Internal/Flow.cs index dc80315..3b869e5 100644 --- a/source/Runtime/Internal/Flow.cs +++ b/source/Runtime/Internal/Flow.cs @@ -9,32 +9,52 @@ namespace SharpLab.Runtime.Internal { public static class Flow { private const int MaxReportLength = 20; private const int MaxReportItemCount = 3; + private const int MaxReportStepNotesPerLineCount = 3; - private static readonly List _lines = new List(); - public static IReadOnlyList Lines => _lines; + private static readonly IDictionary _stepNotesCountPerLine = new Dictionary(); + private static readonly List _steps = new List(); + public static IReadOnlyList Steps => _steps; public static void ReportLineStart(int lineNumber) { - _lines.Add(new Line(lineNumber)); + _steps.Add(new Step(lineNumber)); } public static void ReportVariable(string name, T value) { - var line = _lines[_lines.Count - 1]; - var notes = line.Notes; + var step = _steps[_steps.Count - 1]; + if (!_stepNotesCountPerLine.TryGetValue(step.LineNumber, out int countPerLine)) + countPerLine = 0; + countPerLine += 1; + _stepNotesCountPerLine[step.LineNumber] = countPerLine; + + if (countPerLine == MaxReportStepNotesPerLineCount + 1) { + if (step.Notes != null) // already has "…" + return; + step.Notes = new StringBuilder("…"); + _steps[_steps.Count - 1] = step; + return; + } + + if (countPerLine > MaxReportStepNotesPerLineCount + 1) + return; + + var notes = step.Notes; + if (notes == null) { + notes = new StringBuilder(); + step.Notes = notes; + } + if (notes.Length > 0) notes.Append(", "); notes.Append(name).Append(": "); AppendValue(notes, value); - _lines[_lines.Count - 1] = line; + // Have to reassign in case we set Notes + _steps[_steps.Count - 1] = step; } public static void ReportException(object exception) { - var line = _lines[_lines.Count - 1]; - line.Exception = exception; - _lines[_lines.Count - 1] = line; - } - - private static bool HadException() { - return Marshal.GetExceptionPointers() != IntPtr.Zero || Marshal.GetExceptionCode() != 0; + var step = _steps[_steps.Count - 1]; + step.Exception = exception; + _steps[_steps.Count - 1] = step; } private static StringBuilder AppendValue(StringBuilder builder, T value) { @@ -84,26 +104,16 @@ namespace SharpLab.Runtime.Internal { } [Serializable] - public struct Line { - private StringBuilder _notes; - - public Line(int number) { - Number = number; - _notes = null; + public struct Step { + public Step(int lineNumber) { + LineNumber = lineNumber; + Notes = null; Exception = null; } - public int Number { get; } + public int LineNumber { get; } public object Exception { get; internal set; } - - public bool HasNotes => _notes != null; - public StringBuilder Notes { - get { - if (_notes == null) - _notes = new StringBuilder(); - return _notes; - } - } + public StringBuilder Notes { get; internal set; } } } } diff --git a/source/Server/Execution/ExecutionResult.cs b/source/Server/Execution/ExecutionResult.cs index 9047dc7..1024411 100644 --- a/source/Server/Execution/ExecutionResult.cs +++ b/source/Server/Execution/ExecutionResult.cs @@ -5,18 +5,18 @@ using SharpLab.Runtime.Internal; namespace SharpLab.Server.Execution { [Serializable] public class ExecutionResult { - public ExecutionResult(string returnValue, IReadOnlyList flow) { + public ExecutionResult(string returnValue, IReadOnlyList flow) { ReturnValue = returnValue; Flow = flow; } - public ExecutionResult(Exception exception, IReadOnlyList flow) { + public ExecutionResult(Exception exception, IReadOnlyList flow) { Exception = exception; Flow = flow; } public string ReturnValue { get; } public Exception Exception { get; } - public IReadOnlyList Flow { get; } + public IReadOnlyList Flow { get; } } } diff --git a/source/Server/Execution/Executor.cs b/source/Server/Execution/Executor.cs index da3ca64..18acd1e 100644 --- a/source/Server/Execution/Executor.cs +++ b/source/Server/Execution/Executor.cs @@ -62,25 +62,25 @@ namespace SharpLab.Server.Execution { writer.WriteProperty("exception", result.Exception.ToString()); } writer.WritePropertyStartArray("flow"); - foreach (var line in result.Flow) { - SerializeFlowLine(line, writer); + foreach (var step in result.Flow) { + SerializeFlowStep(step, writer); } writer.WriteEndArray(); writer.WriteEndObject(); } - private void SerializeFlowLine(Flow.Line line, IFastJsonWriter writer) { - if (!line.HasNotes && line.Exception == null) { - writer.WriteValue(line.Number); + private void SerializeFlowStep(Flow.Step step, IFastJsonWriter writer) { + if (step.Notes == null && step.Exception == null) { + writer.WriteValue(step.LineNumber); return; } writer.WriteStartObject(); - writer.WriteProperty("line", line.Number); - if (line.HasNotes) - writer.WriteProperty("notes", line.Notes); - if (line.Exception != null) - writer.WriteProperty("exception", line.Exception.GetType().Name); + writer.WriteProperty("line", step.LineNumber); + if (step.Notes != null) + writer.WriteProperty("notes", step.Notes); + if (step.Exception != null) + writer.WriteProperty("exception", step.Exception.GetType().Name); writer.WriteEndObject(); } @@ -93,7 +93,7 @@ namespace SharpLab.Server.Execution { using (guardToken.Scope()) { var result = m.Invoke(Activator.CreateInstance(c), null); - return new ExecutionResult(result?.ToString(), Flow.Lines); + return new ExecutionResult(result?.ToString(), Flow.Steps); } } catch (Exception ex) { @@ -101,7 +101,7 @@ namespace SharpLab.Server.Execution { ex = invocationEx.InnerException; Flow.ReportException(ex); - return new ExecutionResult(ex, Flow.Lines); + return new ExecutionResult(ex, Flow.Steps); } } diff --git a/source/Tests/ExecutionTests.cs b/source/Tests/ExecutionTests.cs index 4495305..383257b 100644 --- a/source/Tests/ExecutionTests.cs +++ b/source/Tests/ExecutionTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using System.Threading.Tasks; using Xunit; using AshMind.Extensions; @@ -8,7 +9,9 @@ using MirrorSharp; using MirrorSharp.Testing; using SharpLab.Server; using SharpLab.Server.MirrorSharp.Internal; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; namespace SharpLab.Tests { public class ExecutionTests { @@ -25,12 +28,34 @@ namespace SharpLab.Tests { var result = await driver.SendSlowUpdateAsync(); var errors = result.JoinErrors(); - var lines = result.ExtensionResult.Flow - .Select(f => new { Line = (f as JObject)?.Value("line") ?? f.Value(), Exception = (f as JObject)?.Value("exception") }) + var steps = result.ExtensionResult.Flow + .Select(s => new { s.Line, s.Exception }) .ToArray(); Assert.True(errors.IsNullOrEmpty(), errors); - Assert.Contains(new { Line = expectedLineNumber, Exception = expectedExceptionTypeName }, lines); + Assert.Contains(new { Line = expectedLineNumber, Exception = expectedExceptionTypeName }, steps); + } + + [Fact] + public async Task SlowUpdate_ReportsLimitedNumberOfNotesPerLine() { + var driver = await NewTestDriverAsync(LoadCodeFromResource("Loops.For.10Iterations.cs")); + + var result = await driver.SendSlowUpdateAsync(); + var errors = result.JoinErrors(); + + var notes = string.Join( + ", ", + result.ExtensionResult.Flow + .Where(s => s.Line == 3 && s.Notes != null) + .Select(s => s.Notes) + ); + + Assert.True(errors.IsNullOrEmpty(), errors); + Assert.Equal("i: 0, i: 1, i: 2, …", notes); + } + + private static int LineNumberFromFlowStep(JToken step) { + return (step as JObject)?.Value("line") ?? step.Value(); } private static string LoadCodeFromResource(string resourceName) { @@ -48,7 +73,34 @@ namespace SharpLab.Tests { private class ExecutionResultData { public string Exception { get; set; } - public IList Flow { get; } = new List(); - } + [JsonIgnore] + public IList Flow { get; } = new List(); + [JsonProperty("flow")] + private IList FlowRaw { get; } = new List(); + + [OnDeserialized] + private void OnDeserialized(StreamingContext context) { + foreach (var token in FlowRaw) { + Flow.Add(ParseStepData(token)); + } + } + + private FlowStepData ParseStepData(JToken token) { + if (token is JValue value) + return new FlowStepData { Line = value.Value() }; + + return new FlowStepData { + Line = token.Value("line"), + Exception = token.Value("exception"), + Notes = token.Value("notes") + }; + } + } + + private class FlowStepData { + public int Line { get; set; } + public string Exception { get; set; } + public string Notes { get; set; } + } } } diff --git a/source/Tests/TestCode/Execution/Loops.For.10Iterations.cs b/source/Tests/TestCode/Execution/Loops.For.10Iterations.cs new file mode 100644 index 0000000..9d96fbc --- /dev/null +++ b/source/Tests/TestCode/Execution/Loops.For.10Iterations.cs @@ -0,0 +1,6 @@ +public class C { + public void M() { + for (var i = 0; i < 10; i++) { + } + } +} \ No newline at end of file