Serialization performance improvements (dotnet/corefx#41098)
authorSteve Harter <steveharter@users.noreply.github.com>
Mon, 16 Sep 2019 16:20:43 +0000 (11:20 -0500)
committerGitHub <noreply@github.com>
Mon, 16 Sep 2019 16:20:43 +0000 (11:20 -0500)
Commit migrated from https://github.com/dotnet/corefx/commit/017a038abaabedcbbc61c85c9f7405f588ca3b46

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleObject.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.String.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs

index ba52359..1923347 100644 (file)
@@ -24,9 +24,13 @@ namespace System.Text.Json
         // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache.
         private const int PropertyNameCountCacheThreshold = 64;
 
-        // All of the serializable properties on a POCO keyed on property name.
+        // All of the serializable properties on a POCO (except the optional extension property) keyed on property name.
         public volatile Dictionary<string, JsonPropertyInfo> PropertyCache;
 
+        // All of the serializable properties on a POCO including the optional extension property.
+        // Used for performance during serialization instead of 'PropertyCache' above.
+        public volatile JsonPropertyInfo[] PropertyCacheArray;
+
         // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache.
         // Use an array (instead of List<T>) for highest performance.
         private volatile PropertyRef[] _propertyRefsSorted;
@@ -159,14 +163,26 @@ namespace System.Text.Json
                             }
                         }
 
+                        JsonPropertyInfo[] cacheArray;
                         if (DetermineExtensionDataProperty(cache))
                         {
                             // Remove from cache since it is handled independently.
                             cache.Remove(DataExtensionProperty.NameAsString);
+
+                            cacheArray = new JsonPropertyInfo[cache.Count + 1];
+
+                            // Set the last element to the extension property.
+                            cacheArray[cache.Count] = DataExtensionProperty;
+                        }
+                        else
+                        {
+                            cacheArray = new JsonPropertyInfo[cache.Count];
                         }
 
-                        // Set as a unit to avoid concurrency issues.
+                        // Set fields when finished to avoid concurrency issues.
                         PropertyCache = cache;
+                        cache.Values.CopyTo(cacheArray, 0);
+                        PropertyCacheArray = cacheArray;
                     }
                     break;
                 case ClassType.Enumerable:
@@ -213,6 +229,7 @@ namespace System.Text.Json
                     // Add a single property that maps to the class type so we can have policies applied.
                     AddPolicyProperty(type, options);
                     PropertyCache = new Dictionary<string, JsonPropertyInfo>();
+                    PropertyCacheArray = Array.Empty<JsonPropertyInfo>();
                     break;
                 default:
                     Debug.Fail($"Unexpected class type: {ClassType}");
index b61647f..63672b6 100644 (file)
@@ -237,7 +237,8 @@ namespace System.Text.Json
         public JsonDictionaryConverter DictionaryConverter { get; private set; }
 
         // The escaped name passed to the writer.
-        public JsonEncodedText? EscapedName { get; private set; }
+        // Use a field here (not a property) to avoid value semantics.
+        public JsonEncodedText? EscapedName;
 
         public static TAttribute GetAttribute<TAttribute>(PropertyInfo propertyInfo) where TAttribute : Attribute
         {
index 4395899..2ec63b8 100644 (file)
@@ -2,13 +2,15 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using System.Collections.Generic;
 using System.Diagnostics;
+using System.Runtime.CompilerServices;
 
 namespace System.Text.Json
 {
     public static partial class JsonSerializer
     {
+        // AggressiveInlining used although a large method it is only called from one location and is on a hot path.
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private static bool WriteObject(
             JsonSerializerOptions options,
             Utf8JsonWriter writer,
@@ -26,32 +28,25 @@ namespace System.Text.Json
                 }
 
                 state.Current.WriteObjectOrArrayStart(ClassType.Object, writer, options);
-                state.Current.PropertyEnumerator = state.Current.JsonClassInfo.PropertyCache.GetEnumerator();
                 state.Current.PropertyEnumeratorActive = true;
-                state.Current.NextProperty();
+                state.Current.MoveToNextProperty = true;
             }
-            else if (state.Current.MoveToNextProperty)
+
+            if (state.Current.MoveToNextProperty)
             {
                 state.Current.NextProperty();
             }
 
             // Determine if we are done enumerating properties.
-            // If the ClassType is unknown, there will be a policy property applied
-            JsonClassInfo classInfo = state.Current.JsonClassInfo;
-            if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyEnumeratorActive)
+            if (state.Current.PropertyEnumeratorActive)
             {
-                HandleObject(state.Current.PropertyEnumerator.Current.Value, options, writer, ref state);
-                return false;
-            }
+                // If ClassType.Unknown at this point, we are typeof(object) which should not have any properties.
+                Debug.Assert(state.Current.JsonClassInfo.ClassType != ClassType.Unknown);
 
-            if (state.Current.ExtensionDataStatus == Serialization.ExtensionDataWriteStatus.Writing)
-            {
-                JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.DataExtensionProperty;
-                if (jsonPropertyInfo != null)
-                {
-                    HandleObject(jsonPropertyInfo, options, writer, ref state);
-                    return false;
-                }
+                JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.PropertyCacheArray[state.Current.PropertyEnumeratorIndex - 1];
+                HandleObject(jsonPropertyInfo, options, writer, ref state);
+
+                return false;
             }
 
             writer.WriteEndObject();
@@ -72,11 +67,13 @@ namespace System.Text.Json
             return true;
         }
 
-        private static bool HandleObject(
-                JsonPropertyInfo jsonPropertyInfo,
-                JsonSerializerOptions options,
-                Utf8JsonWriter writer,
-                ref WriteStack state)
+        // AggressiveInlining used although a large method it is only called from one location and is on a hot path.
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static void HandleObject(
+            JsonPropertyInfo jsonPropertyInfo,
+            JsonSerializerOptions options,
+            Utf8JsonWriter writer,
+            ref WriteStack state)
         {
             Debug.Assert(
                 state.Current.JsonClassInfo.ClassType == ClassType.Object ||
@@ -85,7 +82,7 @@ namespace System.Text.Json
             if (!jsonPropertyInfo.ShouldSerialize)
             {
                 state.Current.MoveToNextProperty = true;
-                return true;
+                return;
             }
 
             bool obtainedValue = false;
@@ -105,7 +102,7 @@ namespace System.Text.Json
             {
                 jsonPropertyInfo.Write(ref state, writer);
                 state.Current.MoveToNextProperty = true;
-                return true;
+                return;
             }
 
             // A property that returns an enumerator keeps the same stack frame.
@@ -117,7 +114,7 @@ namespace System.Text.Json
                     state.Current.MoveToNextProperty = true;
                 }
 
-                return endOfEnumerable;
+                return;
             }
 
             // A property that returns a dictionary keeps the same stack frame.
@@ -129,7 +126,7 @@ namespace System.Text.Json
                     state.Current.MoveToNextProperty = true;
                 }
 
-                return endOfEnumerable;
+                return;
             }
 
             // A property that returns a type that is deserialized by passing an
@@ -144,7 +141,7 @@ namespace System.Text.Json
                     state.Current.MoveToNextProperty = true;
                 }
 
-                return endOfEnumerable;
+                return;
             }
 
             // A property that returns an object.
@@ -174,8 +171,6 @@ namespace System.Text.Json
 
                 state.Current.MoveToNextProperty = true;
             }
-
-            return true;
         }
     }
 }
index 52552d8..88acff6 100644 (file)
@@ -90,31 +90,25 @@ namespace System.Text.Json
             return result;
         }
 
-        private static string WriteValueCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options)
+        private static void WriteValueCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options)
         {
             if (options == null)
             {
                 options = JsonSerializerOptions.s_defaultOptions;
             }
 
-            string result;
-
-            using (var output = new PooledByteBufferWriter(options.DefaultBufferSize))
-            {
-                WriteCore(writer, output, value, type, options);
-                result = JsonReaderHelper.TranscodeHelper(output.WrittenMemory.Span);
-            }
-
-            return result;
+            WriteCore(writer, value, type, options);
         }
 
         private static void WriteCore(PooledByteBufferWriter output, object value, Type type, JsonSerializerOptions options)
         {
-            using var writer = new Utf8JsonWriter(output, options.GetWriterOptions());
-            WriteCore(writer, output, value, type, options);
+            using (var writer = new Utf8JsonWriter(output, options.GetWriterOptions()))
+            {
+                WriteCore(writer, value, type, options);
+            }
         }
 
-        private static void WriteCore(Utf8JsonWriter writer, PooledByteBufferWriter output, object value, Type type, JsonSerializerOptions options)
+        private static void WriteCore(Utf8JsonWriter writer, object value, Type type, JsonSerializerOptions options)
         {
             Debug.Assert(type != null || value == null);
 
index 1e5a661..8937e2c 100644 (file)
@@ -18,7 +18,7 @@ namespace System.Text.Json
         /// </remarks>
         public static string Serialize<TValue>(TValue value, JsonSerializerOptions options = null)
         {
-            return ToStringInternal(value, typeof(TValue), options);
+            return WriteCoreString(value, typeof(TValue), options);
         }
 
         /// <summary>
@@ -35,12 +35,6 @@ namespace System.Text.Json
         public static string Serialize(object value, Type inputType, JsonSerializerOptions options = null)
         {
             VerifyValueAndType(value, inputType);
-
-            return ToStringInternal(value, inputType, options);
-        }
-
-        private static string ToStringInternal(object value, Type inputType, JsonSerializerOptions options)
-        {
             return WriteCoreString(value, inputType, options);
         }
     }
index a133d5a..5d40d26 100644 (file)
@@ -36,24 +36,21 @@ namespace System.Text.Json
                             current.JsonPropertyInfo.Write(ref state, writer);
                             finishedSerializing = true;
                             break;
-                        case ClassType.Object:
-                            finishedSerializing = WriteObject(options, writer, ref state);
-                            break;
                         case ClassType.Dictionary:
                         case ClassType.IDictionaryConstructible:
                             finishedSerializing = HandleDictionary(current.JsonClassInfo.ElementClassInfo, options, writer, ref state);
                             break;
                         default:
-                            Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Unknown);
+                            Debug.Assert(state.Current.JsonClassInfo.ClassType == ClassType.Object ||
+                                state.Current.JsonClassInfo.ClassType == ClassType.Unknown);
 
-                            // Treat typeof(object) as an empty object.
                             finishedSerializing = WriteObject(options, writer, ref state);
                             break;
                     }
 
                     if (finishedSerializing)
                     {
-                        if (writer.CurrentDepth == 0 || writer.CurrentDepth == originalWriterDepth)
+                        if (writer.CurrentDepth == originalWriterDepth)
                         {
                             break;
                         }
@@ -63,7 +60,7 @@ namespace System.Text.Json
                         ThrowHelper.ThrowInvalidOperationException_SerializerCycleDetected(options.MaxDepth);
                     }
 
-                    // If serialization is not yet end and we surpass beyond flush threshold return false and flush stream.
+                    // If serialization is not finished and we surpass flush threshold then return false which will flush stream.
                     if (flushThreshold >= 0 && writer.BytesPending > flushThreshold)
                     {
                         return false;
index a409b14..f2b7913 100644 (file)
@@ -3,8 +3,8 @@
 // See the LICENSE file in the project root for more information.
 
 using System.Collections;
-using System.Collections.Generic;
 using System.Diagnostics;
+using System.Runtime.CompilerServices;
 using System.Text.Json.Serialization;
 
 namespace System.Text.Json
@@ -32,8 +32,8 @@ namespace System.Text.Json
 
         // The current property.
         public bool PropertyEnumeratorActive;
+        public int PropertyEnumeratorIndex;
         public ExtensionDataWriteStatus ExtensionDataStatus;
-        public Dictionary<string, JsonPropertyInfo>.Enumerator PropertyEnumerator;
         public JsonPropertyInfo JsonPropertyInfo;
 
         public void Initialize(Type type, JsonSerializerOptions options)
@@ -111,7 +111,7 @@ namespace System.Text.Json
             ExtensionDataStatus = ExtensionDataWriteStatus.NotStarted;
             IsIDictionaryConstructible = false;
             JsonClassInfo = null;
-            PropertyEnumerator = default;
+            PropertyEnumeratorIndex = 0;
             PropertyEnumeratorActive = false;
             PopStackOnEndCollection = false;
             PopStackOnEndObject = false;
@@ -119,6 +119,7 @@ namespace System.Text.Json
             EndProperty();
         }
 
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public void EndProperty()
         {
             IsIDictionaryConstructibleProperty = false;
@@ -139,20 +140,28 @@ namespace System.Text.Json
             PopStackOnEndCollection = false;
         }
 
+        // AggressiveInlining used although a large method it is only called from one location and is on a hot path.
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public void NextProperty()
         {
             EndProperty();
 
             if (PropertyEnumeratorActive)
             {
-                if (PropertyEnumerator.MoveNext())
+                int len = JsonClassInfo.PropertyCacheArray.Length;
+                if (PropertyEnumeratorIndex < len)
                 {
+                    if ((PropertyEnumeratorIndex == len - 1) && JsonClassInfo.DataExtensionProperty != null)
+                    {
+                        ExtensionDataStatus = ExtensionDataWriteStatus.Writing;
+                    }
+
+                    PropertyEnumeratorIndex++;
                     PropertyEnumeratorActive = true;
                 }
                 else
                 {
                     PropertyEnumeratorActive = false;
-                    ExtensionDataStatus = ExtensionDataWriteStatus.Writing;
                 }
             }
             else