Skip to content

Commit 5c66ce6

Browse files
authored
Reduce allocated strings in CodeWriter (#12366)
For nearly all cases, the result of GetIndentString calls can be returned without allocation, but slicing into a string of spaces or tabs. ComputeIndent accounts for 11.0 MB (0.4%) of total allocations and 27 ms in in the ScrollingAndTypingInCohosting speedometer test. With this change, those numbers go to 0.0 MB (0.0%) and 13 ms respectively. Test insertion: https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequest/680751
1 parent 0dccbfd commit 5c66ce6

File tree

3 files changed

+104
-27
lines changed

3 files changed

+104
-27
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
using Xunit;
5+
6+
using IndentCache = Microsoft.AspNetCore.Razor.Language.CodeGeneration.CodeWriter.IndentCache;
7+
8+
namespace Microsoft.AspNetCore.Razor.Language.Test.CodeGeneration;
9+
10+
public class IndentCacheTest
11+
{
12+
[Theory]
13+
[InlineData(0, false, 4, "")]
14+
[InlineData(4, false, 4, " ")]
15+
[InlineData(8, false, 4, " ")]
16+
[InlineData(0, true, 4, "")]
17+
[InlineData(4, true, 4, "\t")]
18+
[InlineData(8, true, 4, "\t\t")]
19+
[InlineData(6, true, 4, "\t ")]
20+
[InlineData(5, true, 4, "\t ")]
21+
[InlineData(3, true, 4, " ")]
22+
public void GetIndentString_ReturnsExpectedString(int size, bool useTabs, int tabSize, string expected)
23+
{
24+
var result = IndentCache.GetIndentString(size, useTabs, tabSize);
25+
Assert.Equal(expected, result.ToString());
26+
}
27+
28+
[Fact]
29+
public void GetIndentString_TabSizeOne_UsesOnlyTabs()
30+
{
31+
var result = IndentCache.GetIndentString(size: 5, useTabs: true, tabSize: 1);
32+
Assert.Equal(new string('\t', 5), result.ToString());
33+
}
34+
35+
[Fact]
36+
public void GetIndentString_TabSizeGreaterThanSize_UsesSpaces()
37+
{
38+
var result = IndentCache.GetIndentString(size: 3, useTabs: true, tabSize: 10);
39+
Assert.Equal(" ", result.ToString());
40+
}
41+
42+
[Fact]
43+
public void GetIndentString_TabsAndSpacesInResultExceedCachedSizes()
44+
{
45+
var spaceCount = IndentCache.MaxSpaceCount + 1;
46+
var tabCount = IndentCache.MaxTabCount + 1;
47+
var tabSize = spaceCount + 1;
48+
49+
var size = tabSize * tabCount + spaceCount;
50+
var result = IndentCache.GetIndentString(size, useTabs: true, tabSize);
51+
52+
var expected = new string('\t', tabCount) + new string(' ', spaceCount);
53+
Assert.Equal(expected, result.ToString());
54+
}
55+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
using System;
5+
6+
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration;
7+
8+
public sealed partial class CodeWriter
9+
{
10+
internal static class IndentCache
11+
{
12+
internal const int MaxTabCount = 64;
13+
internal const int MaxSpaceCount = 128;
14+
15+
private static readonly ReadOnlyMemory<char> s_tabs = new string('\t', MaxTabCount).AsMemory();
16+
private static readonly ReadOnlyMemory<char> s_spaces = new string(' ', MaxSpaceCount).AsMemory();
17+
18+
public static ReadOnlyMemory<char> GetIndentString(int size, bool useTabs, int tabSize)
19+
{
20+
if (!useTabs)
21+
{
22+
return SliceOrCreate(size, s_spaces);
23+
}
24+
25+
var tabCount = size / tabSize;
26+
var spaceCount = size % tabSize;
27+
28+
if (spaceCount == 0)
29+
{
30+
return SliceOrCreate(tabCount, s_tabs);
31+
}
32+
33+
return string.Create(length: tabCount + spaceCount, state: tabCount, static (destination, tabCount) =>
34+
{
35+
destination[..tabCount].Fill('\t');
36+
destination[tabCount..].Fill(' ');
37+
}).AsMemory();
38+
}
39+
40+
private static ReadOnlyMemory<char> SliceOrCreate(int length, ReadOnlyMemory<char> chars)
41+
{
42+
return (length <= chars.Length)
43+
? chars[..length]
44+
: new string(chars.Span[0], length).AsMemory();
45+
}
46+
}
47+
}

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

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using System.IO;
1010
using System.Runtime.CompilerServices;
1111
using System.Text;
12-
using Microsoft.AspNetCore.Razor.PooledObjects;
1312
using Microsoft.CodeAnalysis.Text;
1413

1514
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration;
@@ -118,7 +117,7 @@ public int CurrentIndent
118117
if (_indentSize != value)
119118
{
120119
_indentSize = value;
121-
_indentString = ComputeIndent(value, IndentWithTabs, TabSize);
120+
_indentString = IndentCache.GetIndentString(value, IndentWithTabs, TabSize);
122121
}
123122
}
124123
}
@@ -189,7 +188,7 @@ public CodeWriter Indent(int size)
189188

190189
var indentString = size == _indentSize
191190
? _indentString
192-
: ComputeIndent(size, IndentWithTabs, TabSize);
191+
: IndentCache.GetIndentString(size, IndentWithTabs, TabSize);
193192

194193
AddTextChunk(indentString);
195194

@@ -200,30 +199,6 @@ public CodeWriter Indent(int size)
200199
return this;
201200
}
202201

203-
private static ReadOnlyMemory<char> ComputeIndent(int size, bool useTabs, int tabSize)
204-
{
205-
if (size == 0)
206-
{
207-
return ReadOnlyMemory<char>.Empty;
208-
}
209-
210-
if (useTabs)
211-
{
212-
var tabCount = size / tabSize;
213-
var spaceCount = size % tabSize;
214-
215-
using var _ = StringBuilderPool.GetPooledObject(out var builder);
216-
builder.SetCapacityIfLarger(tabCount + spaceCount);
217-
218-
builder.Append('\t', tabCount);
219-
builder.Append(' ', spaceCount);
220-
221-
return builder.ToString().AsMemory();
222-
}
223-
224-
return new string(' ', size).AsMemory();
225-
}
226-
227202
public CodeWriter Write(string value)
228203
{
229204
ArgHelper.ThrowIfNull(value);

0 commit comments

Comments
 (0)