2
0
Эх сурвалжийг харах

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
Tonttu 4 сар өмнө
parent
commit
bc8bf380b2

+ 48 - 0
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;
+
+/// <summary>
+/// Compares the Set and Append implementations in combination.
+/// </summary>
+/// <remarks>
+/// 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.
+/// </remarks>
+[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<object> StringBuilderSource ()
+    {
+        return [new StringBuilder ()];
+    }
+}

+ 101 - 70
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -411,25 +411,25 @@ public static class EscSeqUtils
     {
     {
         // These control characters are used in the vtXXX emulation.
         // These control characters are used in the vtXXX emulation.
         return c switch
         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
         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
+        };
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -512,18 +512,18 @@ public static class EscSeqUtils
     public static ConsoleModifiers GetConsoleModifiers (string? value)
     public static ConsoleModifiers GetConsoleModifiers (string? value)
     {
     {
         return value switch
         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
 
 
     /// <summary>
     /// <summary>
     ///     Gets all the needed information about an escape sequence.
     ///     Gets all the needed information about an escape sequence.
@@ -1675,6 +1675,19 @@ public static class EscSeqUtils
     /// <returns></returns>
     /// <returns></returns>
     public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
     public static string CSI_SetCursorPosition (int row, int col) { return $"{CSI}{row};{col}H"; }
 
 
+    /// <summary>
+    ///     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
+    /// </summary>
+    /// <param name="builder">StringBuilder where to append the cursor position sequence.</param>
+    /// <param name="row">Origin is (1,1).</param>
+    /// <param name="col">Origin is (1,1).</param>
+    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 [ <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
     //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
     //ESC [ s - ANSISYSSC       Save Cursor – Ansi.sys emulation	**With no parameters, performs a save cursor operation like DECSC
     //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
     //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
     /// </summary>
     /// </summary>
     public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
     public static string CSI_SetForegroundColorRGB (int r, int g, int b) { return $"{CSI}38;2;{r};{g};{b}m"; }
 
 
+    /// <summary>
+    ///     ESC[38;2;{r};{g};{b}m	Append foreground color as RGB to StringBuilder.
+    /// </summary>
+    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");
+    }
+
     /// <summary>
     /// <summary>
     ///     ESC[48;2;{r};{g};{b}m	Set background color as RGB.
     ///     ESC[48;2;{r};{g};{b}m	Set background color as RGB.
     /// </summary>
     /// </summary>
     public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
     public static string CSI_SetBackgroundColorRGB (int r, int g, int b) { return $"{CSI}48;2;{r};{g};{b}m"; }
 
 
+    /// <summary>
+    ///     ESC[48;2;{r};{g};{b}m	Append background color as RGB to StringBuilder.
+    /// </summary>
+    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
     #endregion
 
 
     #region Requests
     #region Requests

+ 11 - 11
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs

@@ -176,7 +176,7 @@ internal class WindowsConsole
             _stringBuilder.Clear ();
             _stringBuilder.Clear ();
 
 
             _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
             _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition);
-            _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0));
+            EscSeqUtils.CSI_AppendCursorPosition (_stringBuilder, 0, 0);
 
 
             Attribute? prev = null;
             Attribute? prev = null;
 
 
@@ -187,8 +187,8 @@ internal class WindowsConsole
                 if (attr != prev)
                 if (attr != prev)
                 {
                 {
                     prev = attr;
                     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')
                 if (info.Char != '\x1b')
@@ -710,14 +710,14 @@ internal class WindowsConsole
         public readonly override string ToString ()
         public readonly override string ToString ()
         {
         {
             return (EventType switch
             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
+            })!;
         }
         }
     }
     }
 
 

+ 1 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs

@@ -252,7 +252,7 @@ internal class WindowsDriver : ConsoleDriver
         else
         else
         {
         {
             var sb = new StringBuilder ();
             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 ());
             WinConsole?.WriteANSI (sb.ToString ());
         }
         }