Skip to content

Commit

Permalink
Optimize JSON serialization (#1512)
Browse files Browse the repository at this point in the history
* optimize ObjectInstance.GetOwnPropertyKeys for non-sorting case
* increase StringBuilderPool max capacity check
* smarter checks for integer numbers in JsonParser
  • Loading branch information
lahma authored Mar 26, 2023
1 parent ef89768 commit abc3646
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 88 deletions.
2 changes: 1 addition & 1 deletion Jint/Native/Function/FunctionInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnPro
}
}

internal override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
internal sealed override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
{
if (_length != null)
{
Expand Down
28 changes: 21 additions & 7 deletions Jint/Native/Json/JsonParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ private string ScanPunctuatorValue(int start, char code)
private Token ScanNumericLiteral(ref State state)
{
var sb = state.TokenBuffer;
int start = _index;
char ch = _source.CharCodeAt(_index);
var start = _index;
var ch = _source.CharCodeAt(_index);
var canBeInteger = true;

// Number start with a -
if (ch == '-')
Expand All @@ -186,14 +187,15 @@ private Token ScanNumericLiteral(ref State state)

if (ch != '.')
{
char firstCharacter = ch;
var firstCharacter = ch;
sb.Append(ch);
ch = _source.CharCodeAt(++_index);

// Hex number starts with '0x'.
// Octal number starts with '0'.
if (sb.Length == 1 && firstCharacter == '0')
{
canBeInteger = false;
// decimal number starts with '0' such as '09' is illegal.
if (ch > 0 && IsDecimalDigit(ch))
{
Expand All @@ -210,6 +212,7 @@ private Token ScanNumericLiteral(ref State state)

if (ch == '.')
{
canBeInteger = false;
sb.Append(ch);
_index++;

Expand All @@ -220,11 +223,12 @@ private Token ScanNumericLiteral(ref State state)
}
}

if (ch == 'e' || ch == 'E')
if (ch is 'e' or 'E')
{
canBeInteger = false;
sb.Append(ch);
ch = _source.CharCodeAt(++_index);
if (ch == '+' || ch == '-')
if (ch is '+' or '-')
{
sb.Append(ch);
ch = _source.CharCodeAt(++_index);
Expand All @@ -243,9 +247,19 @@ private Token ScanNumericLiteral(ref State state)
}
}

string number = sb.ToString();
var number = sb.ToString();
sb.Clear();
JsNumber value = new JsNumber(double.Parse(number, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture));

JsNumber value;
if (canBeInteger && long.TryParse(number, out var longResult) && longResult != -0)
{
value = JsNumber.Create(longResult);
}
else
{
value = new JsNumber(double.Parse(number, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture));
}

return CreateToken(Tokens.Number, number, '\0', value, new TextRange(start, _index));
}

Expand Down
106 changes: 51 additions & 55 deletions Jint/Native/Json/JsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,16 @@ 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));

SerializeTarget target = new SerializeTarget();
try
{
if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined)
{
return JsValue.Undefined;
}
return new JsString(target.Json.ToString());
}
finally
using var jsonBuilder = StringBuilderPool.Rent();
using var numberBuilder = StringBuilderPool.Rent();

var target = new SerializerState(jsonBuilder.Builder, numberBuilder.Builder);
if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined)
{
target.Return();
return JsValue.Undefined;
}

return new JsString(target.Json.ToString());
}

private void SetupReplacer(JsValue replacer)
Expand Down Expand Up @@ -157,21 +154,19 @@ private static string BuildSpacingGap(JsValue space)
/// <summary>
/// https://tc39.es/ecma262/#sec-serializejsonproperty
/// </summary>
private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref SerializeTarget target)
private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref SerializerState target)
{
var value = ReadUnwrappedValue(key, holder);

if (ReferenceEquals(value, JsValue.Null))
{
target.Json.Append(JsString.NullString.ToString());
target.Json.Append(JsString.NullString);
return SerializeResult.NotUndefined;
}

if (value.IsBoolean())
{
target.Json.Append(((JsBoolean) value)._value
? JsString.TrueString.ToString()
: JsString.FalseString.ToString());
target.Json.Append(((JsBoolean) value)._value ? "true" : "false");
return SerializeResult.NotUndefined;
}

Expand All @@ -183,13 +178,20 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S

if (value.IsNumber())
{
var doubleValue = TypeConverter.ToNumber(value);
var doubleValue = ((JsNumber) value)._value;

if (value.IsInteger())
{
target.Json.Append((long) doubleValue);
return SerializeResult.NotUndefined;
}

var isFinite = !double.IsNaN(doubleValue) && !double.IsInfinity(doubleValue);
if (isFinite)
{
if (TypeConverter.CanBeStringifiedAsLong(doubleValue))
{
target.Json.Append(TypeConverter.ToString((long) doubleValue));
target.Json.Append((long) doubleValue);
return SerializeResult.NotUndefined;
}

Expand Down Expand Up @@ -234,6 +236,12 @@ private SerializeResult SerializeJSONProperty(JsValue key, JsValue holder, ref S
private JsValue ReadUnwrappedValue(JsValue key, JsValue holder)
{
var value = holder.Get(key);

if (value._type <= InternalTypes.Integer && _replacerFunction.IsUndefined())
{
return value;
}

var isBigInt = value is BigIntInstance || value.IsBigInt();
if (value.IsObject() || isBigInt)
{
Expand Down Expand Up @@ -295,17 +303,16 @@ private static bool CanSerializesAsArray(ObjectInstance value)
/// <summary>
/// https://tc39.es/ecma262/#sec-quotejsonstring
/// </summary>
private static void QuoteJSONString(string value, ref SerializeTarget target)
private static void QuoteJSONString(string value, ref SerializerState target)
{
int len = value.Length;
if (len == 0)
if (value.Length == 0)
{
target.Json.Append("\"\"");
return;
}

target.Json.Append('"');
for (var i = 0; i < len; i++)
for (var i = 0; i < value.Length; i++)
{
var c = value[i];
switch (c)
Expand Down Expand Up @@ -358,7 +365,7 @@ private static void QuoteJSONString(string value, ref SerializeTarget target)
/// <summary>
/// https://tc39.es/ecma262/#sec-serializejsonarray
/// </summary>
private void SerializeJSONArray(ObjectInstance value, ref SerializeTarget target)
private void SerializeJSONArray(ObjectInstance value, ref SerializerState target)
{
var len = TypeConverter.ToUint32(value.Get(CommonProperties.Length));
if (len == 0)
Expand Down Expand Up @@ -424,7 +431,7 @@ private void SerializeJSONArray(ObjectInstance value, ref SerializeTarget target
/// <summary>
/// https://tc39.es/ecma262/#sec-serializejsonobject
/// </summary>
private void SerializeJSONObject(ObjectInstance value, ref SerializeTarget target)
private void SerializeJSONObject(ObjectInstance value, ref SerializerState target)
{
var enumeration = _propertyList is null
? PropertyEnumeration.FromObjectInstance(value)
Expand All @@ -444,9 +451,10 @@ private void SerializeJSONObject(ObjectInstance value, ref SerializeTarget targe

const string separator = ",";

bool hasPrevious = false;
foreach (var p in enumeration.Keys)
var hasPrevious = false;
for (var i = 0; i < enumeration.Keys.Count; i++)
{
var p = enumeration.Keys[i];
int position = target.Json.Length;

if (hasPrevious)
Expand Down Expand Up @@ -500,30 +508,17 @@ private void SerializeJSONObject(ObjectInstance value, ref SerializeTarget targe
_indent = stepback;
}

private readonly ref struct SerializeTarget
private readonly ref struct SerializerState
{
private readonly StringBuilderPool.BuilderWrapper _jsonBuilder;
private readonly StringBuilderPool.BuilderWrapper _numberBuilder;

public SerializeTarget()
public SerializerState(StringBuilder jsonBuilder, StringBuilder numberBuffer)
{
_jsonBuilder = StringBuilderPool.Rent();
_numberBuilder = StringBuilderPool.Rent();
Json = _jsonBuilder.Builder;
NumberBuffer = _numberBuilder.Builder;
Json = jsonBuilder;
NumberBuffer = numberBuffer;
}

public StringBuilder Json { get; }

public StringBuilder NumberBuffer { get; }

public DtoaBuilder DtoaBuilder { get; } = TypeConverter.CreateDtoaBuilderForDouble();

public void Return()
{
_jsonBuilder.Dispose();
_numberBuilder.Dispose();
}
public readonly StringBuilder Json;
public readonly StringBuilder NumberBuffer;
public readonly DtoaBuilder DtoaBuilder = TypeConverter.CreateDtoaBuilderForDouble();
}

private enum SerializeResult
Expand All @@ -534,7 +529,7 @@ private enum SerializeResult

private readonly struct PropertyEnumeration
{
private PropertyEnumeration(IEnumerable<JsValue> keys, bool isEmpty)
private PropertyEnumeration(List<JsValue> keys, bool isEmpty)
{
Keys = keys;
IsEmpty = isEmpty;
Expand All @@ -545,26 +540,27 @@ public static PropertyEnumeration FromList(List<JsValue> keys)

public static PropertyEnumeration FromObjectInstance(ObjectInstance instance)
{
List<JsValue> allKeys = instance.GetOwnPropertyKeys(Types.String);
var allKeys = instance.GetOwnPropertyKeys(Types.String);
RemoveUnserializableProperties(instance, allKeys);
return new PropertyEnumeration(allKeys, allKeys.Count == 0);
}

private static void RemoveUnserializableProperties(ObjectInstance instance, List<JsValue> keys)
{
keys.RemoveAll(key =>
for (var i = 0; i < keys.Count; i++)
{
if (!key.IsString())
var key = keys[i];
var desc = instance.GetOwnProperty(key);
if (desc == PropertyDescriptor.Undefined || !desc.Enumerable)
{
return true;
keys.RemoveAt(i);
i--;
}

var desc = instance.GetOwnProperty(key);
return desc == PropertyDescriptor.Undefined || !desc.Enumerable;
});
}
}

public IEnumerable<JsValue> Keys { get; }
public List<JsValue> Keys { get; }

public bool IsEmpty { get; }
}
Expand Down
Loading

0 comments on commit abc3646

Please sign in to comment.