Skip to content

Commit 6763310

Browse files
authored
Optimize allocations under RuntimeNodeWriter.WriteHtmlContent (#11945)
This method was creating a StringBuilder with unknown size allocating both a final string and char array along with resize allocations. Instead, calculate the result length and calculate a single char array. This reduced allocations in the typing scenario in the razor lsp speedometer test under this method from about 3.8% to 2.3%. Speedometer run: https://dev.azure.com/devdiv/DevDiv/_apps/hub/ms-vseng.pit-vsengPerf.pit-hub?targetBuild=10711.132.dn-bot.250612.042302.643010&targetBranch=main&targetPerfBuildId=11746459&runGroup=Speedometer&baselineBuild=10711.80&baselineBranch=main Allocations before change ![image](https://github.com/user-attachments/assets/17a7348f-1be1-470d-8c53-6850bb5043c7) Allocations with change ![image](https://github.com/user-attachments/assets/6c3fb51a-fc0f-4c81-93dc-4179903f1154)
2 parents 72bf181 + d90ed2e commit 6763310

File tree

2 files changed

+38
-45
lines changed

2 files changed

+38
-45
lines changed

src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/RuntimeNodeWriterTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ public void WriteHtmlLiteral_WithinMaxSize_WritesSingleLiteral()
407407
using var context = TestCodeRenderingContext.CreateRuntime();
408408

409409
// Act
410-
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "Hello");
410+
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "Hello".AsMemory());
411411

412412
// Assert
413413
var csharp = context.CodeWriter.GetText().ToString();
@@ -426,7 +426,7 @@ public void WriteHtmlLiteral_GreaterThanMaxSize_WritesMultipleLiterals()
426426
using var context = TestCodeRenderingContext.CreateRuntime();
427427

428428
// Act
429-
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "Hello World");
429+
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "Hello World".AsMemory());
430430

431431
// Assert
432432
var csharp = context.CodeWriter.GetText().ToString();
@@ -446,7 +446,7 @@ public void WriteHtmlLiteral_GreaterThanMaxSize_SingleEmojisSplit()
446446
using var context = TestCodeRenderingContext.CreateRuntime();
447447

448448
// Act
449-
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 2, " 👦");
449+
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 2, " 👦".AsMemory());
450450

451451
// Assert
452452
var csharp = context.CodeWriter.GetText().ToString();
@@ -466,7 +466,7 @@ public void WriteHtmlLiteral_GreaterThanMaxSize_SequencedZeroWithJoinedEmojisSpl
466466
using var context = TestCodeRenderingContext.CreateRuntime();
467467

468468
// Act
469-
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "👩‍👩‍👧‍👧👩‍👩‍👧‍👧");
469+
writer.WriteHtmlLiteral(context, maxStringLiteralLength: 6, "👩‍👩‍👧‍👧👩‍👩‍👧‍👧".AsMemory());
470470

471471
// Assert
472472
var csharp = context.CodeWriter.GetText().ToString();

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/RuntimeNodeWriter.cs

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
using System;
77
using System.Globalization;
88
using System.Linq;
9-
using System.Text;
109
using Microsoft.AspNetCore.Razor.Language.Intermediate;
10+
using Microsoft.AspNetCore.Razor.PooledObjects;
1111

1212
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration;
1313

@@ -251,62 +251,55 @@ public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentI
251251
{
252252
const int MaxStringLiteralLength = 1024;
253253

254-
var builder = new StringBuilder();
255-
for (var i = 0; i < node.Children.Count; i++)
254+
using var htmlContentBuilder = new PooledArrayBuilder<ReadOnlyMemory<char>>();
255+
256+
var length = 0;
257+
foreach (var child in node.Children)
256258
{
257-
if (node.Children[i] is IntermediateToken token && token.IsHtml)
259+
if (child is IntermediateToken token && token.IsHtml)
258260
{
259-
builder.Append(token.Content);
261+
var htmlContent = token.Content.AsMemory();
262+
263+
htmlContentBuilder.Add(htmlContent);
264+
length += htmlContent.Length;
260265
}
261266
}
262267

263-
var content = builder.ToString();
268+
// Can't use a pooled builder here as the memory will be stored in the context.
269+
var content = new char[length];
270+
var contentIndex = 0;
271+
foreach (var htmlContent in htmlContentBuilder)
272+
{
273+
htmlContent.Span.CopyTo(content.AsSpan(contentIndex));
274+
contentIndex += htmlContent.Length;
275+
}
264276

265-
WriteHtmlLiteral(context, MaxStringLiteralLength, content);
277+
WriteHtmlLiteral(context, MaxStringLiteralLength, content.AsMemory());
266278
}
267279

268280
// Internal for testing
269-
internal void WriteHtmlLiteral(CodeRenderingContext context, int maxStringLiteralLength, string literal)
281+
internal void WriteHtmlLiteral(CodeRenderingContext context, int maxStringLiteralLength, ReadOnlyMemory<char> literal)
270282
{
271-
if (literal.Length <= maxStringLiteralLength)
283+
while (literal.Length > maxStringLiteralLength)
272284
{
273-
WriteLiteral(literal);
274-
return;
275-
}
285+
// String is too large, render the string in pieces to avoid Roslyn OOM exceptions at compile time: https://github.com/aspnet/External/issues/54
286+
var lastCharBeforeSplit = literal.Span[maxStringLiteralLength - 1];
276287

277-
// String is too large, render the string in pieces to avoid Roslyn OOM exceptions at compile time: https://github.com/aspnet/External/issues/54
278-
var charactersConsumed = 0;
279-
do
280-
{
281-
var charactersRemaining = literal.Length - charactersConsumed;
282-
var charactersToSubstring = Math.Min(maxStringLiteralLength, charactersRemaining);
283-
var lastCharBeforeSplitIndex = charactersConsumed + charactersToSubstring - 1;
284-
var lastCharBeforeSplit = literal[lastCharBeforeSplitIndex];
288+
// If character at splitting point is a high surrogate, take one less character this iteration
289+
// as we're attempting to split a surrogate pair. This can happen when something like an
290+
// emoji sits on the barrier between splits; if we were to split the emoji we'd end up with
291+
// invalid bytes in our output.
292+
var renderCharCount = char.IsHighSurrogate(lastCharBeforeSplit) ? maxStringLiteralLength - 1 : maxStringLiteralLength;
285293

286-
if (char.IsHighSurrogate(lastCharBeforeSplit))
287-
{
288-
if (charactersRemaining > 1)
289-
{
290-
// Take one less character this iteration. We're attempting to split inbetween a surrogate pair.
291-
// This can happen when something like an emoji sits on the barrier between splits; if we were to
292-
// split the emoji we'd end up with invalid bytes in our output.
293-
charactersToSubstring--;
294-
}
295-
else
296-
{
297-
// The user has an invalid file with a partial surrogate a the splitting point.
298-
// We'll let the invalid character flow but we'll explode later on.
299-
}
300-
}
294+
WriteLiteral(literal[..renderCharCount]);
301295

302-
var textToRender = literal.Substring(charactersConsumed, charactersToSubstring);
303-
304-
WriteLiteral(textToRender);
296+
literal = literal[renderCharCount..];
297+
}
305298

306-
charactersConsumed += textToRender.Length;
307-
} while (charactersConsumed < literal.Length);
299+
WriteLiteral(literal);
300+
return;
308301

309-
void WriteLiteral(string content)
302+
void WriteLiteral(ReadOnlyMemory<char> content)
310303
{
311304
context.CodeWriter
312305
.WriteStartMethodInvocation(WriteHtmlContentMethod)

0 commit comments

Comments
 (0)