diff --git a/Jint/Native/Array/ArrayPrototype.cs b/Jint/Native/Array/ArrayPrototype.cs index f1e16039d6..057f0d7575 100644 --- a/Jint/Native/Array/ArrayPrototype.cs +++ b/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 @@ static string StringFromJsValue(JsValue value) 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(); diff --git a/Jint/Native/BigInt/BigIntPrototype.cs b/Jint/Native/BigInt/BigIntPrototype.cs index 86875afaed..e953d7e0d9 100644 --- a/Jint/Native/BigInt/BigIntPrototype.cs +++ b/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 @@ private JsValue ToBigIntString(JsValue thisObject, JsValue[] arguments) 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; } /// diff --git a/Jint/Native/Json/JsonParser.cs b/Jint/Native/Json/JsonParser.cs index d1173854e0..20cb3a8963 100644 --- a/Jint/Native/Json/JsonParser.cs +++ b/Jint/Native/Json/JsonParser.cs @@ -5,8 +5,6 @@ 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 @@ private string ScanPunctuatorValue(int start, char code) 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 @@ private Token ScanNumericLiteral(ref State state) } 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 @@ private Token ScanStringLiteral(ref State state) 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 @@ private Token ScanStringLiteral(ref State state) } string value = sb.ToString(); - sb.Clear(); return CreateToken(Tokens.String, value, '\"', new JsString(value), new TextRange(start, _index)); } @@ -704,8 +700,7 @@ public JsValue Parse(string code, ParserOptions? options) _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 @@ public JsValue Parse(string code, ParserOptions? options) return jsv; } + [StructLayout(LayoutKind.Auto)] private ref struct State { - public State(StringBuilder tokenBuffer) - { - TokenBuffer = tokenBuffer; - CurrentDepth = 0; - } - - /// - /// 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. - /// - public StringBuilder TokenBuffer { get; } - /// /// The current recursion depth /// diff --git a/Jint/Native/Json/JsonSerializer.cs b/Jint/Native/Json/JsonSerializer.cs index c13da3cb97..2b61208f1f 100644 --- a/Jint/Native/Json/JsonSerializer.cs +++ b/Jint/Native/Json/JsonSerializer.cs @@ -9,7 +9,6 @@ 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 @@ public JsValue Serialize(JsValue value, JsValue replacer, JsValue space) 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 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S } 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; } diff --git a/Jint/Native/Number/NumberPrototype.cs b/Jint/Native/Number/NumberPrototype.cs index 186be11282..08e8a81947 100644 --- a/Jint/Native/Number/NumberPrototype.cs +++ b/Jint/Native/Number/NumberPrototype.cs @@ -4,7 +4,6 @@ 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 @@ private JsValue ToPrecision(JsValue thisObject, JsValue[] arguments) 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 @@ private static string CreateExponentialRepresentation( 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 @@ internal static string ToBase(long n, int radix) 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 @@ internal static string ToFractionBase(double n, int radix) 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 @@ internal static string ToFractionBase(double n, int radix) 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 @@ internal static void NumberToString( 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 @@ internal static void NumberToString( 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 @@ internal static void NumberToString( exponent = -exponent; } - stringBuilder.Append(exponent); + stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture)); } } } diff --git a/Jint/Native/RegExp/RegExpPrototype.cs b/Jint/Native/RegExp/RegExpPrototype.cs index 20f989a52b..1ef594a19b 100644 --- a/Jint/Native/RegExp/RegExpPrototype.cs +++ b/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 @@ internal static string GetSubstitution( // $` 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 @@ internal static string GetSubstitution( 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 @@ internal static string GetSubstitution( } } - return replacementBuilder.ToString(); + return sb.ToString(); } /// diff --git a/Jint/Native/String/StringConstructor.cs b/Jint/Native/String/StringConstructor.cs index fbb971fe55..de2304233d 100644 --- a/Jint/Native/String/StringConstructor.cs +++ b/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 @@ private static JsValue FromCharCode(JsValue? thisObj, JsValue[] arguments) 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 @@ private JsValue Raw(JsValue thisObject, JsValue[] arguments) 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(); diff --git a/Jint/Native/String/StringPrototype.cs b/Jint/Native/String/StringPrototype.cs index 1cf2201bef..221637c1c4 100644 --- a/Jint/Native/String/StringPrototype.cs +++ b/Jint/Native/String/StringPrototype.cs @@ -9,7 +9,6 @@ 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 @@ static int StringIndexOf(string s, string search, int fromIndex) 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 @@ static int StringIndexOf(string s, string search, int fromIndex) 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 @@ static int StringIndexOf(string s, string search, int fromIndex) 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 @@ private JsValue Repeat(JsValue thisObject, JsValue[] arguments) 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 @@ private JsValue ToWellFormed(JsValue thisObject, JsValue[] arguments) 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 @@ private JsValue ToWellFormed(JsValue thisObject, JsValue[] arguments) } else { - result.Append(s, k, cp.CodeUnitCount); + result.Append(s.AsSpan(k, cp.CodeUnitCount)); } k += cp.CodeUnitCount; } diff --git a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs b/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs index dff4978054..02a1609294 100644 --- a/Jint/Native/TypedArray/IntrinsicTypedArrayPrototype.cs +++ b/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.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 @@ static string StringFromJsValue(JsValue value) 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(); } /// diff --git a/Jint/Pooling/StringBuilderPool.cs b/Jint/Pooling/StringBuilderPool.cs deleted file mode 100644 index c5184d7ac0..0000000000 --- a/Jint/Pooling/StringBuilderPool.cs +++ /dev/null @@ -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 -{ - /// - /// Pooling of StringBuilder instances. - /// - internal static class StringBuilderPool - { - private static readonly ConcurrentObjectPool _pool; - - static StringBuilderPool() - { - _pool = new ConcurrentObjectPool(() => 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 _pool; - - public BuilderWrapper(StringBuilder builder, ConcurrentObjectPool 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); - } - } - } - } -} diff --git a/Jint/Pooling/ValueStringBuilder.cs b/Jint/Pooling/ValueStringBuilder.cs new file mode 100644 index 0000000000..d8358f4ee7 --- /dev/null +++ b/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 _chars; + private int _pos; + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.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); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// 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)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + 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; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span 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 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 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 dst = _chars.Slice(_pos, length); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = *value++; + } + _pos += length; + } + + public void Append(scoped ReadOnlySpan 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 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); + } + + /// + /// Resize the internal buffer either by doubling current buffer size or + /// by adding to + /// whichever is greater. + /// + /// + /// Number of chars requested beyond current position. + /// + [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.Shared.Rent(newCapacity); + + _chars.Slice(0, _pos).CopyTo(poolArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.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.Shared.Return(toReturn); + } + } +} diff --git a/Jint/Runtime/CallStack/JintCallStack.cs b/Jint/Runtime/CallStack/JintCallStack.cs index d6a234791d..128b3d31c3 100644 --- a/Jint/Runtime/CallStack/JintCallStack.cs +++ b/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 @@ public override string ToString() 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 @@ static void AppendLocation( 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 @@ static void AppendLocation( 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(); } /// diff --git a/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs b/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs index e3b7688ac4..d891a7f389 100644 --- a/Jint/Runtime/Interpreter/Expressions/JintTemplateLiteralExpression.cs +++ b/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 @@ protected override object EvaluateInternal(EvaluationContext context) _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)); } } diff --git a/Jint/Runtime/JavaScriptException.cs b/Jint/Runtime/JavaScriptException.cs index d285606da4..d79b546062 100644 --- a/Jint/Runtime/JavaScriptException.cs +++ b/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 override string? StackTrace 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 override string ToString() sb.Append(stackTrace); } - return rent.ToString(); + return sb.ToString(); } } } diff --git a/Jint/Runtime/TypeConverter.cs b/Jint/Runtime/TypeConverter.cs index 381f22bf4d..146fe779c5 100644 --- a/Jint/Runtime/TypeConverter.cs +++ b/Jint/Runtime/TypeConverter.cs @@ -2,6 +2,7 @@ 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.Object; using Jint.Native.String; using Jint.Native.Symbol; -using Jint.Pooling; using Jint.Runtime.Interop; namespace Jint.Runtime @@ -896,10 +896,10 @@ internal static string ToString(double d) 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(); } ///