Skip to content

Conversation

@davidwengier
Copy link
Member

Fixes #8781 and https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1828376

Context on benchmarks:
I benchmarked a few scenarios with this one, because its a little hard to guess at what the scenario is that PRISM found. All of the benchmarks simulate a full "format" operation, so for each line of the file, they find the first non-whitespace character, and then try to find the node that owns it. This is what the real formatting engine does (multiple times)

  • "Current" is the current code, and it has in-built caching using a conditional weak table, so under the benchmark its actually pretty fast, because we're repeatedly operating on the same syntax tree. That will happen in the wild to some extent too.
  • "Current_NoCache" is me deliberately clearing the conditional weak table before each iteration, so effectively this measures worst case. When a user is typing, or formatting is changing the document, this is what they would be getting, though is overstating the problem somewhat, as when the formatting engine calls ShouldFormat repeatedly on the same document, it would have some benefit from caching.
  • "TryGetPreviousSibling" is the new code, as in this PR.
  • "TryGetTwoPreviousSiblings" is a new method I wrote to try to further optimize the workaround, since it has two calls to the TryGetPreviousSibling method, which would both have to iterate the tree. After looking at the benchmarks it didn't make enough of an impact to be worthwhile IMO, so left the existing code as its easy to read and debug.

The next four benchmarks, that end in "NewSyntaxTree", mirror the above scenarios but the parsing of the syntax tree is part of the iteration. This means absolute times are longer, but levels the playing field, so that there is no benefit from caching at all, including any lazy evaluation the syntax nodes may do.

The "DocType" field specifies which of the four documents I ran with. "Before" and "After" have a <div> element with 100 attributes, at the start or end of the document, respectively. "BeforeNested" and "AfterNested" have 100 nested <div> elements at the start or end. In all cases, the extra div elements would have had to be scanned by the PreviousSpan() and the TryGetPrevousSibling() methods.

Summary:
In the best case, the new code performs on par with the existing code for CPU, and better for memory. In the worst case, it performs much better than the existing code. Given reality is presumably somewhere in the middle, I consider the new code to be enough of a win, and if PRISM continues to find issues we can always take a second look at things.

Benchmark results:

Method DocType Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
Current Before 430.4 us 8.42 us 8.27 us 427.5 us 421.0 us 446.9 us 0.03 0.00 4.8828 2.4414 31.46 KB 0.42
Current_NoCache Before 12,949.5 us 108.56 us 84.75 us 12,922.9 us 12,848.4 us 13,134.1 us 1.00 0.00 - - 74.31 KB 1.00
TryGetPreviousSibling Before 701.4 us 7.48 us 6.99 us 703.1 us 692.3 us 716.1 us 0.05 0.00 1.9531 0.9766 12.53 KB 0.17
TryGetTwoPreviousSiblings Before 701.5 us 13.97 us 14.95 us 699.8 us 678.8 us 736.7 us 0.05 0.00 1.9531 0.9766 12.53 KB 0.17
Current_NewSyntaxTree Before 14,515.8 us 269.87 us 225.35 us 14,496.3 us 14,118.9 us 15,012.8 us 1.12 0.02 78.1250 15.6250 471.36 KB 6.34
Current_NoCache_NewSyntaxTree Before 14,453.3 us 197.39 us 164.83 us 14,447.5 us 14,164.5 us 14,743.4 us 1.12 0.01 78.1250 31.2500 475.48 KB 6.40
TryGetPreviousSibling_NewSyntaxTree Before 1,855.6 us 32.64 us 59.68 us 1,861.1 us 1,751.5 us 1,958.8 us 0.14 0.00 48.8281 15.6250 291.79 KB 3.93
TryGetTwoPreviousSiblings_NewSyntaxTree Before 1,613.4 us 31.58 us 32.43 us 1,602.5 us 1,566.5 us 1,673.3 us 0.13 0.00 48.8281 15.6250 291.79 KB 3.93
Current After 449.1 us 4.62 us 4.09 us 447.5 us 443.8 us 455.9 us 0.03 0.00 4.8828 2.4414 31.46 KB 0.42
Current_NoCache After 12,966.5 us 175.09 us 155.21 us 12,938.8 us 12,736.4 us 13,332.0 us 1.00 0.00 - - 74.37 KB 1.00
TryGetPreviousSibling After 759.9 us 12.92 us 13.83 us 755.4 us 744.3 us 793.3 us 0.06 0.00 1.9531 0.9766 12.53 KB 0.17
TryGetTwoPreviousSiblings After 758.0 us 10.34 us 8.63 us 757.8 us 744.2 us 770.3 us 0.06 0.00 1.9531 0.9766 12.53 KB 0.17
Current_NewSyntaxTree After 14,963.1 us 272.16 us 267.30 us 14,895.9 us 14,657.3 us 15,533.1 us 1.15 0.03 78.1250 15.6250 471.36 KB 6.34
Current_NoCache_NewSyntaxTree After 14,036.6 us 268.61 us 309.33 us 13,992.8 us 13,673.0 us 14,883.2 us 1.08 0.03 78.1250 31.2500 475.6 KB 6.39
TryGetPreviousSibling_NewSyntaxTree After 1,623.9 us 20.56 us 19.23 us 1,626.1 us 1,590.8 us 1,652.1 us 0.13 0.00 48.8281 15.6250 291.89 KB 3.92
TryGetTwoPreviousSiblings_NewSyntaxTree After 1,656.4 us 30.68 us 28.70 us 1,653.0 us 1,595.2 us 1,717.6 us 0.13 0.00 48.8281 15.6250 291.9 KB 3.92
Current NestedBefore 2,936.8 us 56.07 us 46.82 us 2,944.8 us 2,845.4 us 3,016.6 us 0.12 0.00 15.6250 7.8125 105.25 KB 0.62
Current_NoCache NestedBefore 24,710.0 us 264.52 us 220.89 us 24,646.1 us 24,363.3 us 25,097.3 us 1.00 0.00 - - 169.49 KB 1.00
TryGetPreviousSibling NestedBefore 2,386.4 us 22.80 us 19.04 us 2,382.2 us 2,353.7 us 2,414.8 us 0.10 0.00 7.8125 3.9063 66.66 KB 0.39
TryGetTwoPreviousSiblings NestedBefore 2,388.0 us 46.55 us 62.14 us 2,372.1 us 2,275.1 us 2,540.5 us 0.10 0.00 7.8125 3.9063 66.66 KB 0.39
Current_NewSyntaxTree NestedBefore 28,585.7 us 566.98 us 1,064.93 us 28,663.6 us 26,065.0 us 30,507.0 us 1.17 0.04 156.2500 62.5000 1054.87 KB 6.22
Current_NoCache_NewSyntaxTree NestedBefore 28,832.9 us 576.42 us 1,177.47 us 28,881.4 us 27,199.5 us 31,982.1 us 1.15 0.05 156.2500 62.5000 1063.43 KB 6.27
TryGetPreviousSibling_NewSyntaxTree NestedBefore 4,311.3 us 85.94 us 161.41 us 4,246.1 us 4,122.7 us 4,771.9 us 0.18 0.01 101.5625 39.0625 622.25 KB 3.67
TryGetTwoPreviousSiblings_NewSyntaxTree NestedBefore 4,504.2 us 43.77 us 40.95 us 4,498.1 us 4,406.7 us 4,571.1 us 0.18 0.00 101.5625 39.0625 622.25 KB 3.67
Current NestedAfter 2,968.3 us 39.79 us 35.27 us 2,960.7 us 2,929.3 us 3,051.5 us 0.11 0.00 15.6250 7.8125 105.25 KB 0.62
Current_NoCache NestedAfter 26,783.9 us 199.06 us 166.23 us 26,833.2 us 26,405.2 us 27,014.0 us 1.00 0.00 - - 169.62 KB 1.00
TryGetPreviousSibling NestedAfter 2,487.8 us 28.38 us 25.16 us 2,478.9 us 2,451.5 us 2,538.0 us 0.09 0.00 7.8125 3.9063 66.66 KB 0.39
TryGetTwoPreviousSiblings NestedAfter 2,526.1 us 33.30 us 29.52 us 2,518.5 us 2,488.4 us 2,582.6 us 0.09 0.00 7.8125 3.9063 66.66 KB 0.39
Current_NewSyntaxTree NestedAfter 29,358.6 us 582.03 us 1,019.37 us 29,019.5 us 28,144.9 us 31,731.1 us 1.09 0.04 156.2500 62.5000 1054.87 KB 6.22
Current_NoCache_NewSyntaxTree NestedAfter 28,989.5 us 506.47 us 473.75 us 28,982.6 us 28,068.0 us 29,656.8 us 1.08 0.02 125.0000 62.5000 1074.51 KB 6.33
TryGetPreviousSibling_NewSyntaxTree NestedAfter 4,326.7 us 85.29 us 153.80 us 4,324.7 us 4,119.1 us 4,673.1 us 0.16 0.00 101.5625 39.0625 622.31 KB 3.67
TryGetTwoPreviousSiblings_NewSyntaxTree NestedAfter 4,577.6 us 27.98 us 26.17 us 4,572.6 us 4,525.5 us 4,625.6 us 0.17 0.00 101.5625 39.0625 622.31 KB 3.67

@davidwengier davidwengier requested review from a team as code owners June 6, 2023 01:11
owner is CSharpStatementLiteralSyntax &&
owner.Parent is CSharpCodeBlockSyntax &&
owner.PreviousSpan() is CSharpTransitionSyntax)
owner.TryGetPreviousSibling(out var transition) && transition is CSharpTransitionSyntax)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't flagged by PRISM, but removing all usages of PreviousSpan from the LSP editor just makes sense.


if (owner is CSharpStatementLiteralSyntax &&
owner.PreviousSpan() is { } prevNode &&
owner.TryGetPreviousSibling(out var prevNode) &&
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, this wasn't flagged.

@davidwengier
Copy link
Member Author

Ping @dotnet/razor-tooling, after a bit of back-and-forth this is a tooling exclusive change

var outerNode = owner.GetOutermostNode();
if (outerNode is not null &&
outerNode.TryGetPreviousSibling(out var whiteSpace) && whiteSpace.ContainsOnlyWhitespace() &&
whiteSpace.TryGetPreviousSibling(out var comment) && comment is MarkupCommentBlockSyntax)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this accurate? Previously, it checked literal.PreviousSpan()?.Parent but the parent isn't checked anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. PreviousSpan would return the close angle of the comment, so needed to get to the parent to check if it was a comment. In this case TryGetPreviousSibling is not only faster, it more closely matches what the code wants to do anyway.

The scenario is covered by a couple of tests here: https://github.com/dotnet/razor/blob/main/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting/HtmlFormattingTest.cs#L1020

Copy link
Member

@DustinCampbell DustinCampbell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@davidwengier davidwengier merged commit cba27fe into dotnet:main Jun 13, 2023
@davidwengier davidwengier deleted the DontUsePreviousSpan branch June 13, 2023 23:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make PreviousSpan() extension method faster

4 participants