Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize some character checks and ValueStringBuilder #1986

Merged
merged 1 commit into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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