From bc8bf380b22aec1d97a7ec8a17df59de94582901 Mon Sep 17 00:00:00 2001 From: Tonttu <15074459+TheTonttu@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:36:31 +0200 Subject: [PATCH] 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 --- .../EscSeqUtils/CSI_SetVsAppend.cs | 48 +++++ .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 171 +++++++++++------- .../WindowsDriver/WindowsConsole.cs | 22 +-- .../WindowsDriver/WindowsDriver.cs | 2 +- 4 files changed, 161 insertions(+), 82 deletions(-) create mode 100644 Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs diff --git a/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs b/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs new file mode 100644 index 000000000..169ebd740 --- /dev/null +++ b/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsAppend.cs @@ -0,0 +1,48 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Tui = Terminal.Gui; + +namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils; + +/// +/// Compares the Set and Append implementations in combination. +/// +/// +/// A bit misleading because *CursorPosition is called very seldom compared to the other operations +/// but they are very similar in performance because they do very similar things. +/// +[MemoryDiagnoser] +[BenchmarkCategory (nameof (Tui.EscSeqUtils))] +// Hide useless empty column from results. +[HideColumns ("stringBuilder")] +public class CSI_SetVsAppend +{ + [Benchmark (Baseline = true)] + [ArgumentsSource (nameof (StringBuilderSource))] + public StringBuilder Set (StringBuilder stringBuilder) + { + stringBuilder.Append (Tui.EscSeqUtils.CSI_SetBackgroundColorRGB (1, 2, 3)); + stringBuilder.Append (Tui.EscSeqUtils.CSI_SetForegroundColorRGB (3, 2, 1)); + stringBuilder.Append (Tui.EscSeqUtils.CSI_SetCursorPosition (4, 2)); + // Clear to prevent out of memory exception from consecutive iterations. + stringBuilder.Clear (); + return stringBuilder; + } + + [Benchmark] + [ArgumentsSource (nameof (StringBuilderSource))] + public StringBuilder Append (StringBuilder stringBuilder) + { + Tui.EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, 1, 2, 3); + Tui.EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, 3, 2, 1); + Tui.EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 4, 2); + // Clear to prevent out of memory exception from consecutive iterations. + stringBuilder.Clear (); + return stringBuilder; + } + + public static IEnumerable StringBuilderSource () + { + return [new StringBuilder ()]; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 5ce2af939..5bfdf039e 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -411,25 +411,25 @@ public static class EscSeqUtils { // These control characters are used in the vtXXX emulation. return c switch - { - 'D' => "IND", // Index - 'E' => "NEL", // Next Line - 'H' => "HTS", // Tab Set - 'M' => "RI", // Reverse Index - 'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only - 'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only - 'P' => "DCS", // Device Control String - 'V' => "SPA", // Start of Guarded Area - 'W' => "EPA", // End of Guarded Area - 'X' => "SOS", // Start of String - 'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA) - '[' => "CSI", // Control Sequence Introducer - '\\' => "ST", // String Terminator - ']' => "OSC", // Operating System Command - '^' => "PM", // Privacy Message - '_' => "APC", // Application Program Command - _ => string.Empty - }; + { + 'D' => "IND", // Index + 'E' => "NEL", // Next Line + 'H' => "HTS", // Tab Set + 'M' => "RI", // Reverse Index + 'N' => "SS2", // Single Shift Select of G2 Character Set: affects next character only + 'O' => "SS3", // Single Shift Select of G3 Character Set: affects next character only + 'P' => "DCS", // Device Control String + 'V' => "SPA", // Start of Guarded Area + 'W' => "EPA", // End of Guarded Area + 'X' => "SOS", // Start of String + 'Z' => "DECID", // Return Terminal ID Obsolete form of CSI c (DA) + '[' => "CSI", // Control Sequence Introducer + '\\' => "ST", // String Terminator + ']' => "OSC", // Operating System Command + '^' => "PM", // Privacy Message + '_' => "APC", // Application Program Command + _ => string.Empty + }; } @@ -462,46 +462,46 @@ public static class EscSeqUtils } return (terminator, value) switch - { - ('A', _) => ConsoleKey.UpArrow, - ('B', _) => ConsoleKey.DownArrow, - ('C', _) => ConsoleKey.RightArrow, - ('D', _) => ConsoleKey.LeftArrow, - ('E', _) => ConsoleKey.Clear, - ('F', _) => ConsoleKey.End, - ('H', _) => ConsoleKey.Home, - ('P', _) => ConsoleKey.F1, - ('Q', _) => ConsoleKey.F2, - ('R', _) => ConsoleKey.F3, - ('S', _) => ConsoleKey.F4, - ('Z', _) => ConsoleKey.Tab, - ('~', "2") => ConsoleKey.Insert, - ('~', "3") => ConsoleKey.Delete, - ('~', "5") => ConsoleKey.PageUp, - ('~', "6") => ConsoleKey.PageDown, - ('~', "15") => ConsoleKey.F5, - ('~', "17") => ConsoleKey.F6, - ('~', "18") => ConsoleKey.F7, - ('~', "19") => ConsoleKey.F8, - ('~', "20") => ConsoleKey.F9, - ('~', "21") => ConsoleKey.F10, - ('~', "23") => ConsoleKey.F11, - ('~', "24") => ConsoleKey.F12, - // These terminators are used by macOS on a numeric keypad without keys modifiers - ('l', null) => ConsoleKey.Add, - ('m', null) => ConsoleKey.Subtract, - ('p', null) => ConsoleKey.Insert, - ('q', null) => ConsoleKey.End, - ('r', null) => ConsoleKey.DownArrow, - ('s', null) => ConsoleKey.PageDown, - ('t', null) => ConsoleKey.LeftArrow, - ('u', null) => ConsoleKey.Clear, - ('v', null) => ConsoleKey.RightArrow, - ('w', null) => ConsoleKey.Home, - ('x', null) => ConsoleKey.UpArrow, - ('y', null) => ConsoleKey.PageUp, - (_, _) => 0 - }; + { + ('A', _) => ConsoleKey.UpArrow, + ('B', _) => ConsoleKey.DownArrow, + ('C', _) => ConsoleKey.RightArrow, + ('D', _) => ConsoleKey.LeftArrow, + ('E', _) => ConsoleKey.Clear, + ('F', _) => ConsoleKey.End, + ('H', _) => ConsoleKey.Home, + ('P', _) => ConsoleKey.F1, + ('Q', _) => ConsoleKey.F2, + ('R', _) => ConsoleKey.F3, + ('S', _) => ConsoleKey.F4, + ('Z', _) => ConsoleKey.Tab, + ('~', "2") => ConsoleKey.Insert, + ('~', "3") => ConsoleKey.Delete, + ('~', "5") => ConsoleKey.PageUp, + ('~', "6") => ConsoleKey.PageDown, + ('~', "15") => ConsoleKey.F5, + ('~', "17") => ConsoleKey.F6, + ('~', "18") => ConsoleKey.F7, + ('~', "19") => ConsoleKey.F8, + ('~', "20") => ConsoleKey.F9, + ('~', "21") => ConsoleKey.F10, + ('~', "23") => ConsoleKey.F11, + ('~', "24") => ConsoleKey.F12, + // These terminators are used by macOS on a numeric keypad without keys modifiers + ('l', null) => ConsoleKey.Add, + ('m', null) => ConsoleKey.Subtract, + ('p', null) => ConsoleKey.Insert, + ('q', null) => ConsoleKey.End, + ('r', null) => ConsoleKey.DownArrow, + ('s', null) => ConsoleKey.PageDown, + ('t', null) => ConsoleKey.LeftArrow, + ('u', null) => ConsoleKey.Clear, + ('v', null) => ConsoleKey.RightArrow, + ('w', null) => ConsoleKey.Home, + ('x', null) => ConsoleKey.UpArrow, + ('y', null) => ConsoleKey.PageUp, + (_, _) => 0 + }; } /// @@ -512,18 +512,18 @@ public static class EscSeqUtils public static ConsoleModifiers GetConsoleModifiers (string? value) { return value switch - { - "2" => ConsoleModifiers.Shift, - "3" => ConsoleModifiers.Alt, - "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt, - "5" => ConsoleModifiers.Control, - "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control, - "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control, - "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, - _ => 0 - }; + { + "2" => ConsoleModifiers.Shift, + "3" => ConsoleModifiers.Alt, + "4" => ConsoleModifiers.Shift | ConsoleModifiers.Alt, + "5" => ConsoleModifiers.Control, + "6" => ConsoleModifiers.Shift | ConsoleModifiers.Control, + "7" => ConsoleModifiers.Alt | ConsoleModifiers.Control, + "8" => ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control, + _ => 0 + }; } - #nullable restore +#nullable restore /// /// Gets all the needed information about an escape sequence. @@ -1675,6 +1675,19 @@ public static class EscSeqUtils /// public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; } + /// + /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column + /// of the y line + /// + /// StringBuilder where to append the cursor position sequence. + /// Origin is (1,1). + /// Origin is (1,1). + public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int col) + { + // InterpolatedStringHandler is composed in stack, skipping the string allocation. + builder.Append ($"{CSI}{row};{col}H"); + } + //ESC [ ; f - HVP Horizontal Vertical Position* Cursor moves to; coordinate within the viewport, where is the column of the line //ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC //ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC @@ -1785,11 +1798,29 @@ public static class EscSeqUtils /// public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; } + /// + /// ESC[38;2;{r};{g};{b}m Append foreground color as RGB to StringBuilder. + /// + public static void CSI_AppendForegroundColorRGB (StringBuilder builder, int r, int g, int b) + { + // InterpolatedStringHandler is composed in stack, skipping the string allocation. + builder.Append ($"{CSI}38;2;{r};{g};{b}m"); + } + /// /// ESC[48;2;{r};{g};{b}m Set background color as RGB. /// public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; } + /// + /// ESC[48;2;{r};{g};{b}m Append background color as RGB to StringBuilder. + /// + public static void CSI_AppendBackgroundColorRGB (StringBuilder builder, int r, int g, int b) + { + // InterpolatedStringHandler is composed in stack, skipping the string allocation. + builder.Append ($"{CSI}48;2;{r};{g};{b}m"); + } + #endregion #region Requests diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 826679051..4e93018cf 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -176,7 +176,7 @@ internal class WindowsConsole _stringBuilder.Clear (); _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); + EscSeqUtils.CSI_AppendCursorPosition (_stringBuilder, 0, 0); Attribute? prev = null; @@ -187,8 +187,8 @@ internal class WindowsConsole if (attr != prev) { prev = attr; - _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); - _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); + EscSeqUtils.CSI_AppendForegroundColorRGB (_stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); + EscSeqUtils.CSI_AppendBackgroundColorRGB (_stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B); } if (info.Char != '\x1b') @@ -710,14 +710,14 @@ internal class WindowsConsole public readonly override string ToString () { return (EventType switch - { - EventType.Focus => FocusEvent.ToString (), - EventType.Key => KeyEvent.ToString (), - EventType.Menu => MenuEvent.ToString (), - EventType.Mouse => MouseEvent.ToString (), - EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), - _ => "Unknown event type: " + EventType - })!; + { + EventType.Focus => FocusEvent.ToString (), + EventType.Key => KeyEvent.ToString (), + EventType.Menu => MenuEvent.ToString (), + EventType.Mouse => MouseEvent.ToString (), + EventType.WindowBufferSize => WindowBufferSizeEvent.ToString (), + _ => "Unknown event type: " + EventType + })!; } } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 26ca3023b..1f74c0321 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -252,7 +252,7 @@ internal class WindowsDriver : ConsoleDriver else { var sb = new StringBuilder (); - sb.Append (EscSeqUtils.CSI_SetCursorPosition (position.Y + 1, position.X + 1)); + EscSeqUtils.CSI_AppendCursorPosition (sb, position.Y + 1, position.X + 1); WinConsole?.WriteANSI (sb.ToString ()); }