Prechádzať zdrojové kódy

Replace StringBuilderPool with ValueStringBuilder (#1697)

Marko Lahma 1 rok pred
rodič
commit
90691bbaba

+ 5 - 5
Jint/Native/Array/ArrayPrototype.cs

@@ -1,12 +1,12 @@
 #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue
 
 using System.Linq;
+using System.Text;
 using Jint.Collections;
 using Jint.Native.Iterator;
 using Jint.Native.Number;
 using Jint.Native.Object;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Descriptors.Specialized;
@@ -1265,15 +1265,15 @@ namespace Jint.Native.Array
                 return s;
             }
 
-            using var sb = StringBuilderPool.Rent();
-            sb.Builder.Append(s);
+            var sb = new ValueStringBuilder(stackalloc char[128]);
+            sb.Append(s);
             for (uint k = 1; k < len; k++)
             {
                 if (sep != "")
                 {
-                    sb.Builder.Append(sep);
+                    sb.Append(sep);
                 }
-                sb.Builder.Append(StringFromJsValue(o.Get(k)));
+                sb.Append(StringFromJsValue(o.Get(k)));
             }
 
             return sb.ToString();

+ 13 - 7
Jint/Native/BigInt/BigIntPrototype.cs

@@ -1,9 +1,9 @@
 using System.Globalization;
 using System.Numerics;
+using System.Text;
 using Jint.Collections;
 using Jint.Native.Object;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -114,22 +114,28 @@ internal sealed class BigIntPrototype : Prototype
             value = -value;
         }
 
-        const string digits = "0123456789abcdefghijklmnopqrstuvwxyz";
+        const string Digits = "0123456789abcdefghijklmnopqrstuvwxyz";
 
-        using var builder = StringBuilderPool.Rent();
-        var sb = builder.Builder;
+        var sb = new ValueStringBuilder(stackalloc char[64]);
 
         for (; value > 0; value /= radixMV)
         {
             var d = (int) (value % radixMV);
-            sb.Append(digits[d]);
+            sb.Append(Digits[d]);
         }
 
+#if NET6_0_OR_GREATER
+        var charArray = sb.Length < 512 ? stackalloc char[sb.Length] : new char[sb.Length];
+        sb.AsSpan().CopyTo(charArray);
+        charArray.Reverse();
+#else
         var charArray = new char[sb.Length];
-        sb.CopyTo(0, charArray, 0, charArray.Length);
+        sb.AsSpan().CopyTo(charArray);
         System.Array.Reverse(charArray);
+#endif
 
-        return (negative ? "-" : "") + new string(charArray);
+        var s = new string(charArray);
+        return negative ? '-' + s : s;
     }
 
     /// <summary>

+ 4 - 22
Jint/Native/Json/JsonParser.cs

@@ -5,8 +5,6 @@ using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Text;
 using Esprima;
-using Jint.Native.Object;
-using Jint.Pooling;
 using Jint.Runtime;
 
 namespace Jint.Native.Json
@@ -174,7 +172,7 @@ namespace Jint.Native.Json
 
         private Token ScanNumericLiteral(ref State state)
         {
-            var sb = state.TokenBuffer;
+            var sb = new ValueStringBuilder(stackalloc char[128]);
             var start = _index;
             var ch = _source.CharCodeAt(_index);
             var canBeInteger = true;
@@ -249,7 +247,6 @@ namespace Jint.Native.Json
             }
 
             var number = sb.ToString();
-            sb.Clear();
 
             JsNumber value;
             if (canBeInteger && long.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longResult) && longResult != -0)
@@ -312,7 +309,7 @@ namespace Jint.Native.Json
             int start = _index;
             ++_index;
 
-            var sb = state.TokenBuffer;
+            var sb = new ValueStringBuilder(stackalloc char[128]);
             while (_index < _length)
             {
                 char ch = _source[_index++];
@@ -383,7 +380,6 @@ namespace Jint.Native.Json
             }
 
             string value = sb.ToString();
-            sb.Clear();
             return CreateToken(Tokens.String, value, '\"', new JsString(value), new TextRange(start, _index));
         }
 
@@ -704,8 +700,7 @@ namespace Jint.Native.Json
             _length = _source.Length;
             _lookahead = null!;
 
-            using var wrapper = StringBuilderPool.Rent();
-            State state = new State(wrapper.Builder);
+            State state = new State();
 
             Peek(ref state);
             JsValue jsv = ParseJsonValue(ref state);
@@ -719,22 +714,9 @@ namespace Jint.Native.Json
             return jsv;
         }
 
+        [StructLayout(LayoutKind.Auto)]
         private ref struct State
         {
-            public State(StringBuilder tokenBuffer)
-            {
-                TokenBuffer = tokenBuffer;
-                CurrentDepth = 0;
-            }
-
-            /// <summary>
-            /// StringBuilder instance which can be used to collect
-            /// characters into a single string. Must only be used
-            /// when no child-parser gets called. Must be cleared
-            /// after usage.
-            /// </summary>
-            public StringBuilder TokenBuffer { get; }
-
             /// <summary>
             /// The current recursion depth
             /// </summary>

+ 6 - 4
Jint/Native/Json/JsonSerializer.cs

@@ -9,7 +9,6 @@ using Jint.Native.Number.Dtoa;
 using Jint.Native.Object;
 using Jint.Native.Proxy;
 using Jint.Native.String;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -54,9 +53,9 @@ namespace Jint.Native.Json
             var wrapper = _engine.Realm.Intrinsics.Object.Construct(Arguments.Empty);
             wrapper.DefineOwnProperty(JsString.Empty, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable));
 
-            using var jsonBuilder = StringBuilderPool.Rent();
+            var jsonBuilder = new StringBuilder();
 
-            var target = new SerializerState(jsonBuilder.Builder);
+            var target = new SerializerState(jsonBuilder);
             if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined)
             {
                 return JsValue.Undefined;
@@ -196,7 +195,10 @@ namespace Jint.Native.Json
                     }
 
                     target.DtoaBuilder.Reset();
-                    NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, target.Json);
+                    var sb = new ValueStringBuilder(stackalloc char[128]);
+                    NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, ref sb);
+                    target.Json.Append(sb.ToString());
+
                     return SerializeResult.NotUndefined;
                 }
 

+ 71 - 65
Jint/Native/Number/NumberPrototype.cs

@@ -4,7 +4,6 @@ using System.Text;
 using Jint.Collections;
 using Jint.Native.Number.Dtoa;
 using Jint.Native.Object;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -282,43 +281,42 @@ namespace Jint.Native.Number
                 return CreateExponentialRepresentation(dtoaBuilder, exponent, negative, p);
             }
 
-            using (var builder = StringBuilderPool.Rent())
+            var sb = new ValueStringBuilder(stackalloc char[64]);
+
+            // Use fixed notation.
+            if (negative)
             {
-                // Use fixed notation.
-                if (negative)
-                {
-                    builder.Builder.Append('-');
-                }
+                sb.Append('-');
+            }
 
-                if (decimalPoint <= 0)
-                {
-                    builder.Builder.Append("0.");
-                    builder.Builder.Append('0', -decimalPoint);
-                    builder.Builder.Append(dtoaBuilder._chars, 0, dtoaBuilder.Length);
-                    builder.Builder.Append('0', p - dtoaBuilder.Length);
-                }
-                else
+            if (decimalPoint <= 0)
+            {
+                sb.Append("0.");
+                sb.Append('0', -decimalPoint);
+                sb.Append(dtoaBuilder._chars.AsSpan(0, dtoaBuilder.Length));
+                sb.Append('0', p - dtoaBuilder.Length);
+            }
+            else
+            {
+                int m = System.Math.Min(dtoaBuilder.Length, decimalPoint);
+                sb.Append(dtoaBuilder._chars.AsSpan(0, m));
+                sb.Append('0', System.Math.Max(0, decimalPoint - dtoaBuilder.Length));
+                if (decimalPoint < p)
                 {
-                    int m = System.Math.Min(dtoaBuilder.Length, decimalPoint);
-                    builder.Builder.Append(dtoaBuilder._chars, 0, m);
-                    builder.Builder.Append('0', System.Math.Max(0, decimalPoint - dtoaBuilder.Length));
-                    if (decimalPoint < p)
+                    sb.Append('.');
+                    var extra = negative ? 2 : 1;
+                    if (dtoaBuilder.Length > decimalPoint)
                     {
-                        builder.Builder.Append('.');
-                        var extra = negative ? 2 : 1;
-                        if (dtoaBuilder.Length > decimalPoint)
-                        {
-                            int len = dtoaBuilder.Length - decimalPoint;
-                            int n = System.Math.Min(len, p - (builder.Builder.Length - extra));
-                            builder.Builder.Append(dtoaBuilder._chars, decimalPoint, n);
-                        }
-
-                        builder.Builder.Append('0', System.Math.Max(0, extra + (p - builder.Builder.Length)));
+                        int len = dtoaBuilder.Length - decimalPoint;
+                        int n = System.Math.Min(len, p - (sb.Length - extra));
+                        sb.Append(dtoaBuilder._chars.AsSpan(decimalPoint, n));
                     }
-                }
 
-                return builder.ToString();
+                    sb.Append('0', System.Math.Max(0, extra + (p - sb.Length)));
+                }
             }
+
+            return sb.ToString();
         }
 
         private static string CreateExponentialRepresentation(
@@ -334,26 +332,24 @@ namespace Jint.Native.Number
                 exponent = -exponent;
             }
 
-            using (var builder = StringBuilderPool.Rent())
+            var sb = new ValueStringBuilder(stackalloc char[64]);
+            if (negative)
             {
-                if (negative)
-                {
-                    builder.Builder.Append('-');
-                }
-                builder.Builder.Append(buffer._chars[0]);
-                if (significantDigits != 1)
-                {
-                    builder.Builder.Append('.');
-                    builder.Builder.Append(buffer._chars, 1, buffer.Length - 1);
-                    int length = buffer.Length;
-                    builder.Builder.Append('0', significantDigits - length);
-                }
-
-                builder.Builder.Append('e');
-                builder.Builder.Append(negativeExponent ? '-' : '+');
-                builder.Builder.Append(exponent);
-                return builder.ToString();
+                sb.Append('-');
+            }
+            sb.Append(buffer._chars[0]);
+            if (significantDigits != 1)
+            {
+                sb.Append('.');
+                sb.Append(buffer._chars.AsSpan(1, buffer.Length - 1));
+                int length = buffer.Length;
+                sb.Append('0', significantDigits - length);
             }
+
+            sb.Append('e');
+            sb.Append(negativeExponent ? '-' : '+');
+            sb.Append(exponent.ToString(CultureInfo.InvariantCulture));
+            return sb.ToString();
         }
 
         private JsValue ToNumberString(JsValue thisObject, JsValue[] arguments)
@@ -419,15 +415,25 @@ namespace Jint.Native.Number
                 return "0";
             }
 
-            using var result = StringBuilderPool.Rent();
+            using var sb = new ValueStringBuilder(stackalloc char[64]);
             while (n > 0)
             {
                 var digit = (int) (n % radix);
-                n = n / radix;
-                result.Builder.Insert(0, Digits[digit]);
+                n /= radix;
+                sb.Append(Digits[digit]);
             }
 
-            return result.ToString();
+#if NET6_0_OR_GREATER
+            var charArray = sb.Length < 512 ? stackalloc char[sb.Length] : new char[sb.Length];
+            sb.AsSpan().CopyTo(charArray);
+            charArray.Reverse();
+#else
+            var charArray = new char[sb.Length];
+            sb.AsSpan().CopyTo(charArray);
+            System.Array.Reverse(charArray);
+#endif
+
+            return new string(charArray);
         }
 
         internal static string ToFractionBase(double n, int radix)
@@ -441,14 +447,14 @@ namespace Jint.Native.Number
                 return "0";
             }
 
-            using var result = StringBuilderPool.Rent();
+            using var result = new ValueStringBuilder(stackalloc char[64]);
             while (n > 0 && result.Length < 50) // arbitrary limit
             {
                 var c = n*radix;
                 var d = (int) c;
                 n = c - d;
 
-                result.Builder.Append(Digits[d]);
+                result.Append(Digits[d]);
             }
 
             return result.ToString();
@@ -456,15 +462,15 @@ namespace Jint.Native.Number
 
         private static string ToNumberString(double m)
         {
-            using var stringBuilder = StringBuilderPool.Rent();
-            NumberToString(m, new DtoaBuilder(), stringBuilder.Builder);
-            return stringBuilder.Builder.ToString();
+            var stringBuilder = new ValueStringBuilder(stackalloc char[128]);
+            NumberToString(m, new DtoaBuilder(), ref stringBuilder);
+            return stringBuilder.ToString();
         }
 
         internal static void NumberToString(
             double m,
             DtoaBuilder builder,
-            StringBuilder stringBuilder)
+            ref ValueStringBuilder stringBuilder)
         {
             if (double.IsNaN(m))
             {
@@ -500,22 +506,22 @@ namespace Jint.Native.Number
             if (builder.Length <= decimal_point && decimal_point <= 21)
             {
                 // ECMA-262 section 9.8.1 step 6.
-                stringBuilder.Append(builder._chars, 0, builder.Length);
+                stringBuilder.Append(builder._chars.AsSpan(0, builder.Length));
                 stringBuilder.Append('0', decimal_point - builder.Length);
             }
             else if (0 < decimal_point && decimal_point <= 21)
             {
                 // ECMA-262 section 9.8.1 step 7.
-                stringBuilder.Append(builder._chars, 0, decimal_point);
+                stringBuilder.Append(builder._chars.AsSpan(0, decimal_point));
                 stringBuilder.Append('.');
-                stringBuilder.Append(builder._chars, decimal_point, builder.Length - decimal_point);
+                stringBuilder.Append(builder._chars.AsSpan(decimal_point, builder.Length - decimal_point));
             }
             else if (decimal_point <= 0 && decimal_point > -6)
             {
                 // ECMA-262 section 9.8.1 step 8.
                 stringBuilder.Append("0.");
                 stringBuilder.Append('0', -decimal_point);
-                stringBuilder.Append(builder._chars, 0, builder.Length);
+                stringBuilder.Append(builder._chars.AsSpan(0, builder.Length));
             }
             else
             {
@@ -524,7 +530,7 @@ namespace Jint.Native.Number
                 if (builder.Length != 1)
                 {
                     stringBuilder.Append('.');
-                    stringBuilder.Append(builder._chars, 1, builder.Length - 1);
+                    stringBuilder.Append(builder._chars.AsSpan(1, builder.Length - 1));
                 }
 
                 stringBuilder.Append('e');
@@ -535,7 +541,7 @@ namespace Jint.Native.Number
                     exponent = -exponent;
                 }
 
-                stringBuilder.Append(exponent);
+                stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture));
             }
         }
     }

+ 5 - 8
Jint/Native/RegExp/RegExpPrototype.cs

@@ -1,12 +1,12 @@
 #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue
 
+using System.Text;
 using System.Text.RegularExpressions;
 using Jint.Collections;
 using Jint.Native.Number;
 using Jint.Native.Object;
 using Jint.Native.String;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -337,8 +337,7 @@ namespace Jint.Native.RegExp
             // $`	Inserts the portion of the string that precedes the matched substring.
             // $'	Inserts the portion of the string that follows the matched substring.
             // $n or $nn	Where n or nn are decimal digits, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object.
-            using var replacementBuilder = StringBuilderPool.Rent();
-            var sb = replacementBuilder.Builder;
+            var sb = new ValueStringBuilder();
             for (var i = 0; i < replacement.Length; i++)
             {
                 char c = replacement[i];
@@ -353,14 +352,12 @@ namespace Jint.Native.RegExp
                         case '&':
                             sb.Append(matched);
                             break;
-#pragma warning disable CA1846
                         case '`':
-                            sb.Append(str.Substring(0, position));
+                            sb.Append(str.AsSpan(0, position));
                             break;
                         case '\'':
-                            sb.Append(str.Substring(position + matched.Length));
+                            sb.Append(str.AsSpan(position + matched.Length));
                             break;
-#pragma warning restore CA1846
                         case '<':
                             var gtPos = replacement.IndexOf('>', i + 1);
                             if (gtPos == -1 || namedCaptures.IsUndefined())
@@ -430,7 +427,7 @@ namespace Jint.Native.RegExp
                 }
             }
 
-            return replacementBuilder.ToString();
+            return sb.ToString();
         }
 
         /// <summary>

+ 5 - 6
Jint/Native/String/StringConstructor.cs

@@ -1,10 +1,10 @@
 #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue
 
+using System.Text;
 using Jint.Collections;
 using Jint.Native.Array;
 using Jint.Native.Function;
 using Jint.Native.Object;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -82,8 +82,7 @@ namespace Jint.Native.String
         private JsValue FromCodePoint(JsValue thisObject, JsValue[] arguments)
         {
             JsNumber codePoint;
-            using var wrapper = StringBuilderPool.Rent();
-            var result = wrapper.Builder;
+            var result = new ValueStringBuilder(stackalloc char[10]);
             foreach (var a in arguments)
             {
                 int point;
@@ -145,18 +144,18 @@ namespace Jint.Native.String
                 return JsString.Empty;
             }
 
-            using var result = StringBuilderPool.Rent();
+            var result = new ValueStringBuilder();
             for (var i = 0; i < length; i++)
             {
                 if (i > 0)
                 {
                     if (i < arguments.Length && !arguments[i].IsUndefined())
                     {
-                        result.Builder.Append(TypeConverter.ToString(arguments[i]));
+                        result.Append(TypeConverter.ToString(arguments[i]));
                     }
                 }
 
-                result.Builder.Append(TypeConverter.ToString(operations.Get((ulong) i)));
+                result.Append(TypeConverter.ToString(operations.Get((ulong) i)));
             }
 
             return result.ToString();

+ 9 - 11
Jint/Native/String/StringPrototype.cs

@@ -9,7 +9,6 @@ using Jint.Native.Json;
 using Jint.Native.Object;
 using Jint.Native.RegExp;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -643,8 +642,7 @@ namespace Jint.Native.String
             var advanceBy = System.Math.Max(1, searchLength);
 
             var endOfLastMatch = 0;
-            using var pool = StringBuilderPool.Rent();
-            var result = pool.Builder;
+            var result = new ValueStringBuilder();
 
             var position = StringIndexOf(thisString, searchString, 0);
             while (position != -1)
@@ -662,7 +660,9 @@ namespace Jint.Native.String
                     replacement =  RegExpPrototype.GetSubstitution(searchString, thisString, position, captures, Undefined, TypeConverter.ToString(replaceValue));
                 }
 
-                result.Append(preserved).Append(replacement);
+                result.Append(preserved);
+                result.Append(replacement);
+
                 endOfLastMatch = position + searchLength;
 
                 position = StringIndexOf(thisString, searchString, position + advanceBy);
@@ -671,7 +671,7 @@ namespace Jint.Native.String
             if (endOfLastMatch < thisString.Length)
             {
 #if NETFRAMEWORK
-                result.Append(thisString.Substring(endOfLastMatch));
+                result.Append(thisString.AsSpan(endOfLastMatch));
 #else
                 result.Append(thisString[endOfLastMatch..]);
 #endif
@@ -1144,11 +1144,10 @@ namespace Jint.Native.String
                 return new string(s[0], (int) n);
             }
 
-            using var sb = StringBuilderPool.Rent();
-            sb.Builder.EnsureCapacity((int) (n * s.Length));
+            var sb = new ValueStringBuilder((int) (n * s.Length));
             for (var i = 0; i < n; ++i)
             {
-                sb.Builder.Append(s);
+                sb.Append(s);
             }
 
             return sb.ToString();
@@ -1170,8 +1169,7 @@ namespace Jint.Native.String
             var strLen = s.Length;
             var k = 0;
 
-            using var builder = StringBuilderPool.Rent();
-            var result = builder.Builder;
+            var result = new ValueStringBuilder();
             while (k < strLen)
             {
                 var cp = CodePointAt(s, k);
@@ -1182,7 +1180,7 @@ namespace Jint.Native.String
                 }
                 else
                 {
-                    result.Append(s, k, cp.CodeUnitCount);
+                    result.Append(s.AsSpan(k, cp.CodeUnitCount));
                 }
                 k += cp.CodeUnitCount;
             }

+ 6 - 6
Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CA1859 // Use concrete types when possible for improved performance -- most of prototype methods return JsValue
 
 using System.Linq;
+using System.Text;
 using Jint.Collections;
 using Jint.Native.Array;
 using Jint.Native.ArrayBuffer;
@@ -8,7 +9,6 @@ using Jint.Native.Iterator;
 using Jint.Native.Number;
 using Jint.Native.Object;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime;
 using Jint.Runtime.Descriptors;
 using Jint.Runtime.Interop;
@@ -637,15 +637,15 @@ namespace Jint.Native.TypedArray
                 return s;
             }
 
-            using var sb = StringBuilderPool.Rent();
-            sb.Builder.Append(s);
+            var result = new ValueStringBuilder();
+            result.Append(s);
             for (var k = 1; k < len; k++)
             {
-                sb.Builder.Append(sep);
-                sb.Builder.Append(StringFromJsValue(o[k]));
+                result.Append(sep);
+                result.Append(StringFromJsValue(o[k]));
             }
 
-            return sb.ToString();
+            return result.ToString();
         }
 
         /// <summary>

+ 0 - 62
Jint/Pooling/StringBuilderPool.cs

@@ -1,62 +0,0 @@
-// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.
-
-using System.Diagnostics;
-using System.Text;
-
-namespace Jint.Pooling
-{
-    /// <summary>
-    /// Pooling of StringBuilder instances.
-    /// </summary>
-    internal static class StringBuilderPool
-    {
-        private static readonly ConcurrentObjectPool<StringBuilder> _pool;
-
-        static StringBuilderPool()
-        {
-            _pool = new ConcurrentObjectPool<StringBuilder>(() => new StringBuilder());
-        }
-
-        public static BuilderWrapper Rent()
-        {
-            var builder = _pool.Allocate();
-            Debug.Assert(builder.Length == 0);
-            return new BuilderWrapper(builder, _pool);
-        }
-
-        internal readonly struct BuilderWrapper : IDisposable
-        {
-            public readonly StringBuilder Builder;
-            private readonly ConcurrentObjectPool<StringBuilder> _pool;
-
-            public BuilderWrapper(StringBuilder builder, ConcurrentObjectPool<StringBuilder> pool)
-            {
-                Builder = builder;
-                _pool = pool;
-            }
-
-            public int Length => Builder.Length;
-
-            public override string ToString()
-            {
-                return Builder.ToString();
-            }
-
-            public void Dispose()
-            {
-                var builder = Builder;
-
-                // do not store builders that are too large.
-                if (builder.Capacity <= 1024 * 1024)
-                {
-                    builder.Clear();
-                    _pool.Free(builder);
-                }
-                else
-                {
-                    _pool.ForgetTrackedObject(builder);
-                }
-            }
-        }
-    }
-}

+ 331 - 0
Jint/Pooling/ValueStringBuilder.cs

@@ -0,0 +1,331 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// ReSharper disable once CheckNamespace
+namespace System.Text;
+
+internal ref struct ValueStringBuilder
+{
+    private char[]? _arrayToReturnToPool;
+    private Span<char> _chars;
+    private int _pos;
+
+    public ValueStringBuilder(Span<char> initialBuffer)
+    {
+        _arrayToReturnToPool = null;
+        _chars = initialBuffer;
+        _pos = 0;
+    }
+
+    public ValueStringBuilder(int initialCapacity)
+    {
+        _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
+        _chars = _arrayToReturnToPool;
+        _pos = 0;
+    }
+
+    public int Length
+    {
+        get => _pos;
+        set
+        {
+            Debug.Assert(value >= 0);
+            Debug.Assert(value <= _chars.Length);
+            _pos = value;
+        }
+    }
+
+    public int Capacity => _chars.Length;
+
+    public void EnsureCapacity(int capacity)
+    {
+        // This is not expected to be called this with negative capacity
+        Debug.Assert(capacity >= 0);
+
+        // If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
+        if ((uint)capacity > (uint)_chars.Length)
+            Grow(capacity - _pos);
+    }
+
+    /// <summary>
+    /// Get a pinnable reference to the builder.
+    /// Does not ensure there is a null char after <see cref="Length"/>
+    /// This overload is pattern matched in the C# 7.3+ compiler so you can omit
+    /// the explicit method call, and write eg "fixed (char* c = builder)"
+    /// </summary>
+    public ref char GetPinnableReference()
+    {
+        return ref MemoryMarshal.GetReference(_chars);
+    }
+
+    /// <summary>
+    /// Get a pinnable reference to the builder.
+    /// </summary>
+    /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
+    public ref char GetPinnableReference(bool terminate)
+    {
+        if (terminate)
+        {
+            EnsureCapacity(Length + 1);
+            _chars[Length] = '\0';
+        }
+        return ref MemoryMarshal.GetReference(_chars);
+    }
+
+    public ref char this[int index]
+    {
+        get
+        {
+            Debug.Assert(index < _pos);
+            return ref _chars[index];
+        }
+    }
+
+    public override string ToString()
+    {
+        string s = _chars.Slice(0, _pos).ToString();
+        Dispose();
+        return s;
+    }
+
+    /// <summary>Returns the underlying storage of the builder.</summary>
+    public Span<char> RawChars => _chars;
+
+    /// <summary>
+    /// Returns a span around the contents of the builder.
+    /// </summary>
+    /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
+    public ReadOnlySpan<char> AsSpan(bool terminate)
+    {
+        if (terminate)
+        {
+            EnsureCapacity(Length + 1);
+            _chars[Length] = '\0';
+        }
+        return _chars.Slice(0, _pos);
+    }
+
+    public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
+    public ReadOnlySpan<char> AsSpan(int start) => _chars.Slice(start, _pos - start);
+    public ReadOnlySpan<char> AsSpan(int start, int length) => _chars.Slice(start, length);
+
+    public bool TryCopyTo(Span<char> destination, out int charsWritten)
+    {
+        if (_chars.Slice(0, _pos).TryCopyTo(destination))
+        {
+            charsWritten = _pos;
+            Dispose();
+            return true;
+        }
+        else
+        {
+            charsWritten = 0;
+            Dispose();
+            return false;
+        }
+    }
+
+    public void Insert(int index, char value, int count)
+    {
+        if (_pos > _chars.Length - count)
+        {
+            Grow(count);
+        }
+
+        int remaining = _pos - index;
+        _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+        _chars.Slice(index, count).Fill(value);
+        _pos += count;
+    }
+
+    public void Insert(int index, string? s)
+    {
+        if (s == null)
+        {
+            return;
+        }
+
+        int count = s.Length;
+
+        if (_pos > (_chars.Length - count))
+        {
+            Grow(count);
+        }
+
+        int remaining = _pos - index;
+        _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
+        s
+#if !NETCOREAPP
+            .AsSpan()
+#endif
+            .CopyTo(_chars.Slice(index));
+        _pos += count;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Append(char c)
+    {
+        int pos = _pos;
+        Span<char> chars = _chars;
+        if ((uint)pos < (uint)chars.Length)
+        {
+            chars[pos] = c;
+            _pos = pos + 1;
+        }
+        else
+        {
+            GrowAndAppend(c);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Append(string? s)
+    {
+        if (s == null)
+        {
+            return;
+        }
+
+        int pos = _pos;
+        if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
+        {
+            _chars[pos] = s[0];
+            _pos = pos + 1;
+        }
+        else
+        {
+            AppendSlow(s);
+        }
+    }
+
+    private void AppendSlow(string s)
+    {
+        int pos = _pos;
+        if (pos > _chars.Length - s.Length)
+        {
+            Grow(s.Length);
+        }
+
+        s
+#if !NETCOREAPP
+            .AsSpan()
+#endif
+            .CopyTo(_chars.Slice(pos));
+        _pos += s.Length;
+    }
+
+    public void Append(char c, int count)
+    {
+        if (_pos > _chars.Length - count)
+        {
+            Grow(count);
+        }
+
+        Span<char> dst = _chars.Slice(_pos, count);
+        for (int i = 0; i < dst.Length; i++)
+        {
+            dst[i] = c;
+        }
+        _pos += count;
+    }
+
+    public unsafe void Append(char* value, int length)
+    {
+        int pos = _pos;
+        if (pos > _chars.Length - length)
+        {
+            Grow(length);
+        }
+
+        Span<char> dst = _chars.Slice(_pos, length);
+        for (int i = 0; i < dst.Length; i++)
+        {
+            dst[i] = *value++;
+        }
+        _pos += length;
+    }
+
+    public void Append(scoped ReadOnlySpan<char> value)
+    {
+        int pos = _pos;
+        if (pos > _chars.Length - value.Length)
+        {
+            Grow(value.Length);
+        }
+
+        value.CopyTo(_chars.Slice(_pos));
+        _pos += value.Length;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public Span<char> AppendSpan(int length)
+    {
+        int origPos = _pos;
+        if (origPos > _chars.Length - length)
+        {
+            Grow(length);
+        }
+
+        _pos = origPos + length;
+        return _chars.Slice(origPos, length);
+    }
+
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private void GrowAndAppend(char c)
+    {
+        Grow(1);
+        Append(c);
+    }
+
+    /// <summary>
+    /// Resize the internal buffer either by doubling current buffer size or
+    /// by adding <paramref name="additionalCapacityBeyondPos"/> to
+    /// <see cref="_pos"/> whichever is greater.
+    /// </summary>
+    /// <param name="additionalCapacityBeyondPos">
+    /// Number of chars requested beyond current position.
+    /// </param>
+    [MethodImpl(MethodImplOptions.NoInlining)]
+    private void Grow(int additionalCapacityBeyondPos)
+    {
+        Debug.Assert(additionalCapacityBeyondPos > 0);
+        Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
+
+        const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
+
+        // Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
+        // to double the size if possible, bounding the doubling to not go beyond the max array length.
+        int newCapacity = (int)Math.Max(
+            (uint)(_pos + additionalCapacityBeyondPos),
+            Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
+
+        // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
+        // This could also go negative if the actual required length wraps around.
+        char[] poolArray = ArrayPool<char>.Shared.Rent(newCapacity);
+
+        _chars.Slice(0, _pos).CopyTo(poolArray);
+
+        char[]? toReturn = _arrayToReturnToPool;
+        _chars = _arrayToReturnToPool = poolArray;
+        if (toReturn != null)
+        {
+            ArrayPool<char>.Shared.Return(toReturn);
+        }
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void Dispose()
+    {
+        char[]? toReturn = _arrayToReturnToPool;
+        this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
+        if (toReturn != null)
+        {
+            ArrayPool<char>.Shared.Return(toReturn);
+        }
+    }
+}

+ 16 - 19
Jint/Runtime/CallStack/JintCallStack.cs

@@ -1,11 +1,11 @@
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.Linq;
 using System.Text;
 using Esprima;
 using Esprima.Ast;
 using Jint.Collections;
 using Jint.Native.Function;
-using Jint.Pooling;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interpreter.Expressions;
 
@@ -119,19 +119,17 @@ namespace Jint.Runtime.CallStack
         internal string BuildCallStackString(Location location, int excludeTop = 0)
         {
             static void AppendLocation(
-                StringBuilder sb,
+                ref ValueStringBuilder sb,
                 string shortDescription,
                 in Location loc,
                 in CallStackElement? element)
             {
-                sb
-                    .Append("   at");
+                sb.Append("   at");
 
                 if (!string.IsNullOrWhiteSpace(shortDescription))
                 {
-                    sb
-                        .Append(' ')
-                        .Append(shortDescription);
+                    sb.Append(' ');
+                    sb.Append(shortDescription);
                 }
 
                 if (element?.Arguments is not null)
@@ -151,24 +149,23 @@ namespace Jint.Runtime.CallStack
                     sb.Append(')');
                 }
 
-                sb
-                    .Append(' ')
-                    .Append(loc.Source)
-                    .Append(':')
-                    .Append(loc.End.Line)
-                    .Append(':')
-                    .Append(loc.Start.Column + 1) // report column number instead of index
-                    .AppendLine();
+                sb.Append(' ');
+                sb.Append(loc.Source);
+                sb.Append(':');
+                sb.Append(loc.End.Line.ToString(CultureInfo.InvariantCulture));
+                sb.Append(':');
+                sb.Append((loc.Start.Column + 1).ToString(CultureInfo.InvariantCulture)); // report column number instead of index
+                sb.Append(Environment.NewLine);
             }
 
-            using var sb = StringBuilderPool.Rent();
+            var builder = new ValueStringBuilder();
 
             // stack is one frame behind function-wise when we start to process it from expression level
             var index = _stack._size - 1 - excludeTop;
             var element = index >= 0 ? _stack[index] : (CallStackElement?) null;
             var shortDescription = element?.ToString() ?? "";
 
-            AppendLocation(sb.Builder, shortDescription, location, element);
+            AppendLocation(ref builder, shortDescription, location, element);
 
             location = element?.Location ?? default;
             index--;
@@ -178,13 +175,13 @@ namespace Jint.Runtime.CallStack
                 element = index >= 0 ? _stack[index] : null;
                 shortDescription = element?.ToString() ?? "";
 
-                AppendLocation(sb.Builder, shortDescription, location, element);
+                AppendLocation(ref builder, shortDescription, location, element);
 
                 location = element?.Location ?? default;
                 index--;
             }
 
-            return sb.ToString().TrimEnd();
+            return builder.ToString().TrimEnd();
         }
 
         /// <summary>

+ 4 - 4
Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs

@@ -1,6 +1,6 @@
+using System.Text;
 using Esprima.Ast;
 using Jint.Native;
-using Jint.Pooling;
 
 namespace Jint.Runtime.Interpreter.Expressions;
 
@@ -40,16 +40,16 @@ internal sealed class JintTemplateLiteralExpression : JintExpression
             _initialized = true;
         }
 
-        using var sb = StringBuilderPool.Rent();
+        var sb = new ValueStringBuilder();
         ref readonly var elements = ref _templateLiteralExpression.Quasis;
         for (var i = 0; i < elements.Count; i++)
         {
             var quasi = elements[i];
-            sb.Builder.Append(quasi.Value.Cooked);
+            sb.Append(quasi.Value.Cooked);
             if (i < _expressions.Length)
             {
                 var value = _expressions[i].GetValue(context);
-                sb.Builder.Append(TypeConverter.ToString(value));
+                sb.Append(TypeConverter.ToString(value));
             }
         }
 

+ 3 - 4
Jint/Runtime/JavaScriptException.cs

@@ -1,8 +1,8 @@
+using System.Text;
 using Esprima;
 using Jint.Native;
 using Jint.Native.Error;
 using Jint.Native.Object;
-using Jint.Pooling;
 using Jint.Runtime.Descriptors;
 
 namespace Jint.Runtime;
@@ -132,8 +132,7 @@ public class JavaScriptException : JintException
 
         public override string ToString()
         {
-            using var rent = StringBuilderPool.Rent();
-            var sb = rent.Builder;
+            var sb = new ValueStringBuilder();
 
             sb.Append("Error");
             var message = Message;
@@ -150,7 +149,7 @@ public class JavaScriptException : JintException
                 sb.Append(stackTrace);
             }
 
-            return rent.ToString();
+            return sb.ToString();
         }
     }
 }

+ 4 - 4
Jint/Runtime/TypeConverter.cs

@@ -2,6 +2,7 @@ using System.Globalization;
 using System.Numerics;
 using System.Reflection;
 using System.Runtime.CompilerServices;
+using System.Text;
 using Esprima;
 using Esprima.Ast;
 using Jint.Extensions;
@@ -11,7 +12,6 @@ using Jint.Native.Number.Dtoa;
 using Jint.Native.Object;
 using Jint.Native.String;
 using Jint.Native.Symbol;
-using Jint.Pooling;
 using Jint.Runtime.Interop;
 
 namespace Jint.Runtime
@@ -896,10 +896,10 @@ namespace Jint.Runtime
                 return ToString((long) d);
             }
 
-            using var stringBuilder = StringBuilderPool.Rent();
+            var stringBuilder = new ValueStringBuilder(stackalloc char[128]);
             // we can create smaller array as we know the format to be short
-            NumberPrototype.NumberToString(d, CreateDtoaBuilderForDouble(), stringBuilder.Builder);
-            return stringBuilder.Builder.ToString();
+            NumberPrototype.NumberToString(d, CreateDtoaBuilderForDouble(), ref stringBuilder);
+            return stringBuilder.ToString();
         }
 
         /// <summary>