浏览代码

Optimize JSON serialization (#1512)

* optimize ObjectInstance.GetOwnPropertyKeys for non-sorting case
* increase StringBuilderPool max capacity check
* smarter checks for integer numbers in JsonParser
Marko Lahma 2 年之前
父节点
当前提交
abc36466d7

+ 1 - 1
Jint/Native/Function/FunctionInstance.cs

@@ -104,7 +104,7 @@ namespace Jint.Native.Function
             }
         }
 
-        internal override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
+        internal sealed override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
         {
             if (_length != null)
             {

+ 21 - 7
Jint/Native/Json/JsonParser.cs

@@ -174,8 +174,9 @@ namespace Jint.Native.Json
         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 == '-')
@@ -186,7 +187,7 @@ namespace Jint.Native.Json
 
             if (ch != '.')
             {
-                char firstCharacter = ch;
+                var firstCharacter = ch;
                 sb.Append(ch);
                 ch = _source.CharCodeAt(++_index);
 
@@ -194,6 +195,7 @@ namespace Jint.Native.Json
                 // 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))
                     {
@@ -210,6 +212,7 @@ namespace Jint.Native.Json
 
             if (ch == '.')
             {
+                canBeInteger = false;
                 sb.Append(ch);
                 _index++;
 
@@ -220,11 +223,12 @@ namespace Jint.Native.Json
                 }
             }
 
-            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);
@@ -243,9 +247,19 @@ namespace Jint.Native.Json
                 }
             }
 
-            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));
         }
 

+ 51 - 55
Jint/Native/Json/JsonSerializer.cs

@@ -53,19 +53,16 @@ namespace Jint.Native.Json
             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)
@@ -157,21 +154,19 @@ namespace Jint.Native.Json
         /// <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;
             }
 
@@ -183,13 +178,20 @@ namespace Jint.Native.Json
 
             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;
                     }
 
@@ -234,6 +236,12 @@ namespace Jint.Native.Json
         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)
             {
@@ -295,17 +303,16 @@ namespace Jint.Native.Json
         /// <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)
@@ -358,7 +365,7 @@ namespace Jint.Native.Json
         /// <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)
@@ -424,7 +431,7 @@ namespace Jint.Native.Json
         /// <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)
@@ -444,9 +451,10 @@ namespace Jint.Native.Json
 
             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)
@@ -500,30 +508,17 @@ namespace Jint.Native.Json
             _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
@@ -534,7 +529,7 @@ namespace Jint.Native.Json
 
         private readonly struct PropertyEnumeration
         {
-            private PropertyEnumeration(IEnumerable<JsValue> keys, bool isEmpty)
+            private PropertyEnumeration(List<JsValue> keys, bool isEmpty)
             {
                 Keys = keys;
                 IsEmpty = isEmpty;
@@ -545,26 +540,27 @@ namespace Jint.Native.Json
 
             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; }
         }

+ 59 - 15
Jint/Native/Object/ObjectInstance.cs

@@ -225,16 +225,66 @@ namespace Jint.Native.Object
         {
             EnsureInitialized();
 
+            var returningSymbols = (types & Types.Symbol) != 0 && _symbols?.Count > 0;
+            var returningStringKeys = (types & Types.String) != 0 && _properties?.Count > 0;
+
             var propertyKeys = new List<JsValue>();
             if ((types & Types.String) != 0)
             {
-                propertyKeys.AddRange(GetInitialOwnStringPropertyKeys());
+                var initialOwnStringPropertyKeys = GetInitialOwnStringPropertyKeys();
+                if (!ReferenceEquals(initialOwnStringPropertyKeys, System.Linq.Enumerable.Empty<JsValue>()))
+                {
+                    propertyKeys.AddRange(initialOwnStringPropertyKeys);
+                }
+            }
+
+            // check fast case where we don't need to sort, which should be the common case
+            if (!returningSymbols)
+            {
+                if (!returningStringKeys)
+                {
+                    return propertyKeys;
+                }
+
+                var propertyKeyCount = propertyKeys.Count;
+                propertyKeys.Capacity += _properties!.Count;
+                foreach (var pair in _properties)
+                {
+                    // check if we can rely on the property name not being an unsigned number
+                    var c = pair.Key.Name.Length > 0 ? pair.Key.Name[0] : 'a';
+                    if (char.IsDigit(c) && propertyKeyCount + _properties.Count > 1)
+                    {
+                        // jump to slow path, return list to original state
+                        propertyKeys.RemoveRange(propertyKeyCount, propertyKeys.Count - propertyKeyCount);
+                        return GetOwnPropertyKeysSorted(propertyKeys, returningStringKeys, returningSymbols);
+                    }
+                    propertyKeys.Add(new JsString(pair.Key.Name));
+                }
+
+                // seems good
+                return propertyKeys;
+            }
+
+            if ((types & Types.String) == 0 && (types & Types.Symbol) != 0)
+            {
+                // only symbols requested
+                if (_symbols != null)
+                {
+                    foreach (var pair in _symbols!)
+                    {
+                        propertyKeys.Add(pair.Key);
+                    }
+                }
+                return propertyKeys;
             }
 
-            var keys = new List<JsValue>(_properties?.Count ?? 0 + _symbols?.Count ?? 0 + propertyKeys.Count);
-            List<JsValue>? symbolKeys = null;
+            return GetOwnPropertyKeysSorted(propertyKeys, returningStringKeys, returningSymbols);
+        }
 
-            if ((types & Types.String) != 0 && _properties != null)
+        private List<JsValue> GetOwnPropertyKeysSorted(List<JsValue> initialOwnPropertyKeys, bool returningStringKeys, bool returningSymbols)
+        {
+            var keys = new List<JsValue>(_properties?.Count ?? 0 + _symbols?.Count ?? 0 + initialOwnPropertyKeys.Count);
+            if (returningStringKeys && _properties != null)
             {
                 foreach (var pair in _properties)
                 {
@@ -247,28 +297,22 @@ namespace Jint.Native.Object
                     }
                     else
                     {
-                        propertyKeys.Add(new JsString(propertyName));
+                        initialOwnPropertyKeys.Add(new JsString(propertyName));
                     }
                 }
             }
 
             keys.Sort((v1, v2) => TypeConverter.ToNumber(v1).CompareTo(TypeConverter.ToNumber(v2)));
-            keys.AddRange(propertyKeys);
+            keys.AddRange(initialOwnPropertyKeys);
 
-            if ((types & Types.Symbol) != 0 && _symbols != null)
+            if (returningSymbols)
             {
-                foreach (var pair in _symbols)
+                foreach (var pair in _symbols!)
                 {
-                    symbolKeys ??= new List<JsValue>();
-                    symbolKeys.Add(pair.Key);
+                    keys.Add(pair.Key);
                 }
             }
 
-            if (symbolKeys != null)
-            {
-                keys.AddRange(symbolKeys);
-            }
-
             return keys;
         }
 

+ 7 - 7
Jint/Native/String/StringInstance.cs

@@ -33,7 +33,7 @@ internal class StringInstance : ObjectInstance, IPrimitiveInstance
         return false;
     }
 
-    public override PropertyDescriptor GetOwnProperty(JsValue property)
+    public sealed override PropertyDescriptor GetOwnProperty(JsValue property)
     {
         if (property == CommonProperties.Infinity)
         {
@@ -66,7 +66,7 @@ internal class StringInstance : ObjectInstance, IPrimitiveInstance
         return new PropertyDescriptor(str[index], PropertyFlag.OnlyEnumerable);
     }
 
-    public override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()
+    public sealed override IEnumerable<KeyValuePair<JsValue, PropertyDescriptor>> GetOwnProperties()
     {
         foreach (var entry in base.GetOwnProperties())
         {
@@ -79,12 +79,12 @@ internal class StringInstance : ObjectInstance, IPrimitiveInstance
         }
     }
 
-    internal override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
+    internal sealed override IEnumerable<JsValue> GetInitialOwnStringPropertyKeys()
     {
-        return new[] { JsString.LengthString };
+        yield return JsString.LengthString;
     }
 
-    public override List<JsValue> GetOwnPropertyKeys(Types types)
+    public sealed override List<JsValue> GetOwnPropertyKeys(Types types)
     {
         var keys = new List<JsValue>(StringData.Length + 1);
         if ((types & Types.String) != 0)
@@ -105,7 +105,7 @@ internal class StringInstance : ObjectInstance, IPrimitiveInstance
         return keys;
     }
 
-    protected internal override void SetOwnProperty(JsValue property, PropertyDescriptor desc)
+    protected internal sealed override void SetOwnProperty(JsValue property, PropertyDescriptor desc)
     {
         if (property == CommonProperties.Length)
         {
@@ -117,7 +117,7 @@ internal class StringInstance : ObjectInstance, IPrimitiveInstance
         }
     }
 
-    public override void RemoveOwnProperty(JsValue property)
+    public sealed override void RemoveOwnProperty(JsValue property)
     {
         if (property == CommonProperties.Length)
         {

+ 1 - 1
Jint/Pooling/StringBuilderPool.cs

@@ -47,7 +47,7 @@ namespace Jint.Pooling
                 var builder = Builder;
 
                 // do not store builders that are too large.
-                if (builder.Capacity <= 1024)
+                if (builder.Capacity <= 1024 * 1024)
                 {
                     builder.Clear();
                     _pool.Free(builder);

+ 1 - 1
Jint/Runtime/Interop/Reflection/IndexerAccessor.cs

@@ -170,7 +170,7 @@ namespace Jint.Runtime.Interop.Reflection
                 }
             }
 
-            return new ReflectionDescriptor(engine, this, target, false);
+            return new ReflectionDescriptor(engine, this, target, enumerable: true);
         }
     }
 }

+ 0 - 1
Jint/Runtime/Interpreter/Statements/JintExportDefaultDeclaration.cs

@@ -1,7 +1,6 @@
 using Esprima.Ast;
 using Jint.Native;
 using Jint.Native.Function;
-using Jint.Native.Object;
 using Jint.Runtime.Environments;
 using Jint.Runtime.Interpreter.Expressions;