Skip to content

Commit

Permalink
Optimize ValueStringBuilder and some character checks (#1986)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma authored Oct 24, 2024
1 parent 6cb6d5d commit 7381086
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 50 deletions.
12 changes: 7 additions & 5 deletions Jint/Extensions/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ namespace Jint.Extensions;
internal static class Character
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInRange(this char c, ushort min, ushort max)
{
Debug.Assert(min <= max);
return c - (uint) min <= max - (uint) min;
}
public static bool IsInRange(this char c, ushort min, ushort max) => (uint)(c - min) <= (uint)(max - min);

/// <summary>
/// https://tc39.es/ecma262/#ASCII-word-characters
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsAsciiWordCharacter(this char c) => c == '_' || c.IsDecimalDigit() || c.IsInRange('a', 'z') || c.IsInRange('A', 'Z');

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOctalDigit(this char c) => c.IsInRange('0', '7');
Expand Down
64 changes: 37 additions & 27 deletions Jint/Native/Global/GlobalObject.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Buffers;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using Jint.Extensions;
Expand Down Expand Up @@ -274,7 +273,7 @@ private static JsValue IsFinite(JsValue thisObject, JsValue[] arguments)
}

private const string UriReservedString = ";/?:@&=+$,";
private const string UriUnescapedString = "-_.!~*'()";
private const string UriUnescapedString = "-.!~*'()";
private static readonly SearchValues<char> UriUnescaped = SearchValues.Create(UriUnescapedString);
private static readonly SearchValues<char> UnescapedUriSet = SearchValues.Create(UriReservedString + UriUnescapedString + '#');
private static readonly SearchValues<char> ReservedUriSet = SearchValues.Create(UriReservedString + '#');
Expand Down Expand Up @@ -303,20 +302,16 @@ private JsValue EncodeUriComponent(JsValue thisObject, JsValue[] arguments)

private JsValue Encode(string uriString, SearchValues<char> unescapedUriSet)
{
const string HexaMap = "0123456789ABCDEF";

var strLen = uriString.Length;

_stringBuilder.EnsureCapacity(uriString.Length);
_stringBuilder.Clear();
var builder = new ValueStringBuilder(uriString.Length);
Span<byte> buffer = stackalloc byte[4];

for (var k = 0; k < strLen; k++)
{
var c = uriString[k];
if (c is >= 'a' and <= 'z' || c is >= 'A' and <= 'Z' || c is >= '0' and <= '9' || unescapedUriSet.Contains(c))
if (c.IsAsciiWordCharacter() || unescapedUriSet.Contains(c))
{
_stringBuilder.Append(c);
builder.Append(c);
}
else
{
Expand Down Expand Up @@ -386,15 +381,13 @@ private JsValue Encode(string uriString, SearchValues<char> unescapedUriSet)

for (var i = 0; i < length; i++)
{
var octet = buffer[i];
var x1 = HexaMap[octet / 16];
var x2 = HexaMap[octet % 16];
_stringBuilder.Append('%').Append(x1).Append(x2);
builder.Append('%');
builder.AppendHex(buffer[i]);
}
}
}

return _stringBuilder.ToString();
return builder.ToString();

uriError:
_engine.SignalError(ExceptionHelper.CreateUriError(_realm, "URI malformed"));
Expand Down Expand Up @@ -583,10 +576,8 @@ private static bool IsDigit(char c, int radix, out int result)
return tmp < radix;
}

private static readonly SearchValues<char> EscapeAllowList = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@*_ + -./");

/// <summary>
/// http://www.ecma-international.org/ecma-262/5.1/#sec-B.2.1
/// https://tc39.es/ecma262/#sec-escape-string
/// </summary>
private JsValue Escape(JsValue thisObject, JsValue[] arguments)
{
Expand All @@ -600,7 +591,7 @@ private JsValue Escape(JsValue thisObject, JsValue[] arguments)
for (var k = 0; k < strLen; k++)
{
var c = uriString[k];
if (EscapeAllowList.Contains(c))
if (c.IsAsciiWordCharacter() || c == '@' || c == '*' || c == '+' || c == '-' || c == '.' || c == '/')
{
_stringBuilder.Append(c);
}
Expand Down Expand Up @@ -636,26 +627,45 @@ private JsValue Unescape(JsValue thisObject, JsValue[] arguments)
{
if (k <= strLen - 6
&& uriString[k + 1] == 'u'
&& uriString.Skip(k + 2).Take(4).All(IsValidHexaChar))
&& AreValidHexChars(uriString.AsSpan(k + 2, 4)))
{
var joined = string.Join(string.Empty, uriString.Skip(k + 2).Take(4));
c = (char) int.Parse(joined, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);

c = ParseHexString(uriString.AsSpan(k + 2, 4));
k += 5;
}
else if (k <= strLen - 3
&& uriString.Skip(k + 1).Take(2).All(IsValidHexaChar))
else if (k <= strLen - 3 && AreValidHexChars(uriString.AsSpan(k + 1, 2)))
{
var joined = string.Join(string.Empty, uriString.Skip(k + 1).Take(2));
c = (char) int.Parse(joined, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);

c = ParseHexString(uriString.AsSpan(k + 1, 2));
k += 2;
}
}
_stringBuilder.Append(c);
}

return _stringBuilder.ToString();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool AreValidHexChars(ReadOnlySpan<char> input)
{
foreach (var c in input)
{
if (!IsValidHexaChar(c))
{
return false;
}
}

return true;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static char ParseHexString(ReadOnlySpan<char> input)
{
#if NET6_0_OR_GREATER
return (char) int.Parse(input, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
#else
return (char) int.Parse(input.ToString(), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
#endif
}
}

// optimized versions with string parameter and without virtual dispatch for global environment usage
Expand Down
4 changes: 2 additions & 2 deletions Jint/Native/Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref V

if (value.IsInteger())
{
json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture));
json.Append((long) doubleValue);
return SerializeResult.NotUndefined;
}

Expand All @@ -193,7 +193,7 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref V
{
if (TypeConverter.CanBeStringifiedAsLong(doubleValue))
{
json.Append(((long) doubleValue).ToString(CultureInfo.InvariantCulture));
json.Append((long) doubleValue);
return SerializeResult.NotUndefined;
}

Expand Down
4 changes: 2 additions & 2 deletions Jint/Native/Number/NumberPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ private static string CreateExponentialRepresentation(

sb.Append('e');
sb.Append(negativeExponent ? '-' : '+');
sb.Append(exponent.ToString(CultureInfo.InvariantCulture));
sb.Append(exponent);

return sb.ToString();
}
Expand Down Expand Up @@ -528,7 +528,7 @@ internal static string ToNumberString(double m)
exponent = -exponent;
}

stringBuilder.Append(exponent.ToString(CultureInfo.InvariantCulture));
stringBuilder.Append(exponent);
}

return stringBuilder.ToString();
Expand Down
4 changes: 0 additions & 4 deletions Jint/Native/String/StringPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,7 @@ static int StringIndexOf(string s, string search, int fromIndex)

if (endOfLastMatch < thisString.Length)
{
#if NETFRAMEWORK
result.Append(thisString.AsSpan(endOfLastMatch));
#else
result.Append(thisString[endOfLastMatch..]);
#endif
}

return result.ToString();
Expand Down
11 changes: 5 additions & 6 deletions Jint/Native/TypedArray/TypedArrayConstructor.Uint8Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ internal static JsString GetAndValidateAlphabetOption(Engine engine, ObjectInsta

internal readonly record struct FromEncodingResult(byte[] Bytes, JavaScriptException? Error, int Read);

private static readonly SearchValues<char> Base64Alphabet = SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");

internal static FromEncodingResult FromBase64(Engine engine, string input, string alphabet, string lastChunkHandling, uint maxLength = uint.MaxValue)
{
if (maxLength == 0)
Expand Down Expand Up @@ -202,7 +200,10 @@ internal static FromEncodingResult FromBase64(Engine engine, string input, strin
}
}

if (!Base64Alphabet.Contains(currentChar))
if (!currentChar.IsDecimalDigit()
&& !char.ToLowerInvariant(currentChar).IsInRange('a', 'z')
&& currentChar != '+'
&& currentChar != '/')
{
return new FromEncodingResult(bytes.ToArray(), ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid base64 character."), read);
}
Expand Down Expand Up @@ -308,8 +309,6 @@ private JsTypedArray FromHex(JsValue thisObject, JsValue[] arguments)
return ta;
}

private static readonly SearchValues<char> HexAlphabet = SearchValues.Create("0123456789abcdefABCDEF");

internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLength = int.MaxValue)
{
var length = s.Length;
Expand All @@ -325,7 +324,7 @@ internal static FromEncodingResult FromHex(Engine engine, string s, uint maxLeng
while (read < length && byteIndex < maxLength)
{
var hexits = s.AsSpan(read, 2);
if (!HexAlphabet.Contains(hexits[0]) || !HexAlphabet.Contains(hexits[1]))
if (!hexits[0].IsHexDigit() || !hexits[1].IsHexDigit())
{
return new FromEncodingResult(bytes.AsSpan(0, byteIndex).ToArray(), ExceptionHelper.CreateSyntaxError(engine.Realm, "Invalid hex value"), read);
}
Expand Down
39 changes: 39 additions & 0 deletions Jint/Pooling/ValueStringBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ public void Append(char c)
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendHex(byte b)
{
const string Map = "0123456789ABCDEF";
Span<char> data = stackalloc char[]
{
Map[b / 16],
Map[b % 16],
};
Append(data);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? s)
{
Expand All @@ -208,6 +220,33 @@ public void Append(string? s)
}
}

#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append<T>(T value) where T : ISpanFormattable
{
if (value.TryFormat(_chars.Slice(_pos), out var charsWritten, format: default, provider: null))
{
_pos += charsWritten;
}
else
{
Append(value.ToString());
}
}
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(int value)
{
Append(value.ToString(System.Globalization.CultureInfo.InvariantCulture));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(long value)
{
Append(value.ToString(System.Globalization.CultureInfo.InvariantCulture));
}
#endif

private void AppendSlow(string s)
{
int pos = _pos;
Expand Down
5 changes: 2 additions & 3 deletions Jint/Runtime/CallStack/JintCallStack.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using Jint.Collections;
Expand Down Expand Up @@ -151,9 +150,9 @@ static void AppendLocation(
sb.Append(' ');
sb.Append(loc.SourceFile);
sb.Append(':');
sb.Append(loc.End.Line.ToString(CultureInfo.InvariantCulture));
sb.Append(loc.End.Line);
sb.Append(':');
sb.Append((loc.Start.Column + 1).ToString(CultureInfo.InvariantCulture)); // report column number instead of index
sb.Append(loc.Start.Column + 1); // report column number instead of index
sb.Append(System.Environment.NewLine);
}

Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/Interop/DefaultTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ private bool TryConvert(
foreach (var constructor in constructors)
{
var parameterInfos = constructor.GetParameters();
if (parameterInfos.All(static p => p.IsOptional) && constructor.IsPublic)
if (Array.TrueForAll(parameterInfos, static p => p.IsOptional) && constructor.IsPublic)
{
constructorParameters = new object[parameterInfos.Length];
found = true;
Expand Down

0 comments on commit 7381086

Please sign in to comment.