Skip to content

Commit ef20ff6

Browse files
TheTonttutig
andauthored
Reduce string allocations in IConsoleOutput implementations (#3978)
* Change IConsoleOutput.Write(string) overload parameter to ReadOnlySpan<char> Allows the caller more flexibility about choosing a buffer per use case. * NetOutput: Write StringBuilder directly to the std out text stream * Add EscSeqUtils.CSI_WriteCursorPosition Writes cursor position sequence to text writer without string allocation. * NetOutput: Skip cursor position escape sequence string allocation * Replace CSI_(Enable|Disable)MouseEvents static properties with readonly fields Changed for the sake of consistency with rest of the EscSegutils fields rather than performance. Also prevents bugs from accidentally setting the properties. * Use EscSeqUtils.CSI_Append(Foreground|Background)ColorRGB in v2 drivers * WindowsOutput SetCursorVisibility: Remove intermediate string builder * WindowsOutput.WriteToConsole: Use rented array as intermediate write buffer The large intermediate string builder remains a challenge. :) * NetOutput: Console.Out for the sake of consistency Also might have missed one of the Console.Out.Write(StringBuilder) calls... * Avoid Rune.ToString() in NetOutput.Write(IOutputBuffer) --------- Co-authored-by: Tig <[email protected]>
1 parent af633fc commit ef20ff6

File tree

7 files changed

+141
-52
lines changed

7 files changed

+141
-52
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Tui = Terminal.Gui;
3+
4+
namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
5+
6+
[MemoryDiagnoser]
7+
// Hide useless column from results.
8+
[HideColumns ("writer")]
9+
public class CSI_SetVsWrite
10+
{
11+
[Benchmark (Baseline = true)]
12+
[ArgumentsSource (nameof (TextWriterSource))]
13+
public TextWriter Set (TextWriter writer)
14+
{
15+
writer.Write (Tui.EscSeqUtils.CSI_SetCursorPosition (1, 1));
16+
return writer;
17+
}
18+
19+
[Benchmark]
20+
[ArgumentsSource (nameof (TextWriterSource))]
21+
public TextWriter Write (TextWriter writer)
22+
{
23+
Tui.EscSeqUtils.CSI_WriteCursorPosition (writer, 1, 1);
24+
return writer;
25+
}
26+
27+
public static IEnumerable<object> TextWriterSource ()
28+
{
29+
return [StringWriter.Null];
30+
}
31+
}

Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#nullable enable
2-
using Terminal.Gui.ConsoleDrivers;
2+
using System.Globalization;
33
using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping;
44

55
namespace Terminal.Gui;
@@ -154,13 +154,13 @@ public enum ClearScreenOptions
154154
/// <summary>
155155
/// Control sequence for disabling mouse events.
156156
/// </summary>
157-
public static string CSI_DisableMouseEvents { get; set; } =
157+
public static readonly string CSI_DisableMouseEvents =
158158
CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse;
159159

160160
/// <summary>
161161
/// Control sequence for enabling mouse events.
162162
/// </summary>
163-
public static string CSI_EnableMouseEvents { get; set; } =
163+
public static readonly string CSI_EnableMouseEvents =
164164
CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse;
165165

166166
/// <summary>
@@ -1688,6 +1688,32 @@ public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int
16881688
builder.Append ($"{CSI}{row};{col}H");
16891689
}
16901690

1691+
/// <summary>
1692+
/// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
1693+
/// of the y line
1694+
/// </summary>
1695+
/// <param name="writer">TextWriter where to write the cursor position sequence.</param>
1696+
/// <param name="row">Origin is (1,1).</param>
1697+
/// <param name="col">Origin is (1,1).</param>
1698+
public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col)
1699+
{
1700+
const int maxInputBufferSize =
1701+
// CSI (2) + ';' + 'H'
1702+
4 +
1703+
// row + col (2x int sign + int max value)
1704+
2 + 20;
1705+
Span<char> buffer = stackalloc char[maxInputBufferSize];
1706+
if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten))
1707+
{
1708+
string tooLongCursorPositionSequence = $"{CSI}{row};{col}H";
1709+
throw new InvalidOperationException (
1710+
$"{nameof(CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length}).");
1711+
}
1712+
1713+
ReadOnlySpan<char> cursorPositionSequence = buffer[..charsWritten];
1714+
writer.Write (cursorPositionSequence);
1715+
}
1716+
16911717
//ESC [ <y> ; <x> f - HVP Horizontal Vertical Position* Cursor moves to<x>; <y> coordinate within the viewport, where <x> is the column of the<y> line
16921718
//ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC
16931719
//ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC

Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public interface IConsoleOutput : IDisposable
1111
/// <see cref="IOutputBuffer"/> overload.
1212
/// </summary>
1313
/// <param name="text"></param>
14-
void Write (string text);
14+
void Write (ReadOnlySpan<char> text);
1515

1616
/// <summary>
1717
/// Write the contents of the <paramref name="buffer"/> to the console

Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs

+29-21
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ public NetOutput ()
3434
}
3535

3636
/// <inheritdoc/>
37-
public void Write (string text) { Console.Write (text); }
37+
public void Write (ReadOnlySpan<char> text)
38+
{
39+
Console.Out.Write (text);
40+
}
3841

3942
/// <inheritdoc/>
4043
public void Write (IOutputBuffer buffer)
@@ -57,6 +60,9 @@ public void Write (IOutputBuffer buffer)
5760
CursorVisibility? savedVisibility = _cachedCursorVisibility;
5861
SetCursorVisibility (CursorVisibility.Invisible);
5962

63+
const int maxCharsPerRune = 2;
64+
Span<char> runeBuffer = stackalloc char[maxCharsPerRune];
65+
6066
for (int row = top; row < rows; row++)
6167
{
6268
if (Console.WindowHeight < 1)
@@ -115,26 +121,28 @@ public void Write (IOutputBuffer buffer)
115121
{
116122
redrawAttr = attr;
117123

118-
output.Append (
119-
EscSeqUtils.CSI_SetForegroundColorRGB (
120-
attr.Foreground.R,
121-
attr.Foreground.G,
122-
attr.Foreground.B
123-
)
124-
);
125-
126-
output.Append (
127-
EscSeqUtils.CSI_SetBackgroundColorRGB (
128-
attr.Background.R,
129-
attr.Background.G,
130-
attr.Background.B
131-
)
132-
);
124+
EscSeqUtils.CSI_AppendForegroundColorRGB (
125+
output,
126+
attr.Foreground.R,
127+
attr.Foreground.G,
128+
attr.Foreground.B
129+
);
130+
131+
EscSeqUtils.CSI_AppendBackgroundColorRGB (
132+
output,
133+
attr.Background.R,
134+
attr.Background.G,
135+
attr.Background.B
136+
);
133137
}
134138

135139
outputWidth++;
140+
141+
// Avoid Rune.ToString() by appending the rune chars.
136142
Rune rune = buffer.Contents [row, col].Rune;
137-
output.Append (rune);
143+
int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer);
144+
ReadOnlySpan<char> runeChars = runeBuffer[..runeCharsWritten];
145+
output.Append (runeChars);
138146

139147
if (buffer.Contents [row, col].CombiningMarks.Count > 0)
140148
{
@@ -162,7 +170,7 @@ public void Write (IOutputBuffer buffer)
162170
if (output.Length > 0)
163171
{
164172
SetCursorPositionImpl (lastCol, row);
165-
Console.Write (output);
173+
Console.Out.Write (output);
166174
}
167175
}
168176

@@ -171,7 +179,7 @@ public void Write (IOutputBuffer buffer)
171179
if (!string.IsNullOrWhiteSpace (s.SixelData))
172180
{
173181
SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
174-
Console.Write (s.SixelData);
182+
Console.Out.Write (s.SixelData);
175183
}
176184
}
177185

@@ -185,7 +193,7 @@ public void Write (IOutputBuffer buffer)
185193
private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
186194
{
187195
SetCursorPositionImpl (lastCol, row);
188-
Console.Write (output);
196+
Console.Out.Write (output);
189197
output.Clear ();
190198
lastCol += outputWidth;
191199
outputWidth = 0;
@@ -222,7 +230,7 @@ private bool SetCursorPositionImpl (int col, int row)
222230

223231
// + 1 is needed because non-Windows is based on 1 instead of 0 and
224232
// Console.CursorTop/CursorLeft isn't reliable.
225-
Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1));
233+
EscSeqUtils.CSI_WriteCursorPosition (Console.Out, row + 1, col + 1);
226234

227235
return true;
228236
}

Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs

+28-20
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
#nullable enable
2+
using System.Buffers;
23
using System.ComponentModel;
34
using System.Runtime.InteropServices;
45
using Microsoft.Extensions.Logging;
56
using static Terminal.Gui.WindowsConsole;
67

78
namespace Terminal.Gui;
89

9-
internal class WindowsOutput : IConsoleOutput
10+
internal partial class WindowsOutput : IConsoleOutput
1011
{
11-
[DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
12-
private static extern bool WriteConsole (
12+
[LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
13+
[return: MarshalAs (UnmanagedType.Bool)]
14+
private static partial bool WriteConsole (
1315
nint hConsoleOutput,
14-
string lpbufer,
16+
ReadOnlySpan<char> lpbufer,
1517
uint numberOfCharsToWriten,
1618
out uint lpNumberOfCharsWritten,
1719
nint lpReserved
@@ -84,7 +86,7 @@ public WindowsOutput ()
8486
}
8587
}
8688

87-
public void Write (string str)
89+
public void Write (ReadOnlySpan<char> str)
8890
{
8991
if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
9092
{
@@ -183,7 +185,6 @@ public void Write (IOutputBuffer buffer)
183185

184186
public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors)
185187
{
186-
var stringBuilder = new StringBuilder ();
187188

188189
//Debug.WriteLine ("WriteToConsole");
189190

@@ -213,10 +214,10 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
213214
}
214215
else
215216
{
216-
stringBuilder.Clear ();
217+
StringBuilder stringBuilder = new();
217218

218219
stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
219-
stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
220+
EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0);
220221

221222
Attribute? prev = null;
222223

@@ -227,8 +228,8 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
227228
if (attr != prev)
228229
{
229230
prev = attr;
230-
stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
231-
stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
231+
EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
232+
EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
232233
}
233234

234235
if (info.Char != '\x1b')
@@ -247,14 +248,20 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
247248
stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition);
248249
stringBuilder.Append (EscSeqUtils.CSI_HideCursor);
249250

250-
var s = stringBuilder.ToString ();
251+
// TODO: Potentially could stackalloc whenever reasonably small (<= 8 kB?) write buffer is needed.
252+
char [] rentedWriteArray = ArrayPool<char>.Shared.Rent (minimumLength: stringBuilder.Length);
253+
try
254+
{
255+
Span<char> writeBuffer = rentedWriteArray.AsSpan(0, stringBuilder.Length);
256+
stringBuilder.CopyTo (0, writeBuffer, stringBuilder.Length);
251257

252-
// TODO: requires extensive testing if we go down this route
253-
// If console output has changed
254-
//if (s != _lastWrite)
255-
//{
256-
// supply console with the new content
257-
result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
258+
// Supply console with the new content.
259+
result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero);
260+
}
261+
finally
262+
{
263+
ArrayPool<char>.Shared.Return (rentedWriteArray);
264+
}
258265

259266
foreach (SixelToRender sixel in Application.Sixel)
260267
{
@@ -297,9 +304,10 @@ public Size GetWindowSize ()
297304
/// <inheritdoc/>
298305
public void SetCursorVisibility (CursorVisibility visibility)
299306
{
300-
var sb = new StringBuilder ();
301-
sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
302-
Write (sb.ToString ());
307+
string cursorVisibilitySequence = visibility != CursorVisibility.Invisible
308+
? EscSeqUtils.CSI_ShowCursor
309+
: EscSeqUtils.CSI_HideCursor;
310+
Write (cursorVisibilitySequence);
303311
}
304312

305313
private Point _lastCursorPosition;

Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Terminal.Gui;
88

9-
internal class WindowsConsole
9+
internal partial class WindowsConsole
1010
{
1111
private CancellationTokenSource? _inputReadyCancellationTokenSource;
1212
private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ());
@@ -926,10 +926,11 @@ public static extern bool WriteConsoleOutput (
926926
ref SmallRect lpWriteRegion
927927
);
928928

929-
[DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)]
930-
private static extern bool WriteConsole (
929+
[LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
930+
[return: MarshalAs (UnmanagedType.Bool)]
931+
private static partial bool WriteConsole (
931932
nint hConsoleOutput,
932-
string lpbufer,
933+
ReadOnlySpan<char> lpbufer,
933934
uint NumberOfCharsToWriten,
934935
out uint lpNumberOfCharsWritten,
935936
nint lpReserved

Tests/UnitTests/Input/EscSeqUtilsTests.cs

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using JetBrains.Annotations;
1+
using System.Text;
22
using UnitTests;
33

44
// ReSharper disable HeuristicUnreachableCode
@@ -685,7 +685,7 @@ public void DecodeEscSeq_Multiple_Tests ()
685685
top.Add (view);
686686
Application.Begin (top);
687687

688-
Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
688+
Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 });
689689

690690
ClearAll ();
691691

@@ -741,7 +741,7 @@ public void DecodeEscSeq_Multiple_Tests ()
741741
// set Application.WantContinuousButtonPressedView to null
742742
view.WantContinuousButtonPressed = false;
743743

744-
Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 });
744+
Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 });
745745

746746
Application.RequestStop ();
747747
}
@@ -1548,6 +1548,21 @@ public void InsertArray_Tests (string toInsert, string current, int? index, stri
15481548
Assert.Equal (result, cki);
15491549
}
15501550

1551+
[Theory]
1552+
[InlineData (0, 0, $"{EscSeqUtils.CSI}0;0H")]
1553+
[InlineData (int.MaxValue, int.MaxValue, $"{EscSeqUtils.CSI}2147483647;2147483647H")]
1554+
[InlineData (int.MinValue, int.MinValue, $"{EscSeqUtils.CSI}-2147483648;-2147483648H")]
1555+
public void CSI_WriteCursorPosition_ReturnsCorrectEscSeq (int row, int col, string expected)
1556+
{
1557+
StringBuilder builder = new();
1558+
using StringWriter writer = new(builder);
1559+
1560+
EscSeqUtils.CSI_WriteCursorPosition (writer, row, col);
1561+
1562+
string actual = builder.ToString();
1563+
Assert.Equal (expected, actual);
1564+
}
1565+
15511566
private void ClearAll ()
15521567
{
15531568
EscSeqRequests.Clear ();

0 commit comments

Comments
 (0)