Browse Source

feat: StringBuilderCache

Giuseppe Lippolis 3 years ago
parent
commit
ba2747b897

+ 1 - 0
src/Avalonia.Base/Avalonia.Base.csproj

@@ -31,6 +31,7 @@
     <InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
+    <InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="Avalonia.Web.Blazor, PublicKey=$(AvaloniaPublicKey)" />
     <InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"/>
   </ItemGroup>

+ 2 - 2
src/Avalonia.Base/Input/KeyGesture.cs

@@ -106,7 +106,7 @@ namespace Avalonia.Input
 
         public override string ToString()
         {
-            var s = new StringBuilder();
+            var s = StringBuilderCache.Acquire();
 
             static void Plus(StringBuilder s)
             {
@@ -142,7 +142,7 @@ namespace Avalonia.Input
             Plus(s);
             s.Append(Key);
 
-            return s.ToString();
+            return StringBuilderCache.GetStringAndRelease(s);
         }
 
         public bool Matches(KeyEventArgs keyEvent) =>

+ 3 - 3
src/Avalonia.Base/Logging/TraceLogSink.cs

@@ -46,7 +46,7 @@ namespace Avalonia.Logging
             object? source,
             object?[]? values)
         {
-            var result = new StringBuilder(template.Length);
+            var result = StringBuilderCache.Acquire(template.Length);
             var r = new CharacterReader(template.AsSpan());
             var i = 0;
 
@@ -89,7 +89,7 @@ namespace Avalonia.Logging
                 result.Append(')');
             }
 
-            return result.ToString();
+            return StringBuilderCache.GetStringAndRelease(result);
         }
 
         private static string Format(
@@ -98,7 +98,7 @@ namespace Avalonia.Logging
             object? source,
             object?[] v)
         {
-            var result = new StringBuilder(template.Length);
+            var result = StringBuilderCache.Acquire(template.Length);
             var r = new CharacterReader(template.AsSpan());
             var i = 0;
 

+ 2 - 2
src/Avalonia.Base/Media/BoxShadow.cs

@@ -80,7 +80,7 @@ namespace Avalonia.Media
 
         public override string ToString()
         {
-            var sb = new StringBuilder();
+            var sb = StringBuilderCache.Acquire();
 
             if (IsEmpty)
             {
@@ -114,7 +114,7 @@ namespace Avalonia.Media
 
             sb.AppendFormat(" {0}", Color.ToString());
 
-            return sb.ToString();
+            return StringBuilderCache.GetStringAndRelease(sb);
         }
 
         public static unsafe BoxShadow Parse(string s)

+ 2 - 2
src/Avalonia.Base/Media/BoxShadows.cs

@@ -45,7 +45,7 @@ namespace Avalonia.Media
 
         public override string ToString()
         {
-            var sb = new StringBuilder();
+            var sb = StringBuilderCache.Acquire();
 
             if (Count == 0)
             {
@@ -57,7 +57,7 @@ namespace Avalonia.Media
                 sb.AppendFormat("{0} ", boxShadow.ToString());
             }
 
-            return sb.ToString();
+            return StringBuilderCache.GetStringAndRelease(sb);
 
         }
 

+ 2 - 2
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@@ -77,7 +77,7 @@ namespace Avalonia.Media.Fonts
         /// </returns>
         public override string ToString()
         {
-            var builder = new StringBuilder();
+            var builder = StringBuilderCache.Acquire();
 
             for (var index = 0; index < Names.Count; index++)
             {
@@ -91,7 +91,7 @@ namespace Avalonia.Media.Fonts
                 builder.Append(", ");
             }
 
-            return builder.ToString();
+            return StringBuilderCache.GetStringAndRelease(builder);
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Base/Media/HslColor.cs

@@ -202,7 +202,7 @@ namespace Avalonia.Media
         /// <inheritdoc/>
         public override string ToString()
         {
-            var sb = new StringBuilder();
+            var sb = StringBuilderCache.Acquire();
 
             // Use a format similar to CSS. However:
             //   - To ensure precision is never lost, allow decimal places.
@@ -225,7 +225,7 @@ namespace Avalonia.Media
             sb.Append(A.ToString(CultureInfo.InvariantCulture));
             sb.Append(')');
 
-            return sb.ToString();
+            return StringBuilderCache.GetStringAndRelease(sb);
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Base/Media/HsvColor.cs

@@ -202,7 +202,7 @@ namespace Avalonia.Media
         /// <inheritdoc/>
         public override string ToString()
         {
-            var sb = new StringBuilder();
+            var sb = StringBuilderCache.Acquire();
 
             // Use a format similar to CSS. However:
             //   - To ensure precision is never lost, allow decimal places.
@@ -225,7 +225,7 @@ namespace Avalonia.Media
             sb.Append(A.ToString(CultureInfo.InvariantCulture));
             sb.Append(')');
 
-            return sb.ToString();
+            return StringBuilderCache.GetStringAndRelease(sb);
         }
 
         /// <summary>

+ 68 - 0
src/Avalonia.Base/StringBuilderCache.cs

@@ -0,0 +1,68 @@
+// This file is imported from dotnet/runtime
+// Source Link: https://github.com/dotnet/runtime/blob/e63d21947e734db2da5093510a6636b5b7fb45b5/src/libraries/Common/src/System/Text/StringBuilderCache.cs
+// Commit: a9c5ead on Feb 10, 2021, https://github.com/dotnet/runtime/commit/a9c5eadd951dcba73167f72cc624eb790573663a
+// 
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Text;
+
+namespace Avalonia;
+
+// <summary>Provide a cached reusable instance of stringbuilder per thread.</summary>
+internal static class StringBuilderCache
+{
+    // The value 360 was chosen in discussion with performance experts as a compromise between using
+    // as little memory per thread as possible and still covering a large part of short-lived
+    // StringBuilder creations on the startup path of VS designers.
+    internal const int MaxBuilderSize = 360;
+    private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity
+
+    // WARNING: We allow diagnostic tools to directly inspect this member (t_cachedInstance).
+    // See https://github.com/dotnet/corert/blob/master/Documentation/design-docs/diagnostics/diagnostics-tools-contract.md for more details.
+    // Please do not change the type, the name, or the semantic usage of this member without understanding the implication for tools.
+    // Get in touch with the diagnostics team if you have questions.
+    [ThreadStatic]
+    private static StringBuilder? t_cachedInstance;
+
+    /// <summary>Get a StringBuilder for the specified capacity.</summary>
+    /// <remarks>If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied.</remarks>
+    public static StringBuilder Acquire(int capacity = DefaultCapacity)
+    {
+        if (capacity <= MaxBuilderSize)
+        {
+            StringBuilder? sb = t_cachedInstance;
+            if (sb != null)
+            {
+                // Avoid stringbuilder block fragmentation by getting a new StringBuilder
+                // when the requested size is larger than the current capacity
+                if (capacity <= sb.Capacity)
+                {
+                    t_cachedInstance = null;
+                    sb.Clear();
+                    return sb;
+                }
+            }
+        }
+
+        return new StringBuilder(capacity);
+    }
+
+    /// <summary>Place the specified builder in the cache if it is not too big.</summary>
+    public static void Release(StringBuilder sb)
+    {
+        if (sb.Capacity <= MaxBuilderSize)
+        {
+            t_cachedInstance = sb;
+        }
+    }
+
+    /// <summary>ToString() the stringbuilder, Release it to the cache, and return the resulting string.</summary>
+    public static string GetStringAndRelease(StringBuilder sb)
+    {
+        string result = sb.ToString();
+        Release(sb);
+        return result;
+    }
+}

+ 3 - 2
src/Avalonia.Base/Styling/NthChildSelector.cs

@@ -110,7 +110,8 @@ namespace Avalonia.Styling
         public override string ToString()
         {
             var expectedCapacity = NthLastChildSelectorName.Length + 8;
-            var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
+            var stringBuilder =  StringBuilderCache.Acquire(expectedCapacity);
+            stringBuilder.Append(_previous?.ToString());
             
             stringBuilder.Append(':');
             stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
@@ -140,7 +141,7 @@ namespace Avalonia.Styling
 
             stringBuilder.Append(')');
 
-            return stringBuilder.ToString();
+            return StringBuilderCache.GetStringAndRelease(stringBuilder);
         }
     }
 }

+ 2 - 2
src/Avalonia.Base/Styling/PropertyEqualsSelector.cs

@@ -42,7 +42,7 @@ namespace Avalonia.Styling
         {
             if (_selectorString == null)
             {
-                var builder = new StringBuilder();
+                var builder = StringBuilderCache.Acquire();
 
                 if (_previous != null)
                 {
@@ -67,7 +67,7 @@ namespace Avalonia.Styling
                 builder.Append(_value ?? string.Empty);
                 builder.Append(']');
 
-                _selectorString = builder.ToString();
+                _selectorString = StringBuilderCache.GetStringAndRelease(builder);
             }
 
             return _selectorString;

+ 2 - 2
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@@ -144,7 +144,7 @@ namespace Avalonia.Styling
 
         private string BuildSelectorString()
         {
-            var builder = new StringBuilder();
+            var builder = StringBuilderCache.Acquire();
 
             if (_previous != null)
             {
@@ -184,7 +184,7 @@ namespace Avalonia.Styling
                 }
             }
 
-            return builder.ToString();
+            return StringBuilderCache.GetStringAndRelease(builder);
         }
     }
 }

+ 1 - 0
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@@ -50,6 +50,7 @@
       <Compile Include="../Avalonia.Base/Utilities/MathUtilities.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
       </Compile>
+      <Compile Include="..\Avalonia.Base\StringBuilderCache.cs" Link="StringBuilderCache.cs" />
       <Compile Include="..\Markup\Avalonia.Markup\Markup\Parsers\ArgumentListParser.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
       </Compile>

+ 2 - 2
src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs

@@ -109,7 +109,7 @@ namespace Avalonia.Controls.Primitives
             // Cache results for next time as well
             if (closestKnownColor != KnownColor.None)
             {
-                StringBuilder sb = new StringBuilder(); 
+                var sb = StringBuilderCache.Acquire();
                 string name = closestKnownColor.ToString();
 
                 // Add spaces converting PascalCase to human-readable names
@@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives
                     sb.Append(name[i]);
                 }
 
-                string displayName = sb.ToString();
+                string displayName = StringBuilderCache.GetStringAndRelease(sb);
 
                 lock (cacheMutex)
                 {

+ 4 - 4
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -5990,7 +5990,7 @@ namespace Avalonia.Controls
         /// <returns>The formatted string.</returns>
         private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
         {
-            var text = new StringBuilder();
+            var text = StringBuilderCache.Acquire();
             var clipboardRowContent = e.ClipboardRowContent;
             var numberOfItem = clipboardRowContent.Count;
             for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
@@ -6007,7 +6007,7 @@ namespace Avalonia.Controls
                     text.Append('\n');
                 }
             }
-            return text.ToString();
+            return StringBuilderCache.GetStringAndRelease(text);
         }
 
         /// <summary>
@@ -6022,7 +6022,7 @@ namespace Avalonia.Controls
 
             if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0)
             {
-                StringBuilder textBuilder = new StringBuilder();
+                var textBuilder = StringBuilderCache.Acquire();
 
                 if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader)
                 {
@@ -6048,7 +6048,7 @@ namespace Avalonia.Controls
                     textBuilder.Append(FormatClipboardContent(itemArgs));
                 }
 
-                string text = textBuilder.ToString();
+                string text = StringBuilderCache.GetStringAndRelease(textBuilder);
 
                 if (!string.IsNullOrEmpty(text))
                 {

+ 4 - 4
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@@ -62,7 +62,7 @@ namespace Avalonia.Controls.Converters
 
         private static string ToString(KeyGesture gesture, string meta)
         {
-            var s = new StringBuilder();
+            var s = StringBuilderCache.Acquire();
 
             static void Plus(StringBuilder s)
             {
@@ -98,12 +98,12 @@ namespace Avalonia.Controls.Converters
             Plus(s);
             s.Append(ToString(gesture.Key));
 
-            return s.ToString();
+            return StringBuilderCache.GetStringAndRelease(s);
         }
 
         private static string ToOSXString(KeyGesture gesture)
         {
-            var s = new StringBuilder();
+            var s = StringBuilderCache.Acquire();
 
             if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control))
             {
@@ -127,7 +127,7 @@ namespace Avalonia.Controls.Converters
 
             s.Append(ToOSXString(gesture.Key));
 
-            return s.ToString();
+            return StringBuilderCache.GetStringAndRelease(s);
         }
 
         private static string ToString(Key key)

+ 2 - 2
src/Avalonia.Controls/Documents/InlineCollection.cs

@@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents
                     return _text;
                 }
 
-                var builder = new StringBuilder();
+                var builder = StringBuilderCache.Acquire();
 
                 foreach (var inline in this)
                 {
                     inline.AppendText(builder);
                 }
 
-                return builder.ToString();
+                return StringBuilderCache.GetStringAndRelease(builder);
             }
             set
             {

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Diagnostics
     {
         public static string PrintVisualTree(IVisual visual)
         {
-            StringBuilder result = new StringBuilder();
+            var result = new StringBuilder();
             PrintVisualTree(visual, result, 0);
             return result.ToString();
         }

+ 3 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj

@@ -7,6 +7,9 @@
     <DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
   </PropertyGroup>
   <Import Project="IncludeXamlIlSre.props" />
+  <ItemGroup>
+    <Compile Include="..\..\Avalonia.Base\StringBuilderCache.cs" Link="StringBuilderCache.cs" />
+  </ItemGroup>
   <ItemGroup>
     <PackageReference Include="System.Reflection.Emit" Version="4.3.0" />
   </ItemGroup>

+ 2 - 2
src/Windows/Avalonia.Win32/ClipboardFormats.cs

@@ -35,9 +35,9 @@ namespace Avalonia.Win32
 
         private static string QueryFormatName(ushort format)
         {
-            StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH);
+            var sb = StringBuilderCache.Acquire(MAX_FORMAT_NAME_LENGTH);
             if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0)
-                return sb.ToString();
+                return StringBuilderCache.GetStringAndRelease(sb);
             return null;
         }
 

+ 2 - 2
src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Win32.Input
 
         public string StringFromVirtualKey(uint virtualKey)
         {
-            StringBuilder result = new StringBuilder(256);
+            var result = StringBuilderCache.Acquire(256);
             int length = UnmanagedMethods.ToUnicode(
                 virtualKey,
                 0,
@@ -57,7 +57,7 @@ namespace Avalonia.Win32.Input
                 result,
                 256,
                 0);
-            return result.ToString();
+            return StringBuilderCache.GetStringAndRelease(result);
         }
 
         private void UpdateKeyStates()

+ 2 - 2
src/Windows/Avalonia.Win32/OleDataObject.cs

@@ -103,11 +103,11 @@ namespace Avalonia.Win32
                 for (int i = 0; i < fileCount; i++)
                 {
                     int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0);
-                    StringBuilder sb = new StringBuilder(pathLen+1);
+                    var sb = StringBuilderCache.Acquire(pathLen+1);
 
                     if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen)
                     {
-                        files.Add(sb.ToString());
+                        files.Add(StringBuilderCache.GetStringAndRelease(sb));
                     }
                 }
             }

+ 1 - 0
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@@ -16,6 +16,7 @@
   <ItemGroup>
     <Compile Include="..\..\..\src\Markup\Avalonia.Markup.Xaml.Loader\CompilerExtensions\**\*.cs" />
     <Compile Include="..\..\..\src\Markup\Avalonia.Markup.Xaml.Loader\AvaloniaXamlIlRuntimeCompiler.cs" />
+    <Compile Include="..\..\Avalonia.Base\StringBuilderCache.cs" Link="StringBuilderCache.cs" />
   </ItemGroup>
   <ItemGroup>
     <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />