Skip to content

Commit

Permalink
Move StackTraceAnalyzer over to VirtualCharSequence (#60404)
Browse files Browse the repository at this point in the history
* Convert to using VirtualChars to reduce string allocations from the stack trace analyzer
* Add benchmarks
* Fixes AB#1504223
  • Loading branch information
ryzngard authored Mar 31, 2022
1 parent d2f4b6a commit 71dfd27
Show file tree
Hide file tree
Showing 9 changed files with 1,871 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<InternalsVisibleTo Include="AnalyzerRunner" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.EditorFeatures2.UnitTests" />
<InternalsVisibleTo Include="Microsoft.CodeAnalysis.Workspaces.Test.Utilities" />
<InternalsVisibleTo Include="IdeBenchmarks" />
<!-- BEGIN MONODEVELOP
These MonoDevelop dependencies don't ship with Visual Studio, so can't break our
binary insertions and are exempted from the ExternalAccess adapter assembly policies.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
internal sealed class DefaultStackParser : IStackFrameParser
{
public bool TryParseLine(string line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
public bool TryParseLine(VirtualCharSequence line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
{
// For now we just keep all text so the user can still see lines they pasted and they
// don't disappear. In the future we might want to restrict what we show.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
Expand All @@ -12,7 +14,7 @@ internal sealed class DotnetStackFrameParser : IStackFrameParser
/// <summary>
/// Uses <see cref="StackFrameParser"/> to parse a line if possible
/// </summary>
public bool TryParseLine(string line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
public bool TryParseLine(VirtualCharSequence line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
{
parsedFrame = null;
var tree = StackFrameParser.TryParse(line);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
internal interface IStackFrameParser
{
bool TryParseLine(string line, [NotNullWhen(returnValue: true)] out ParsedFrame? parsedFrame);
bool TryParseLine(VirtualCharSequence line, [NotNullWhen(returnValue: true)] out ParsedFrame? parsedFrame);
}
}
9 changes: 6 additions & 3 deletions src/Features/Core/Portable/StackTraceExplorer/IgnoredFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,23 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
internal sealed class IgnoredFrame : ParsedFrame
{
private readonly string _originalText;
private readonly VirtualCharSequence _originalText;

public IgnoredFrame(string originalText)
public IgnoredFrame(VirtualCharSequence originalText)
{
_originalText = originalText;
}

public override string ToString()
{
return _originalText;
return _originalText.CreateString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ namespace Microsoft.CodeAnalysis.StackTraceExplorer
internal readonly struct StackTraceAnalysisResult
{
public StackTraceAnalysisResult(
string originalString,
ImmutableArray<ParsedFrame> parsedLines)
{
OriginalString = originalString;
ParsedFrames = parsedLines;
}

public string OriginalString { get; }
public ImmutableArray<ParsedFrame> ParsedFrames { get; }
}
}
82 changes: 68 additions & 14 deletions src/Features/Core/Portable/StackTraceExplorer/StackTraceAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
Expand All @@ -28,40 +31,91 @@ internal static class StackTraceAnalyzer

public static Task<StackTraceAnalysisResult> AnalyzeAsync(string callstack, CancellationToken cancellationToken)
{
var parsedFrames = Parse(callstack, cancellationToken);
return Task.FromResult(new StackTraceAnalysisResult(parsedFrames.ToImmutableArray()));
var result = new StackTraceAnalysisResult(callstack, Parse(callstack, cancellationToken));
return Task.FromResult(result);
}

private static IEnumerable<ParsedFrame> Parse(string callstack, CancellationToken cancellationToken)
private static ImmutableArray<ParsedFrame> Parse(string callstack, CancellationToken cancellationToken)
{
foreach (var line in SplitLines(callstack))
using var _ = ArrayBuilder<ParsedFrame>.GetInstance(out var builder);

// if the callstack comes from ActivityLog.xml it has been
// encoding to be passed over HTTP. This should only decode
// specific characters like "&gt;" and "&lt;" to their "normal"
// equivalents ">" and "<" so we can parse correctly
callstack = WebUtility.HtmlDecode(callstack);

var sequence = VirtualCharSequence.Create(0, callstack);

foreach (var line in SplitLines(sequence))
{
cancellationToken.ThrowIfCancellationRequested();

var trimmedLine = line.Trim();
// For now do the work to removing leading and trailing whitespace.
// This keeps behavior we've had, but may not actually be the desired behavior in the long run.
// Specifically if we ever want to add a copy feature to copy back contents from a frame
var trimmedLine = Trim(line);

if (trimmedLine.IsEmpty)
{
continue;
}

foreach (var parser in s_parsers)
{
if (parser.TryParseLine(trimmedLine, out var parsedFrame))
{
yield return parsedFrame;
builder.Add(parsedFrame);
break;
}
}
}

return builder.ToImmutable();
}

private static readonly char[] s_lineSplit = new[] { '\n' };
private static IEnumerable<VirtualCharSequence> SplitLines(VirtualCharSequence callstack)
{
var position = 0;

for (var i = 0; i < callstack.Length; i++)
{
if (callstack[i].Value == '\n')
{
yield return callstack.GetSubSequence(TextSpan.FromBounds(position, i));

// +1 to skip over the \n character
position = i + 1;
}
}

private static IEnumerable<string> SplitLines(string callstack)
if (position < callstack.Length)
{
yield return callstack.GetSubSequence(TextSpan.FromBounds(position, callstack.Length));
}
}

private static VirtualCharSequence Trim(VirtualCharSequence virtualChars)
{
// if the callstack comes from ActivityLog.xml it has been
// encoding to be passed over HTTP. This should only decode
// specific characters like "&gt;" and "&lt;" to their "normal"
// equivalents ">" and "<" so we can parse correctly
callstack = WebUtility.HtmlDecode(callstack);
if (virtualChars.Length == 0)
{
return virtualChars;
}

var start = 0;
var end = virtualChars.Length - 1;

while (virtualChars[start].IsWhiteSpace && start < end)
{
start++;
}

while (virtualChars[end].IsWhiteSpace && end > start)
{
end--;
}

return callstack.Split(s_lineSplit, StringSplitOptions.RemoveEmptyEntries);
return virtualChars.GetSubSequence(TextSpan.FromBounds(start, end + 1));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,37 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.EmbeddedLanguages.StackFrame;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;

namespace Microsoft.CodeAnalysis.StackTraceExplorer
{
internal sealed class VSDebugCallstackParser : IStackFrameParser
{
public bool TryParseLine(string line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
public bool TryParseLine(VirtualCharSequence line, [NotNullWhen(true)] out ParsedFrame? parsedFrame)
{
// Example line:
// ConsoleApp4.dll!ConsoleApp4.MyClass.ThrowAtOne() Line 19 C#
// |--------------------------------|
// Symbol data we care about
parsedFrame = null;

// +1 here because we always want to skip the '!' character
var startPoint = line.IndexOf('!') + 1;
var startPoint = -1;
for (var i = 0; i < line.Length; i++)
{
if (line[i].Value == '!')
{
// +1 here because we always want to skip the '!' character
startPoint = i + 1;
break;
}
}

if (startPoint == 0 || startPoint == line.Length)
if (startPoint <= 0 || startPoint == line.Length)
{
return false;
}

var textToParse = line[startPoint..];
var textToParse = line.GetSubSequence(TextSpan.FromBounds(startPoint, line.Length));
var tree = StackFrameParser.TryParse(textToParse);

if (tree is null)
Expand Down
Loading

0 comments on commit 71dfd27

Please sign in to comment.