Skip to content

Commit bc8bf38

Browse files
authored
Reduce legacy Windows driver ANSI escape sequence intermediate string allocations (#3936)
* Skip WindowsConsole StringBuilder append ANSI escape sequence intermediate string allocations Appending InterpolatedStringHandler directly to StringBuilder skips the formatting related intermediate string allocation. This should also be usable in other console implementation but currently I have no WSL etc. setup to actually verify correct functionality. * Add CSI_Set* and CSI_Append* comparison benchmark * Clean up CSI_SetVsAppend benchmark * Change benchmark names to match the method group
1 parent af9c6d7 commit bc8bf38

File tree

4 files changed

+161
-82
lines changed

4 files changed

+161
-82
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
6+
7+
/// <summary>
8+
/// Compares the Set and Append implementations in combination.
9+
/// </summary>
10+
/// <remarks>
11+
/// A bit misleading because *CursorPosition is called very seldom compared to the other operations
12+
/// but they are very similar in performance because they do very similar things.
13+
/// </remarks>
14+
[MemoryDiagnoser]
15+
[BenchmarkCategory (nameof (Tui.EscSeqUtils))]
16+
// Hide useless empty column from results.
17+
[HideColumns ("stringBuilder")]
18+
public class CSI_SetVsAppend
19+
{
20+
[Benchmark (Baseline = true)]
21+
[ArgumentsSource (nameof (StringBuilderSource))]
22+
public StringBuilder Set (StringBuilder stringBuilder)
23+
{
24+
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetBackgroundColorRGB (1, 2, 3));
25+
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetForegroundColorRGB (3, 2, 1));
26+
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetCursorPosition (4, 2));
27+
// Clear to prevent out of memory exception from consecutive iterations.
28+
stringBuilder.Clear ();
29+
return stringBuilder;
30+
}
31+
32+
[Benchmark]
33+
[ArgumentsSource (nameof (StringBuilderSource))]
34+
public StringBuilder Append (StringBuilder stringBuilder)
35+
{
36+
Tui.EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, 1, 2, 3);
37+
Tui.EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, 3, 2, 1);
38+
Tui.EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 4, 2);
39+
// Clear to prevent out of memory exception from consecutive iterations.
40+
stringBuilder.Clear ();
41+
return stringBuilder;
42+
}
43+
44+
public static IEnumerable<object> StringBuilderSource ()
45+
{
46+
return [new StringBuilder ()];
47+
}
48+
}

Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

+101-70
Original file line numberDiff line numberDiff line change
@@ -411,25 +411,25 @@ public static string GetC1ControlChar (in char c)
411411
{
412412
// These control characters are used in the vtXXX emulation.
413413
return c switch
414-
{
415-
'D' => "IND", // Index
416-
'E' => "NEL", // Next Line
417-
'H' => "HTS", // Tab Set
418-
'M' => "RI", // Reverse Index
419-
'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only
420-
'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only
421-
'P' => "DCS", // Device Control String
422-
'V' => "SPA", // Start of Guarded Area
423-
'W' => "EPA", // End of Guarded Area
424-
'X' => "SOS", // Start of String
425-
'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA)
426-
'[' => "CSI", // Control Sequence Introducer
427-
'\\' => "ST", // String Terminator
428-
']' => "OSC", // Operating System Command
429-
'^' => "PM", // Privacy Message
430-
'_' => "APC", // Application Program Command
431-
_ => string.Empty
432-
};
414+
{
415+
'D' => "IND", // Index
416+
'E' => "NEL", // Next Line
417+
'H' => "HTS", // Tab Set
418+
'M' => "RI", // Reverse Index
419+
'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only
420+
'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only
421+
'P' => "DCS", // Device Control String
422+
'V' => "SPA", // Start of Guarded Area
423+
'W' => "EPA", // End of Guarded Area
424+
'X' => "SOS", // Start of String
425+
'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA)
426+
'[' => "CSI", // Control Sequence Introducer
427+
'\\' => "ST", // String Terminator
428+
']' => "OSC", // Operating System Command
429+
'^' => "PM", // Privacy Message
430+
'_' => "APC", // Application Program Command
431+
_ => string.Empty
432+
};
433433
}
434434

435435

@@ -462,46 +462,46 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons
462462
}
463463

464464
return (terminator, value) switch
465-
{
466-
('A', _) => ConsoleKey.UpArrow,
467-
('B', _) => ConsoleKey.DownArrow,
468-
('C', _) => ConsoleKey.RightArrow,
469-
('D', _) => ConsoleKey.LeftArrow,
470-
('E', _) => ConsoleKey.Clear,
471-
('F', _) => ConsoleKey.End,
472-
('H', _) => ConsoleKey.Home,
473-
('P', _) => ConsoleKey.F1,
474-
('Q', _) => ConsoleKey.F2,
475-
('R', _) => ConsoleKey.F3,
476-
('S', _) => ConsoleKey.F4,
477-
('Z', _) => ConsoleKey.Tab,
478-
('~', "2") => ConsoleKey.Insert,
479-
('~', "3") => ConsoleKey.Delete,
480-
('~', "5") => ConsoleKey.PageUp,
481-
('~', "6") => ConsoleKey.PageDown,
482-
('~', "15") => ConsoleKey.F5,
483-
('~', "17") => ConsoleKey.F6,
484-
('~', "18") => ConsoleKey.F7,
485-
('~', "19") => ConsoleKey.F8,
486-
('~', "20") => ConsoleKey.F9,
487-
('~', "21") => ConsoleKey.F10,
488-
('~', "23") => ConsoleKey.F11,
489-
('~', "24") => ConsoleKey.F12,
490-
// These terminators are used by macOS on a numeric keypad without keys modifiers
491-
('l', null) => ConsoleKey.Add,
492-
('m', null) => ConsoleKey.Subtract,
493-
('p', null) => ConsoleKey.Insert,
494-
('q', null) => ConsoleKey.End,
495-
('r', null) => ConsoleKey.DownArrow,
496-
('s', null) => ConsoleKey.PageDown,
497-
('t', null) => ConsoleKey.LeftArrow,
498-
('u', null) => ConsoleKey.Clear,
499-
('v', null) => ConsoleKey.RightArrow,
500-
('w', null) => ConsoleKey.Home,
501-
('x', null) => ConsoleKey.UpArrow,
502-
('y', null) => ConsoleKey.PageUp,
503-
(_, _) => 0
504-
};
465+
{
466+
('A', _) => ConsoleKey.UpArrow,
467+
('B', _) => ConsoleKey.DownArrow,
468+
('C', _) => ConsoleKey.RightArrow,
469+
('D', _) => ConsoleKey.LeftArrow,
470+
('E', _) => ConsoleKey.Clear,
471+
('F', _) => ConsoleKey.End,
472+
('H', _) => ConsoleKey.Home,
473+
('P', _) => ConsoleKey.F1,
474+
('Q', _) => ConsoleKey.F2,
475+
('R', _) => ConsoleKey.F3,
476+
('S', _) => ConsoleKey.F4,
477+
('Z', _) => ConsoleKey.Tab,
478+
('~', "2") => ConsoleKey.Insert,
479+
('~', "3") => ConsoleKey.Delete,
480+
('~', "5") => ConsoleKey.PageUp,
481+
('~', "6") => ConsoleKey.PageDown,
482+
('~', "15") => ConsoleKey.F5,
483+
('~', "17") => ConsoleKey.F6,
484+
('~', "18") => ConsoleKey.F7,
485+
('~', "19") => ConsoleKey.F8,
486+
('~', "20") => ConsoleKey.F9,
487+
('~', "21") => ConsoleKey.F10,
488+
('~', "23") => ConsoleKey.F11,
489+
('~', "24") => ConsoleKey.F12,
490+
// These terminators are used by macOS on a numeric keypad without keys modifiers
491+
('l', null) => ConsoleKey.Add,
492+
('m', null) => ConsoleKey.Subtract,
493+
('p', null) => ConsoleKey.Insert,
494+
('q', null) => ConsoleKey.End,
495+
('r', null) => ConsoleKey.DownArrow,
496+
('s', null) => ConsoleKey.PageDown,
497+
('t', null) => ConsoleKey.LeftArrow,
498+
('u', null) => ConsoleKey.Clear,
499+
('v', null) => ConsoleKey.RightArrow,
500+
('w', null) => ConsoleKey.Home,
501+
('x', null) => ConsoleKey.UpArrow,
502+
('y', null) => ConsoleKey.PageUp,
503+
(_, _) => 0
504+
};
505505
}
506506

507507
/// <summary>
@@ -512,18 +512,18 @@ public static ConsoleKey GetConsoleKey (char terminator, string? value, ref Cons
512512
public static ConsoleModifiers GetConsoleModifiers (string? value)
513513
{
514514
return value switch
515-
{
516-
"2" => ConsoleModifiers.Shift,
517-
"3" => ConsoleModifiers.Alt,
518-
"4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt,
519-
"5" => ConsoleModifiers.Control,
520-
"6" => ConsoleModifiers.Shift | ConsoleModifiers.Control,
521-
"7" => ConsoleModifiers.Alt | ConsoleModifiers.Control,
522-
"8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
523-
_ => 0
524-
};
515+
{
516+
"2" => ConsoleModifiers.Shift,
517+
"3" => ConsoleModifiers.Alt,
518+
"4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt,
519+
"5" => ConsoleModifiers.Control,
520+
"6" => ConsoleModifiers.Shift | ConsoleModifiers.Control,
521+
"7" => ConsoleModifiers.Alt | ConsoleModifiers.Control,
522+
"8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control,
523+
_ => 0
524+
};
525525
}
526-
#nullable restore
526+
#nullable restore
527527

528528
/// <summary>
529529
/// Gets all the needed information about an escape sequence.
@@ -1675,6 +1675,19 @@ public static ConsoleKeyInfo [] ToConsoleKeyInfoArray (string ansi)
16751675
/// <returns></returns>
16761676
public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
16771677

1678+
/// <summary>
1679+
/// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column
1680+
/// of the y line
1681+
/// </summary>
1682+
/// <param name="builder">StringBuilder where to append the cursor position sequence.</param>
1683+
/// <param name="row">Origin is (1,1).</param>
1684+
/// <param name="col">Origin is (1,1).</param>
1685+
public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int col)
1686+
{
1687+
// InterpolatedStringHandler is composed in stack, skipping the string allocation.
1688+
builder.Append ($"{CSI}{row};{col}H");
1689+
}
1690+
16781691
//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
16791692
//ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC
16801693
//ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC
@@ -1785,11 +1798,29 @@ public enum DECSCUSR_Style
17851798
/// </summary>
17861799
public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
17871800

1801+
/// <summary>
1802+
/// ESC[38;2;{r};{g};{b}m Append foreground color as RGB to StringBuilder.
1803+
/// </summary>
1804+
public static void CSI_AppendForegroundColorRGB (StringBuilder builder, int r, int g, int b)
1805+
{
1806+
// InterpolatedStringHandler is composed in stack, skipping the string allocation.
1807+
builder.Append ($"{CSI}38;2;{r};{g};{b}m");
1808+
}
1809+
17881810
/// <summary>
17891811
/// ESC[48;2;{r};{g};{b}m Set background color as RGB.
17901812
/// </summary>
17911813
public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
17921814

1815+
/// <summary>
1816+
/// ESC[48;2;{r};{g};{b}m Append background color as RGB to StringBuilder.
1817+
/// </summary>
1818+
public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, int g, int b)
1819+
{
1820+
// InterpolatedStringHandler is composed in stack, skipping the string allocation.
1821+
builder.Append ($"{CSI}48;2;{r};{g};{b}m");
1822+
}
1823+
17931824
#endregion
17941825

17951826
#region Requests

Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

+11-11
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
176176
_stringBuilder.Clear ();
177177

178178
_stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
179-
_stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
179+
EscSeqUtils.CSI_AppendCursorPosition (_stringBuilder, 0, 0);
180180

181181
Attribute? prev = null;
182182

@@ -187,8 +187,8 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
187187
if (attr != prev)
188188
{
189189
prev = attr;
190-
_stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B));
191-
_stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B));
190+
EscSeqUtils.CSI_AppendForegroundColorRGB (_stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B);
191+
EscSeqUtils.CSI_AppendBackgroundColorRGB (_stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B);
192192
}
193193

194194
if (info.Char != '\x1b')
@@ -710,14 +710,14 @@ public struct InputRecord
710710
public readonly override string ToString ()
711711
{
712712
return (EventType switch
713-
{
714-
EventType.Focus => FocusEvent.ToString (),
715-
EventType.Key => KeyEvent.ToString (),
716-
EventType.Menu => MenuEvent.ToString (),
717-
EventType.Mouse => MouseEvent.ToString (),
718-
EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
719-
_ => "Unknown event type: " + EventType
720-
})!;
713+
{
714+
EventType.Focus => FocusEvent.ToString (),
715+
EventType.Key => KeyEvent.ToString (),
716+
EventType.Menu => MenuEvent.ToString (),
717+
EventType.Mouse => MouseEvent.ToString (),
718+
EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (),
719+
_ => "Unknown event type: " + EventType
720+
})!;
721721
}
722722
}
723723

Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public override void UpdateCursor ()
252252
else
253253
{
254254
var sb = new StringBuilder ();
255-
sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1));
255+
EscSeqUtils.CSI_AppendCursorPosition (sb, position.Y + 1, position.X + 1);
256256
WinConsole?.WriteANSI (sb.ToString ());
257257
}
258258

0 commit comments

Comments
 (0)