Skip to content

Commit 9a3657f

Browse files
committed
Share string builder in console loggers.
1 parent ebd1f1e commit 9a3657f

File tree

6 files changed

+162
-34
lines changed

6 files changed

+162
-34
lines changed

src/Build.UnitTests/ConsoleOutputAlignerTests.cs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Text;
56
using Microsoft.Build.BackEnd.Logging;
7+
using Microsoft.Build.Framework;
68
using Shouldly;
79
using Xunit;
810

@@ -18,7 +20,7 @@ public class ConsoleOutputAlignerTests
1820
public void IndentBiggerThanBuffer_IndentedAndNotAligned(string input, bool aligned)
1921
{
2022
string indent = " ";
21-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: aligned);
23+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: aligned, stringBuilderProvider: new TestStringBuilderProvider());
2224

2325
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: indent.Length);
2426

@@ -30,7 +32,7 @@ public void IndentBiggerThanBuffer_IndentedAndNotAligned(string input, bool alig
3032
[InlineData("12345")]
3133
public void NoAlignNoIndent_NotAlignedEvenIfBiggerThanBuffer(string input)
3234
{
33-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: false);
35+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: false, stringBuilderProvider: new TestStringBuilderProvider());
3436

3537
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: 0);
3638

@@ -43,7 +45,7 @@ public void NoAlignNoIndent_NotAlignedEvenIfBiggerThanBuffer(string input)
4345
public void NoBufferWidthNoIndent_NotAligned(int sizeOfMessage)
4446
{
4547
string input = new string('.', sizeOfMessage);
46-
var aligner = new ConsoleOutputAligner(bufferWidth: -1, alignMessages: false);
48+
var aligner = new ConsoleOutputAligner(bufferWidth: -1, alignMessages: false, stringBuilderProvider: new TestStringBuilderProvider());
4749

4850
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: 0);
4951

@@ -55,7 +57,7 @@ public void NoBufferWidthNoIndent_NotAligned(int sizeOfMessage)
5557
[InlineData("12345")]
5658
public void WithoutBufferWidthWithoutIndentWithAlign_NotIndentedAndNotAligned(string input)
5759
{
58-
var aligner = new ConsoleOutputAligner(bufferWidth: -1, alignMessages: true);
60+
var aligner = new ConsoleOutputAligner(bufferWidth: -1, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
5961

6062
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: 0);
6163

@@ -67,7 +69,7 @@ public void WithoutBufferWidthWithoutIndentWithAlign_NotIndentedAndNotAligned(st
6769
[InlineData("12345")]
6870
public void NoAlignPrefixAlreadyWritten_NotChanged(string input)
6971
{
70-
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true);
72+
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
7173

7274
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 0);
7375

@@ -80,7 +82,7 @@ public void NoAlignPrefixAlreadyWritten_NotChanged(string input)
8082
[InlineData(" ", "1")]
8183
public void SmallerThanBuffer_NotAligned(string indent, string input)
8284
{
83-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
85+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
8486

8587
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: indent.Length);
8688

@@ -93,7 +95,7 @@ public void SmallerThanBuffer_NotAligned(string indent, string input)
9395
[InlineData(" ", "12", " 1", " 2")]
9496
public void BiggerThanBuffer_AlignedWithIndent(string indent, string input, string expected1stLine, string expected2ndLine)
9597
{
96-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
98+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
9799

98100
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: indent.Length);
99101

@@ -114,7 +116,7 @@ public void BiggerThanBuffer_AlignedWithIndent(string indent, string input, stri
114116
" 4\n")]
115117
public void XTimesBiggerThanBuffer_AlignedToMultipleLines(string indent, string input, string expected)
116118
{
117-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
119+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
118120

119121
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: indent.Length);
120122

@@ -128,7 +130,7 @@ public void XTimesBiggerThanBuffer_AlignedToMultipleLines(string indent, string
128130
[InlineData(" ", "12", "1", " 2")]
129131
public void BiggerThanBufferWithPrefixAlreadyWritten_AlignedWithIndentFromSecondLine(string indent, string input, string expected1stLine, string expected2ndLine)
130132
{
131-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
133+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
132134

133135
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: indent.Length);
134136

@@ -142,7 +144,7 @@ public void BiggerThanBufferWithPrefixAlreadyWritten_AlignedWithIndentFromSecond
142144
public void MultiLineWithoutAlign_NotChanged(string input)
143145
{
144146
input = input.Replace("\n", Environment.NewLine);
145-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: false);
147+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: false, stringBuilderProvider: new TestStringBuilderProvider());
146148

147149
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 0);
148150

@@ -165,7 +167,7 @@ public void NonStandardNewLines_AlignAsExpected(string input, string expected)
165167
{
166168
expected = expected.Replace("\n", Environment.NewLine) + Environment.NewLine;
167169

168-
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true);
170+
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
169171

170172
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 2);
171173

@@ -179,7 +181,7 @@ public void NonStandardNewLines_AlignAsExpected(string input, string expected)
179181
public void ShortMultiLineWithAlign_NoChange(string input)
180182
{
181183
input = input.Replace("\n", Environment.NewLine);
182-
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true);
184+
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
183185

184186
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 0);
185187

@@ -202,7 +204,7 @@ public void ShortMultiLineWithAlign_NoChange(string input)
202204
public void ShortMultiLineWithMixedNewLines_NewLinesReplacedByActualEnvironmentNewLines(string input)
203205
{
204206
string expected = input.Replace("\r", "").Replace("\n", Environment.NewLine) + Environment.NewLine;
205-
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true);
207+
var aligner = new ConsoleOutputAligner(bufferWidth: 10, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
206208

207209
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 0);
208210

@@ -217,7 +219,7 @@ public void MultiLineWithPrefixAlreadyWritten(string prefix, string input, strin
217219
{
218220
input = input.Replace("\n", Environment.NewLine);
219221
expected = expected.Replace("\n", Environment.NewLine);
220-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
222+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
221223

222224
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: prefix.Length);
223225

@@ -231,7 +233,7 @@ public void MultiLineWithoutPrefixAlreadyWritten(string prefix, string input, st
231233
{
232234
input = input.Replace("\n", Environment.NewLine);
233235
expected = expected.Replace("\n", Environment.NewLine);
234-
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true);
236+
var aligner = new ConsoleOutputAligner(bufferWidth: 4, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
235237

236238
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: false, prefixWidth: prefix.Length);
237239

@@ -244,7 +246,7 @@ public void MultiLineWithoutPrefixAlreadyWritten(string prefix, string input, st
244246
public void ShortTextWithTabs_NoChange(string input)
245247
{
246248
input = input.Replace("\n", Environment.NewLine);
247-
var aligner = new ConsoleOutputAligner(bufferWidth: 50, alignMessages: true);
249+
var aligner = new ConsoleOutputAligner(bufferWidth: 50, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
248250

249251
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: true, prefixWidth: 0);
250252

@@ -259,7 +261,7 @@ public void ShortTextWithTabs_NoChange(string input)
259261
public void LastTabOverLimit_NoChange(string prefix, string input, int bufferWidthWithoutNewLine, bool prefixAlreadyWritten)
260262
{
261263
input = input.Replace("\n", Environment.NewLine);
262-
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true);
264+
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
263265

264266
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: prefixAlreadyWritten, prefixWidth: prefix.Length);
265267

@@ -274,7 +276,7 @@ public void LastTabOverLimit_NoChange(string prefix, string input, int bufferWid
274276
public void LastTabAtLimit_NoChange(string prefix, string input, int bufferWidthWithoutNewLine, bool prefixAlreadyWritten)
275277
{
276278
input = input.Replace("\n", Environment.NewLine);
277-
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true);
279+
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
278280

279281
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: prefixAlreadyWritten, prefixWidth: prefix.Length);
280282

@@ -289,7 +291,7 @@ public void LastTabAtLimit_NoChange(string prefix, string input, int bufferWidth
289291
public void TabsMakesItJustOverLimit_IndentAndAlign(string prefix, string input, int bufferWidthWithoutNewLine, bool prefixAlreadyWritten)
290292
{
291293
input = input.Replace("\n", Environment.NewLine);
292-
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true);
294+
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
293295

294296
string output = aligner.AlignConsoleOutput(message: input + "x", prefixAlreadyWritten: prefixAlreadyWritten, prefixWidth: prefix.Length);
295297

@@ -366,11 +368,17 @@ public void MultiLinesOverLimit_IndentAndAlign(string prefix, string input, stri
366368
{
367369
input = input.Replace("\n", Environment.NewLine);
368370
expected = expected.Replace("\n", Environment.NewLine);
369-
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true);
371+
var aligner = new ConsoleOutputAligner(bufferWidth: bufferWidthWithoutNewLine + 1, alignMessages: true, stringBuilderProvider: new TestStringBuilderProvider());
370372

371373
string output = aligner.AlignConsoleOutput(message: input, prefixAlreadyWritten: prefixAlreadyWritten, prefixWidth: prefix.Length);
372374

373375
output.ShouldBe(expected);
374376
}
377+
378+
private sealed class TestStringBuilderProvider : IReusableStringBuilderProvider
379+
{
380+
public StringBuilder Acquire(int capacity) => new StringBuilder(capacity);
381+
public string GetStringAndRelease(StringBuilder builder) => builder.ToString();
382+
}
375383
}
376384
}

src/Build/Logging/BaseConsoleLogger.cs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Globalization;
89
using System.IO;
910
using System.Linq;
1011
using System.Text;
12+
using System.Threading;
1113
using Microsoft.Build.Evaluation;
1214
using Microsoft.Build.Framework;
1315
using Microsoft.Build.Internal;
@@ -24,7 +26,7 @@ namespace Microsoft.Build.BackEnd.Logging
2426
internal delegate void WriteLinePrettyFromResourceDelegate(int indentLevel, string resourceString, params object[] args);
2527
#endregion
2628

27-
internal abstract class BaseConsoleLogger : INodeLogger
29+
internal abstract class BaseConsoleLogger : INodeLogger, IReusableStringBuilderProvider
2830
{
2931
#region Properties
3032

@@ -130,7 +132,7 @@ public int Compare(Object a, Object b)
130132
/// <param name="indent">Depth to indent.</param>
131133
internal string IndentString(string s, int indent)
132134
{
133-
return OptimizedStringIndenter.IndentString(s, indent);
135+
return OptimizedStringIndenter.IndentString(s, indent, (IReusableStringBuilderProvider)this);
134136
}
135137

136138
/// <summary>
@@ -1187,6 +1189,14 @@ private bool ApplyVerbosityParameter(string parameterValue)
11871189

11881190
internal bool runningWithCharacterFileType = false;
11891191

1192+
/// <summary>
1193+
/// Since logging messages are processed serially, we can use a single StringBuilder wherever needed.
1194+
/// It should not be done directly, but rather through the <see cref="IReusableStringBuilderProvider"/> interface methods.
1195+
/// </summary>
1196+
private StringBuilder _sharedStringBuilder = new StringBuilder(0x100);
1197+
1198+
#endregion
1199+
11901200
#region Per-build Members
11911201

11921202
/// <summary>
@@ -1231,6 +1241,72 @@ private bool ApplyVerbosityParameter(string parameterValue)
12311241

12321242
#endregion
12331243

1234-
#endregion
1244+
/// <summary>
1245+
/// Since logging messages are processed serially, we can reuse a single StringBuilder wherever needed.
1246+
/// </summary>
1247+
StringBuilder IReusableStringBuilderProvider.Acquire(int capacity)
1248+
{
1249+
StringBuilder shared = Interlocked.Exchange(ref _sharedStringBuilder, null);
1250+
1251+
Debug.Assert(shared != null, "This is not supposed to be used in multiple threads or multiple time. One method is expected to return it before next acquire. Most probably it was not returned.");
1252+
if (shared == null)
1253+
{
1254+
// This is not supposed to be used concurrently. One method is expected to return it before next acquire.
1255+
// However to avoid bugs in production, we will create new string builder
1256+
return StringBuilderCache.Acquire(capacity);
1257+
}
1258+
1259+
if (shared.Capacity < capacity)
1260+
{
1261+
const int minimumCapacity = 0x100; // 256 characters, 512 bytes
1262+
const int maximumBracketedCapacity = 0x80_000; // 512K characters, 1MB
1263+
1264+
if (capacity <= minimumCapacity)
1265+
{
1266+
capacity = minimumCapacity;
1267+
}
1268+
else if (capacity < maximumBracketedCapacity)
1269+
{
1270+
// GC likes arrays allocated with power of two bytes. Lets make it happy.
1271+
1272+
// Find next power of two http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
1273+
int v = capacity;
1274+
1275+
v--;
1276+
v |= v >> 1;
1277+
v |= v >> 2;
1278+
v |= v >> 4;
1279+
v |= v >> 8;
1280+
v |= v >> 16;
1281+
v++;
1282+
1283+
capacity = v;
1284+
}
1285+
// If capacity is > maximumCapacity we will respect it and use it as is.
1286+
1287+
// Lets create new instance with enough capacity.
1288+
shared = new StringBuilder(capacity);
1289+
}
1290+
1291+
// Prepare for next use.
1292+
// Equivalent of sb.Clear() that works on .Net 3.5
1293+
shared.Length = 0;
1294+
1295+
return shared;
1296+
}
1297+
1298+
/// <summary>
1299+
/// Acquired StringBuilder must be returned before next use.
1300+
/// Unbalanced releases are not supported.
1301+
/// </summary>
1302+
string IReusableStringBuilderProvider.GetStringAndRelease(StringBuilder builder)
1303+
{
1304+
// This is not supposed to be used concurrently. One method is expected to return it before next acquire.
1305+
// But just for sure if _sharedBuilder was already returned, keep the former.
1306+
StringBuilder previous = Interlocked.CompareExchange(ref _sharedStringBuilder, builder, null);
1307+
Debug.Assert(previous == null, "This is not supposed to be used in multiple threads or multiple time. One method is expected to return it before next acquire. Most probably it was double returned.");
1308+
1309+
return builder.ToString();
1310+
}
12351311
}
12361312
}

src/Build/Logging/OptimizedStringIndenter.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
using System.Runtime.CompilerServices;
99
#else
1010
using System.Text;
11-
using Microsoft.Build.Framework;
1211
#endif
12+
using Microsoft.Build.Framework;
1313

1414
namespace Microsoft.Build.BackEnd.Logging;
1515

@@ -52,7 +52,7 @@ internal static class OptimizedStringIndenter
5252
#if NET7_0_OR_GREATER
5353
[SkipLocalsInit]
5454
#endif
55-
internal static unsafe string IndentString(string? s, int indent)
55+
internal static unsafe string IndentString(string? s, int indent, IReusableStringBuilderProvider stringBuilderProvider)
5656
{
5757
if (s is null)
5858
{
@@ -89,7 +89,7 @@ internal static unsafe string IndentString(string? s, int indent)
8989
});
9090
#pragma warning restore CS8500
9191
#else
92-
StringBuilder builder = StringBuilderCache.Acquire(indentedStringLength);
92+
StringBuilder builder = stringBuilderProvider.Acquire(indentedStringLength);
9393

9494
foreach (StringSegment segment in segments)
9595
{
@@ -99,7 +99,7 @@ internal static unsafe string IndentString(string? s, int indent)
9999
.AppendLine();
100100
}
101101

102-
string result = StringBuilderCache.GetStringAndRelease(builder);
102+
string result = stringBuilderProvider.GetStringAndRelease(builder);
103103
#endif
104104

105105
if (pooledArray is not null)

0 commit comments

Comments
 (0)