Skip to content

Commit e6d2b8b

Browse files
Improve GreenNode.ToString() implementation for efficiency (#12344)
> [!TIP] > I recommend reviewing this PR commit-by-commit. Each commit represents a concrete change and includes a description. The key improvement in this change is to take advantage of the fact that a green node's width is the same as the length of the string produced by `ToString()`. So, we can use the width to call `string.Create(...)` and efficiently create the string rather than building it up with a `StringWriter`. Using a `StringWriter` requires all of the characters to first be copied into the writer's underlying `StringBuilder` and then copy them *again* when creating the final string. We can avoid this extra copying by using `string.Create(...)` and copying the characters directly into the final string. The end result is a simpler implementation that avoids the need for a `StringWriter` pool. ---- CI Build: https://dev.azure.com/dnceng/internal/_build/results?buildId=2817951&view=results Test Insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/680209 Toolset Run: https://dev.azure.com/dnceng/internal/_build/results?buildId=2817952&view=results
2 parents 73622b7 + c079f4d commit e6d2b8b

File tree

12 files changed

+1793
-159
lines changed

12 files changed

+1793
-159
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/Syntax/GreenNodeTests.cs

Lines changed: 1221 additions & 0 deletions
Large diffs are not rendered by default.

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Syntax/ChildSyntaxList.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index)
118118
/// </summary>
119119
internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref SlotData slotData)
120120
{
121-
GreenNode greenChild;
121+
GreenNode? greenChild;
122122
var green = node.Green;
123123

124124
// slotData may contain information that allows us to start the loop below using data
@@ -289,7 +289,7 @@ internal static SyntaxNodeOrToken ChildThatContainsPosition(SyntaxNode node, int
289289
/// </summary>
290290
internal static SyntaxNode? ItemInternalAsNode(SyntaxNode node, int index, ref SlotData slotData)
291291
{
292-
GreenNode greenChild;
292+
GreenNode? greenChild;
293293
var green = node.Green;
294294
var idx = index - slotData.PrecedingOccupantSlotCount;
295295
var slotIndex = slotData.SlotIndex;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
5+
6+
internal abstract partial class GreenNode
7+
{
8+
/// <summary>
9+
/// Provides a depth-first enumerator for traversing <see cref="GreenNode"/> syntax trees.
10+
/// List nodes are treated as "transparent" and are not returned during enumeration,
11+
/// only their children are processed.
12+
/// </summary>
13+
/// <remarks>
14+
/// This enumerator uses a stack-based approach to traverse the syntax tree without recursion.
15+
/// It pushes child nodes in reverse order to maintain correct left-to-right traversal order.
16+
/// The enumerator must be disposed to release the underlying stack resources.
17+
/// </remarks>
18+
public ref struct Enumerator
19+
{
20+
private MemoryBuilder<GreenNode> _stack;
21+
private GreenNode? _current;
22+
23+
public Enumerator(GreenNode node)
24+
{
25+
// MemoryBuilder<T> uses ArrayPool<T>.Shared to acquire an array.
26+
// So, we set an initial capacity of 256 to avoid unnecessary growth.
27+
// In addition, because the arrays used by MemoryBuilder<T> will be
28+
// returned to the pool, we clear them to ensure that GreenNodes aren't
29+
// kept in memory.
30+
_stack = new(initialCapacity: 256, clearArray: true);
31+
_stack.Push(node);
32+
}
33+
34+
public void Dispose()
35+
{
36+
_stack.Dispose();
37+
}
38+
39+
public readonly GreenNode Current => _current!;
40+
41+
public bool MoveNext()
42+
{
43+
while (_stack.TryPop(out var node))
44+
{
45+
// Push children onto the stack in reverse order for correct traversal order.
46+
for (var i = node.SlotCount - 1; i >= 0; i--)
47+
{
48+
var child = node.GetSlot(i);
49+
if (child != null)
50+
{
51+
_stack.Push(child);
52+
}
53+
}
54+
55+
// If this is not a list node, return it as the current item.
56+
// List nodes are "transparent" - we push their children but don't return the list itself.
57+
if (!node.IsList)
58+
{
59+
_current = node;
60+
return true;
61+
}
62+
}
63+
64+
// Stack is empty, no more nodes to process
65+
_current = null;
66+
return false;
67+
}
68+
}
69+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Razor.Language.Syntax;
5+
6+
internal abstract partial class GreenNode
7+
{
8+
/// <summary>
9+
/// Provides enumeration over only the tokens within a <see cref="GreenNode"/> syntax tree,
10+
/// filtering out non-token nodes during traversal.
11+
/// </summary>
12+
public readonly ref struct TokenEnumerable(GreenNode node)
13+
{
14+
public Enumerator GetEnumerator()
15+
=> new(node);
16+
17+
/// <summary>
18+
/// Enumerates only the tokens within a <see cref="GreenNode"/> syntax tree,
19+
/// automatically filtering out non-token nodes during traversal.
20+
/// </summary>
21+
public ref struct Enumerator(GreenNode node)
22+
{
23+
private GreenNode.Enumerator _enumerator = node.GetEnumerator();
24+
private InternalSyntax.SyntaxToken? _current;
25+
26+
public void Dispose()
27+
{
28+
_enumerator.Dispose();
29+
}
30+
31+
public readonly InternalSyntax.SyntaxToken Current => _current!;
32+
33+
public bool MoveNext()
34+
{
35+
while (_enumerator.MoveNext())
36+
{
37+
if (_enumerator.Current.IsToken)
38+
{
39+
_current = (InternalSyntax.SyntaxToken)_enumerator.Current;
40+
return true;
41+
}
42+
}
43+
44+
_current = null;
45+
return false;
46+
}
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)