Selaa lähdekoodia

JSON serialization performance improvement (#1527)

* Json stringify performance improvement

* Undo changed ordering of property removal

* Replaced compiler-conditional compilation with casting

* When the keyboard is faster then the brain - restored removed line
tomatosalat0 2 vuotta sitten
vanhempi
commit
8b0da8ad3a

+ 16 - 0
Jint.Tests/Runtime/JsonSerializerTests.cs

@@ -143,5 +143,21 @@ namespace Jint.Tests.Runtime
             string actual = serializer.Serialize(instance, replacer, JsValue.Undefined).ToString();
             Assert.Equal("{\"b\":42}", actual);
         }
+
+        [Theory]
+        [InlineData("test123\n456", "\"test123\\n456\"")]
+        [InlineData("test123456\n", "\"test123456\\n\"")]
+        [InlineData("\u0002test\u0002", "\"\\u0002test\\u0002\"")]
+        [InlineData("\u0002tes\tt\u0002", "\"\\u0002tes\\tt\\u0002\"")]
+        [InlineData("t\u0002est\u0002", "\"t\\u0002est\\u0002\"")]
+        [InlineData("test😀123456\n", "\"test😀123456\\n\"")]
+        public void JsonStringEncodingFormatsContentCorrectly(string inputString, string expectedOutput)
+        {
+            using var engine = new Engine();
+            var serializer = new JsonSerializer(engine);
+
+            string actual = serializer.Serialize(new JsString(inputString)).ToString();
+            Assert.Equal(expectedOutput, actual);
+        }
     }
 }

+ 103 - 61
Jint/Native/Json/JsonSerializer.cs

@@ -54,9 +54,8 @@ namespace Jint.Native.Json
             wrapper.DefineOwnProperty(JsString.Empty, new PropertyDescriptor(value, PropertyFlag.ConfigurableEnumerableWritable));
 
             using var jsonBuilder = StringBuilderPool.Rent();
-            using var numberBuilder = StringBuilderPool.Rent();
 
-            var target = new SerializerState(jsonBuilder.Builder, numberBuilder.Builder);
+            var target = new SerializerState(jsonBuilder.Builder);
             if (SerializeJSONProperty(JsString.Empty, wrapper, ref target) == SerializeResult.Undefined)
             {
                 return JsValue.Undefined;
@@ -160,7 +159,7 @@ namespace Jint.Native.Json
 
             if (ReferenceEquals(value, JsValue.Null))
             {
-                target.Json.Append(JsString.NullString);
+                target.Json.Append("null");
                 return SerializeResult.NotUndefined;
             }
 
@@ -172,7 +171,7 @@ namespace Jint.Native.Json
 
             if (value.IsString())
             {
-                QuoteJSONString(value.ToString(), ref target);
+                QuoteJSONString(value.ToString(), target.Json);
                 return SerializeResult.NotUndefined;
             }
 
@@ -195,9 +194,8 @@ namespace Jint.Native.Json
                         return SerializeResult.NotUndefined;
                     }
 
-                    target.NumberBuffer.Clear();
                     target.DtoaBuilder.Reset();
-                    target.Json.Append(NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, target.NumberBuffer));
+                    NumberPrototype.NumberToString(doubleValue, target.DtoaBuilder, target.Json);
                     return SerializeResult.NotUndefined;
                 }
 
@@ -303,63 +301,111 @@ namespace Jint.Native.Json
         /// <summary>
         /// https://tc39.es/ecma262/#sec-quotejsonstring
         /// </summary>
-        private static void QuoteJSONString(string value, ref SerializerState target)
+        /// <remarks>
+        /// MethodImplOptions.AggressiveOptimization = 512 which is only exposed in .NET Core.
+        /// </remarks>
+        [MethodImpl(MethodImplOptions.AggressiveInlining | (MethodImplOptions)512)]
+        private static unsafe void QuoteJSONString(string value, StringBuilder target)
         {
             if (value.Length == 0)
             {
-                target.Json.Append("\"\"");
+                target.Append("\"\"");
                 return;
             }
 
-            target.Json.Append('"');
-            for (var i = 0; i < value.Length; i++)
+            target.Append('"');
+
+#if NETCOREAPP1_0_OR_GREATER
+            fixed (char* ptr = value)
             {
-                var c = value[i];
-                switch (c)
+                int remainingLength = value.Length;
+                int offset = 0;
+                while (true)
                 {
-                    case '\"':
-                        target.Json.Append("\\\"");
-                        break;
-                    case '\\':
-                        target.Json.Append("\\\\");
-                        break;
-                    case '\b':
-                        target.Json.Append("\\b");
-                        break;
-                    case '\f':
-                        target.Json.Append("\\f");
-                        break;
-                    case '\n':
-                        target.Json.Append("\\n");
-                        break;
-                    case '\r':
-                        target.Json.Append("\\r");
-                        break;
-                    case '\t':
-                        target.Json.Append("\\t");
+                    int index = System.Text.Encodings.Web.JavaScriptEncoder.Default.FindFirstCharacterToEncode(ptr + offset, remainingLength);
+                    if (index < 0)
+                    {
+                        // append the remaining text which doesn't need any encoding.
+                        target.Append(value.AsSpan(offset));
                         break;
-                    default:
-                        if (char.IsSurrogatePair(value, i))
-                        {
-                            target.Json.Append(value[i]);
-                            i++;
-                            target.Json.Append(value[i]);
-                        }
-                        else if (c < 0x20 || char.IsSurrogate(c))
-                        {
-                            target.Json.Append("\\u");
-                            target.Json.Append(((int) c).ToString("x4"));
-                        }
-                        else
-                        {
-                            target.Json.Append(c);
-                        }
+                    }
+
+                    index += offset;
+                    if (index - offset > 0)
+                    {
+                        // append everything which does not need any encoding until the found index.
+                        target.Append(value.AsSpan(offset, index - offset));
+                    }
 
+                    AppendJsonStringCharacter(value, ref index, target);
+
+                    offset = index + 1;
+                    remainingLength = value.Length - offset;
+                    if (remainingLength == 0)
+                    {
                         break;
+                    }
                 }
             }
+#else
+            for (var i = 0; i < value.Length; i++)
+            {
+                AppendJsonStringCharacter(value, ref i, target);
+            }
+#endif
+            target.Append('"');
+        }
 
-            target.Json.Append('"');
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void AppendJsonStringCharacter(string value, ref int index, StringBuilder target)
+        {
+            var c = value[index];
+            switch (c)
+            {
+                case '\"':
+                    target.Append("\\\"");
+                    break;
+                case '\\':
+                    target.Append("\\\\");
+                    break;
+                case '\b':
+                    target.Append("\\b");
+                    break;
+                case '\f':
+                    target.Append("\\f");
+                    break;
+                case '\n':
+                    target.Append("\\n");
+                    break;
+                case '\r':
+                    target.Append("\\r");
+                    break;
+                case '\t':
+                    target.Append("\\t");
+                    break;
+                default:
+                    if (char.IsSurrogatePair(value, index))
+                    {
+#if NETCOREAPP1_0_OR_GREATER
+                        target.Append(value.AsSpan(index, 2));
+                        index++;
+#else
+                        target.Append(c);
+                        index++;
+                        target.Append(value[index]);
+#endif
+                    }
+                    else if (c < 0x20 || char.IsSurrogate(c))
+                    {
+                        target.Append("\\u");
+                        target.Append(((int) c).ToString("x4"));
+                    }
+                    else
+                    {
+                        target.Append(c);
+                    }
+                    break;
+            }
         }
 
         /// <summary>
@@ -381,7 +427,7 @@ namespace Jint.Native.Json
                 _indent += _gap;
             }
 
-            const string separator = ",";;
+            const char separator = ',';
             bool hasPrevious = false;
 
             for (int i = 0; i < len; i++)
@@ -449,8 +495,7 @@ namespace Jint.Native.Json
                 _indent += _gap;
             }
 
-            const string separator = ",";
-
+            const char separator = ',';
             var hasPrevious = false;
             for (var i = 0; i < enumeration.Keys.Count; i++)
             {
@@ -472,7 +517,7 @@ namespace Jint.Native.Json
                     target.Json.Append(_indent);
                 }
 
-                QuoteJSONString(p.ToString(), ref target);
+                QuoteJSONString(p.ToString(), target.Json);
                 target.Json.Append(':');
                 if (_gap.Length > 0)
                 {
@@ -510,15 +555,14 @@ namespace Jint.Native.Json
 
         private readonly ref struct SerializerState
         {
-            public SerializerState(StringBuilder jsonBuilder, StringBuilder numberBuffer)
+            public SerializerState(StringBuilder jsonBuilder)
             {
                 Json = jsonBuilder;
-                NumberBuffer = numberBuffer;
+                DtoaBuilder = TypeConverter.CreateDtoaBuilderForDouble();
             }
 
             public readonly StringBuilder Json;
-            public readonly StringBuilder NumberBuffer;
-            public readonly DtoaBuilder DtoaBuilder = TypeConverter.CreateDtoaBuilderForDouble();
+            public readonly DtoaBuilder DtoaBuilder;
         }
 
         private enum SerializeResult
@@ -556,13 +600,11 @@ namespace Jint.Native.Json
                         keys.RemoveAt(i);
                         i--;
                     }
-
                 }
             }
 
-            public List<JsValue> Keys { get; }
-
-            public bool IsEmpty { get; }
+            public readonly List<JsValue> Keys;
+            public readonly bool IsEmpty;
         }
     }
 }

+ 11 - 16
Jint/Native/Number/NumberPrototype.cs

@@ -442,35 +442,32 @@ namespace Jint.Native.Number
 
         private string ToNumberString(double m)
         {
-            using (var stringBuilder = StringBuilderPool.Rent())
-            {
-                return NumberToString(m, new DtoaBuilder(), stringBuilder.Builder);
-            }
+            using var stringBuilder = StringBuilderPool.Rent();
+            NumberToString(m, new DtoaBuilder(), stringBuilder.Builder);
+            return stringBuilder.Builder.ToString();
         }
 
-        internal static string NumberToString(
+        internal static void NumberToString(
             double m,
             DtoaBuilder builder,
             StringBuilder stringBuilder)
         {
             if (double.IsNaN(m))
             {
-                return "NaN";
+                stringBuilder.Append("NaN");
+                return;
             }
 
             if (m == 0)
             {
-                return "0";
-            }
-
-            if (double.IsPositiveInfinity(m))
-            {
-                return "Infinity";
+                stringBuilder.Append('0');
+                return;
             }
 
-            if (double.IsNegativeInfinity(m))
+            if (double.IsInfinity(m))
             {
-                return "-Infinity";
+                stringBuilder.Append(double.IsNegativeInfinity(m) ? "-Infinity" : "Infinity");
+                return;
             }
 
             DtoaNumberFormatter.DoubleToAscii(
@@ -526,8 +523,6 @@ namespace Jint.Native.Number
 
                 stringBuilder.Append(exponent);
             }
-
-            return stringBuilder.ToString();
         }
     }
 }

+ 2 - 1
Jint/Runtime/TypeConverter.cs

@@ -890,7 +890,8 @@ namespace Jint.Runtime
 
             using var stringBuilder = StringBuilderPool.Rent();
             // we can create smaller array as we know the format to be short
-            return NumberPrototype.NumberToString(d, CreateDtoaBuilderForDouble(), stringBuilder.Builder);
+            NumberPrototype.NumberToString(d, CreateDtoaBuilderForDouble(), stringBuilder.Builder);
+            return stringBuilder.Builder.ToString();
         }
 
         /// <summary>