Performance improvements on property lookup (dotnet/corefx#38902)
authorSteve Harter <steveharter@users.noreply.github.com>
Wed, 26 Jun 2019 13:46:23 +0000 (06:46 -0700)
committerGitHub <noreply@github.com>
Wed, 26 Jun 2019 13:46:23 +0000 (06:46 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/5e6c939705fd23e81d2a74c4a00e236a3d4289b5

15 files changed:
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.AddProperty.cs
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/JsonPropertyInfoNotNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNotNullableContravariant.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfoNullable.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleDictionary.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.HandleEnumerable.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/ReadStackFrame.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStackFrame.cs
src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Object.cs

index e7a112d..b6a8578 100644 (file)
@@ -10,18 +10,18 @@ namespace System.Text.Json
 {
     internal partial class JsonClassInfo
     {
-        private JsonPropertyInfo AddPolicyProperty(Type propertyType, JsonSerializerOptions options)
+        private void AddPolicyProperty(Type propertyType, JsonSerializerOptions options)
         {
             // A policy property is not a real property on a type; instead it leverages the existing converter
             // logic and generic support to avoid boxing. It is used with values types and elements from collections and
             // dictionaries. Typically it would represent a CLR type such as System.String.
-            return AddProperty(
+            PolicyProperty = AddProperty(
                 propertyType,
                 propertyInfo : null,        // Not a real property so this is null.
                 classType : typeof(object), // A dummy type (not used).
                 options : options);
-
         }
+
         private JsonPropertyInfo AddProperty(Type propertyType, PropertyInfo propertyInfo, Type classType, JsonSerializerOptions options)
         {
             JsonPropertyInfo jsonInfo = CreateProperty(propertyType, propertyType, propertyInfo, classType, options);
@@ -54,16 +54,6 @@ namespace System.Text.Json
                 }
             }
 
-            if (propertyInfo != null)
-            {
-                _propertyRefs.Add(new PropertyRef(GetKey(jsonInfo.NameUsedToCompare), jsonInfo));
-            }
-            else
-            {
-                // A single property or an IEnumerable
-                _propertyRefs.Add(new PropertyRef(0, jsonInfo));
-            }
-
             return jsonInfo;
         }
 
index d52e6e9..938d4b5 100644 (file)
@@ -5,6 +5,7 @@
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Linq;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Text.Json.Serialization;
@@ -18,9 +19,13 @@ namespace System.Text.Json
         // The length of the property name embedded in the key (in bytes).
         private const int PropertyNameKeyLength = 6;
 
-        private readonly List<PropertyRef> _propertyRefs = new List<PropertyRef>();
+        // The limit to how many property names from the JSON are cached before using PropertyCache.
+        private const int PropertyNameCountCacheThreshold = 64;
 
-        // Cache of properties by first ordering. Use an array for highest performance.
+        // The properties on a POCO keyed on property name.
+        public volatile Dictionary<string, JsonPropertyInfo> PropertyCache;
+
+        // Cache of properties by first JSON ordering. Use an array for highest performance.
         private volatile PropertyRef[] _propertyRefsSorted = null;
 
         internal delegate object ConstructorDelegate();
@@ -33,47 +38,37 @@ namespace System.Text.Json
         // If enumerable, the JsonClassInfo for the element type.
         internal JsonClassInfo ElementClassInfo { get; private set; }
 
+        public JsonSerializerOptions Options { get; private set; }
+
         public Type Type { get; private set; }
 
         internal void UpdateSortedPropertyCache(ref ReadStackFrame frame)
         {
-            // Todo: on classes with many properties (about 50) we need to switch to a hashtable for performance.
-            // Todo: when using PropertyNameCaseInsensitive we also need to use the hashtable with case-insensitive
-            // comparison to handle Turkish etc. cultures properly.
-
-            Debug.Assert(_propertyRefs != null);
-
-            // Set the sorted property cache. Overwrite any existing cache which can occur in multi-threaded cases.
-            if (frame.PropertyRefCache != null)
+            // Check if we are trying to build the sorted cache.
+            if (frame.PropertyRefCache == null)
             {
-                HashSet<PropertyRef> cache = frame.PropertyRefCache;
-
-                // Add any missing properties. This creates a consistent cache count which is important for
-                // the loop in GetProperty() when there are multiple threads in a race conditions each generating
-                // a cache for a given POCO type but with different property counts in the json.
-                while (cache.Count < _propertyRefs.Count)
-                {
-                    for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++)
-                    {
-                        PropertyRef propertyRef = _propertyRefs[iProperty];
-                        // Cache the missing property or override the existing property.
-                        cache.Add(propertyRef);
-                    }
-                }
+                return;
+            }
 
-                Debug.Assert(cache.Count == _propertyRefs.Count);
-                if (_propertyRefsSorted == null || _propertyRefsSorted.Length < cache.Count)
-                {
-                    _propertyRefsSorted = new PropertyRef[cache.Count];
-                }
-                cache.CopyTo(_propertyRefsSorted);
-                frame.PropertyRefCache = null;
+            List<PropertyRef> newList;
+            if (_propertyRefsSorted != null)
+            {
+                newList = new List<PropertyRef>(_propertyRefsSorted);
+                newList.AddRange(frame.PropertyRefCache);
+                _propertyRefsSorted = newList.ToArray();
             }
+            else
+            {
+                _propertyRefsSorted = frame.PropertyRefCache.ToArray();
+            }
+
+            frame.PropertyRefCache = null;
         }
 
         internal JsonClassInfo(Type type, JsonSerializerOptions options)
         {
             Type = type;
+            Options = options;
             ClassType = GetClassType(type, options);
 
             CreateObject = options.MemberAccessorStrategy.CreateConstructor(type);
@@ -83,9 +78,11 @@ namespace System.Text.Json
             {
                 case ClassType.Object:
                     {
-                        var propertyNames = new HashSet<string>(StringComparer.Ordinal);
+                        PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+
+                        Dictionary<string, JsonPropertyInfo> cache = CreatePropertyCache(properties.Length);
 
-                        foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+                        foreach (PropertyInfo propertyInfo in properties)
                         {
                             // Ignore indexers
                             if (propertyInfo.GetIndexParameters().Length > 0)
@@ -99,18 +96,19 @@ namespace System.Text.Json
                             {
                                 JsonPropertyInfo jsonPropertyInfo = AddProperty(propertyInfo.PropertyType, propertyInfo, type, options);
 
-                                Debug.Assert(jsonPropertyInfo.NameUsedToCompareAsString != null);
-
                                 // If the JsonPropertyNameAttribute or naming policy results in collisions, throw an exception.
-                                if (!propertyNames.Add(jsonPropertyInfo.NameUsedToCompareAsString))
+                                if (cache.ContainsKey(jsonPropertyInfo.NameAsString))
                                 {
                                     ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(this, jsonPropertyInfo);
                                 }
 
-                                jsonPropertyInfo.ClearUnusedValuesAfterAdd();
+                                cache.Add(jsonPropertyInfo.NameAsString, jsonPropertyInfo);
                             }
                         }
 
+                        // Set as a unit to avoid concurrency issues.
+                        PropertyCache = cache;
+
                         DetermineExtensionDataProperty();
                     }
                     break;
@@ -118,10 +116,10 @@ namespace System.Text.Json
                 case ClassType.Dictionary:
                     {
                         // Add a single property that maps to the class type so we can have policies applied.
-                        JsonPropertyInfo policyProperty = AddPolicyProperty(type, options);
+                         AddPolicyProperty(type, options);
 
                         // Use the type from the property policy to get any late-bound concrete types (from an interface like IDictionary).
-                        CreateObject = options.MemberAccessorStrategy.CreateConstructor(policyProperty.RuntimePropertyType);
+                        CreateObject = options.MemberAccessorStrategy.CreateConstructor(PolicyProperty.RuntimePropertyType);
 
                         // Create a ClassInfo that maps to the element type which is used for (de)serialization and policies.
                         Type elementType = GetElementType(type, parentType: null, memberInfo: null, options: options);
@@ -143,9 +141,13 @@ namespace System.Text.Json
                     }
                     break;
                 case ClassType.Value:
+                    // Add a single property that maps to the class type so we can have policies applied.
+                    AddPolicyProperty(type, options);
+                    break;
                 case ClassType.Unknown:
                     // Add a single property that maps to the class type so we can have policies applied.
                     AddPolicyProperty(type, options);
+                    PropertyCache = new Dictionary<string, JsonPropertyInfo>();
                     break;
                 default:
                     Debug.Fail($"Unexpected class type: {ClassType}");
@@ -171,14 +173,12 @@ namespace System.Text.Json
 
         private JsonPropertyInfo GetPropertyThatHasAttribute(Type attributeType)
         {
-            Debug.Assert(_propertyRefs != null);
+            Debug.Assert(PropertyCache != null);
 
             JsonPropertyInfo property = null;
 
-            for (int iProperty = 0; iProperty < _propertyRefs.Count; iProperty++)
+            foreach (JsonPropertyInfo jsonPropertyInfo in PropertyCache.Values)
             {
-                PropertyRef propertyRef = _propertyRefs[iProperty];
-                JsonPropertyInfo jsonPropertyInfo = propertyRef.Info;
                 Attribute attribute = jsonPropertyInfo.PropertyInfo.GetCustomAttribute(attributeType);
                 if (attribute != null)
                 {
@@ -194,37 +194,29 @@ namespace System.Text.Json
             return property;
         }
 
-        internal JsonPropertyInfo GetProperty(JsonSerializerOptions options, ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
+        internal JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
         {
-            // If we should compare with case-insensitive, normalize to an uppercase format since that is what is cached on the propertyInfo.
-            if (options.PropertyNameCaseInsensitive)
-            {
-                string utf16PropertyName = JsonHelpers.Utf8GetString(propertyName);
-                string upper = utf16PropertyName.ToUpperInvariant();
-                propertyName = Encoding.UTF8.GetBytes(upper);
-            }
-
-            ulong key = GetKey(propertyName);
             JsonPropertyInfo info = null;
 
-            // First try sorted lookup.
-            int propertyIndex = frame.PropertyIndex;
-
             // If we're not trying to build the cache locally, and there is an existing cache, then use it.
-            bool hasPropertyCache = frame.PropertyRefCache == null && _propertyRefsSorted != null;
-            if (hasPropertyCache)
+            if (_propertyRefsSorted != null)
             {
+                ulong key = GetKey(propertyName);
+
+                // First try sorted lookup.
+                int propertyIndex = frame.PropertyIndex;
+
                 // This .Length is consistent no matter what json data intialized _propertyRefsSorted.
                 int count = _propertyRefsSorted.Length;
                 if (count != 0)
                 {
-                    int iForward = propertyIndex;
-                    int iBackward = propertyIndex - 1;
+                    int iForward = Math.Min(propertyIndex, count);
+                    int iBackward = iForward - 1;
                     while (iForward < count || iBackward >= 0)
                     {
                         if (iForward < count)
                         {
-                            if (TryIsPropertyRefEqual(ref _propertyRefsSorted[iForward], propertyName, key, ref info))
+                            if (TryIsPropertyRefEqual(_propertyRefsSorted[iForward], propertyName, key, ref info))
                             {
                                 return info;
                             }
@@ -233,7 +225,7 @@ namespace System.Text.Json
 
                         if (iBackward >= 0)
                         {
-                            if (TryIsPropertyRefEqual(ref _propertyRefsSorted[iBackward], propertyName, key, ref info))
+                            if (TryIsPropertyRefEqual(_propertyRefsSorted[iBackward], propertyName, key, ref info))
                             {
                                 return info;
                             }
@@ -246,60 +238,77 @@ namespace System.Text.Json
             // Try the main list which has all of the properties in a consistent order.
             // We could get here even when hasPropertyCache==true if there is a race condition with different json
             // property ordering and _propertyRefsSorted is re-assigned while in the loop above.
-            for (int i = 0; i < _propertyRefs.Count; i++)
+
+            string stringPropertyName = Encoding.UTF8.GetString(propertyName
+#if netstandard
+                        .ToArray()
+#endif
+            );
+
+            if (PropertyCache.TryGetValue(stringPropertyName, out info))
             {
-                PropertyRef propertyRef = _propertyRefs[i];
-                if (TryIsPropertyRefEqual(ref propertyRef, propertyName, key, ref info))
+                // For performance, only add to cache up to a threshold and then just use the dictionary.
+                int count;
+                if (_propertyRefsSorted != null)
                 {
-                    break;
+                    count = _propertyRefsSorted.Length;
                 }
-            }
-
-            if (!hasPropertyCache)
-            {
-                if (propertyIndex == 0 && frame.PropertyRefCache == null)
+                else
                 {
-                    // Create the temporary list on first property access to prevent a partially filled List.
-                    frame.PropertyRefCache = new HashSet<PropertyRef>();
+                    count = 0;
                 }
-
-                if (info != null)
+                
+                // Do a quick check for the stable (after warm-up) case.
+                if (count < PropertyNameCountCacheThreshold)
                 {
-                    Debug.Assert(frame.PropertyRefCache != null);
-                    frame.PropertyRefCache.Add(new PropertyRef(key, info));
+                    if (frame.PropertyRefCache != null)
+                    {
+                        count += frame.PropertyRefCache.Count;
+                    }
+
+                    // Check again to fill up to the limit.
+                    if (count < PropertyNameCountCacheThreshold)
+                    {
+                        if (frame.PropertyRefCache == null)
+                        {
+                            frame.PropertyRefCache = new List<PropertyRef>();
+                        }
+
+                        ulong key = info.PropertyNameKey;
+                        PropertyRef propertyRef = new PropertyRef(key, info);
+                        frame.PropertyRefCache.Add(propertyRef);
+                    }
                 }
             }
 
             return info;
         }
 
-        internal JsonPropertyInfo GetPolicyProperty()
+        private Dictionary<string, JsonPropertyInfo> CreatePropertyCache(int capacity)
         {
-            Debug.Assert(_propertyRefs.Count == 1);
-            return _propertyRefs[0].Info;
-        }
-
-        internal JsonPropertyInfo GetProperty(int index)
-        {
-            Debug.Assert(index < _propertyRefs.Count);
-            return _propertyRefs[index].Info;
-        }
+            StringComparer comparer;
 
-        internal int PropertyCount
-        {
-            get
+            if (Options.PropertyNameCaseInsensitive)
+            {
+                comparer = StringComparer.OrdinalIgnoreCase;
+            }
+            else
             {
-                return _propertyRefs.Count;
+                comparer = StringComparer.Ordinal;
             }
+
+            return new Dictionary<string, JsonPropertyInfo>(capacity, comparer);
         }
 
-        private static bool TryIsPropertyRefEqual(ref PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
+        public JsonPropertyInfo PolicyProperty { get; private set; }
+
+        private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
         {
             if (key == propertyRef.Key)
             {
                 if (propertyName.Length <= PropertyNameKeyLength ||
                     // We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster)
-                    propertyName.SequenceEqual(propertyRef.Info.NameUsedToCompare))
+                    propertyName.SequenceEqual(propertyRef.Info.Name))
                 {
                     info = propertyRef.Info;
                     return true;
@@ -323,7 +332,7 @@ namespace System.Text.Json
             return false;
         }
 
-        private static ulong GetKey(ReadOnlySpan<byte> propertyName)
+        public static ulong GetKey(ReadOnlySpan<byte> propertyName)
         {
             ulong key;
             int length = propertyName.Length;
index 5e59965..b5d9d4c 100644 (file)
@@ -30,21 +30,14 @@ namespace System.Text.Json
 
         public ClassType ClassType;
 
-        // After the property is added, clear any state not used later.
-        public void ClearUnusedValuesAfterAdd()
-        {
-            NameAsString = null;
-            NameUsedToCompareAsString = null;
-        }
-
         public abstract JsonConverter ConverterBase { get; set; }
 
         // Copy any settings defined at run-time to the new property.
         public void CopyRuntimeSettingsTo(JsonPropertyInfo other)
         {
-            other.Name = Name;
-            other.NameUsedToCompare = NameUsedToCompare;
             other.EscapedName = EscapedName;
+            other.Name = Name;
+            other.PropertyNameKey = PropertyNameKey;
         }
 
         public abstract IList CreateConverterList();
@@ -112,20 +105,11 @@ namespace System.Text.Json
             // At this point propertyName is valid UTF16, so just call the simple UTF16->UTF8 encoder.
             Name = Encoding.UTF8.GetBytes(NameAsString);
 
-            // Set the compare name.
-            if (Options.PropertyNameCaseInsensitive)
-            {
-                NameUsedToCompareAsString = NameAsString.ToUpperInvariant();
-                NameUsedToCompare = Encoding.UTF8.GetBytes(NameUsedToCompareAsString);
-            }
-            else
-            {
-                NameUsedToCompareAsString = NameAsString;
-                NameUsedToCompare = Name;
-            }
-
             // Cache the escaped name.
             EscapedName = JsonEncodedText.Encode(Name);
+
+            ulong key = JsonClassInfo.GetKey(Name);
+            PropertyNameKey = key;
         }
 
         private void DetermineSerializationCapabilities()
@@ -311,9 +295,8 @@ namespace System.Text.Json
         public byte[] Name { get; private set; }
         public string NameAsString { get; private set; }
 
-        // Used to support case-insensitive comparison
-        public byte[] NameUsedToCompare { get; private set; }
-        public string NameUsedToCompareAsString { get; private set; }
+        // Key for fast property name lookup.
+        public ulong PropertyNameKey { get; set; }
 
         // Options can be referenced here since all JsonPropertyInfos originate from a JsonClassInfo that is cached on JsonSerializerOptions.
         protected JsonSerializerOptions Options { get; set; }
@@ -335,7 +318,7 @@ namespace System.Text.Json
             if (ElementClassInfo != null)
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
-                JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
+                JsonPropertyInfo propertyInfo = ElementClassInfo.PolicyProperty;
                 propertyInfo.ReadEnumerable(tokenType, ref state, ref reader);
             }
             else
@@ -419,10 +402,10 @@ namespace System.Text.Json
         {
             Debug.Assert(ShouldSerialize);
 
-            if (state.Current.Enumerator != null)
+            if (state.Current.CollectionEnumerator != null)
             {
                 // Forward the setter to the value-based JsonPropertyInfo.
-                JsonPropertyInfo propertyInfo = ElementClassInfo.GetPolicyProperty();
+                JsonPropertyInfo propertyInfo = ElementClassInfo.PolicyProperty;
                 propertyInfo.WriteEnumerable(ref state, writer);
             }
             else
index 660d933..3a84542 100644 (file)
@@ -104,17 +104,17 @@ namespace System.Text.Json
         {
             if (Converter != null)
             {
-                Debug.Assert(current.Enumerator != null);
+                Debug.Assert(current.CollectionEnumerator != null);
 
                 TConverter value;
-                if (current.Enumerator is IEnumerator<TConverter> enumerator)
+                if (current.CollectionEnumerator is IEnumerator<TConverter> enumerator)
                 {
                     // Avoid boxing for strongly-typed enumerators such as returned from IList<T>.
                     value = enumerator.Current;
                 }
                 else
                 {
-                    value = (TConverter)current.Enumerator.Current;
+                    value = (TConverter)current.CollectionEnumerator.Current;
                 }
 
                 if (value == null)
index 234b7bc..dfa9193 100644 (file)
@@ -105,17 +105,17 @@ namespace System.Text.Json.Serialization
         {
             if (Converter != null)
             {
-                Debug.Assert(current.Enumerator != null);
+                Debug.Assert(current.CollectionEnumerator != null);
 
                 TConverter value;
-                if (current.Enumerator is IEnumerator<TConverter> enumerator)
+                if (current.CollectionEnumerator is IEnumerator<TConverter> enumerator)
                 {
                     // Avoid boxing for strongly-typed enumerators such as returned from IList<T>.
                     value = enumerator.Current;
                 }
                 else
                 {
-                    value = (TConverter)current.Enumerator.Current;
+                    value = (TConverter)current.CollectionEnumerator.Current;
                 }
 
                 if (value == null)
index 81c9ad5..46dc319 100644 (file)
@@ -85,17 +85,17 @@ namespace System.Text.Json
         {
             if (Converter != null)
             {
-                Debug.Assert(current.Enumerator != null);
+                Debug.Assert(current.CollectionEnumerator != null);
 
                 TProperty? value;
-                if (current.Enumerator is IEnumerator<TProperty?> enumerator)
+                if (current.CollectionEnumerator is IEnumerator<TProperty?> enumerator)
                 {
                     // Avoid boxing for strongly-typed enumerators such as returned from IList<T>.
                     value = enumerator.Current;
                 }
                 else
                 {
-                    value = (TProperty?)current.Enumerator.Current;
+                    value = (TProperty?)current.CollectionEnumerator.Current;
                 }
 
                 if (value == null)
index d4066fc..147be56 100644 (file)
@@ -49,7 +49,7 @@ namespace System.Text.Json
 
                     if (state.Current.IsDictionary || state.Current.IsIDictionaryConstructible)
                     {
-                        state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetPolicyProperty();
+                        state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.PolicyProperty;
                     }
 
                     Debug.Assert(
@@ -73,7 +73,7 @@ namespace System.Text.Json
                     propertyName = GetUnescapedString(propertyName, idx);
                 }
 
-                state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(options, propertyName, ref state.Current);
+                state.Current.JsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(propertyName, ref state.Current);
                 if (state.Current.JsonPropertyInfo == null)
                 {
                     if (state.Current.JsonClassInfo.DataExtensionProperty == null)
index cb45ac1..ae2568b 100644 (file)
@@ -58,7 +58,7 @@ namespace System.Text.Json
                             readStack.Push();
                             readStack.Current.Drain = true;
                         }
-                        else if (readStack.Current.IsProcessingValue)
+                        else if (readStack.Current.IsProcessingValue(tokenType))
                         {
                             if (!HandleObjectAsValue(tokenType, options, ref reader, ref readStack, ref initialState))
                             {
@@ -92,7 +92,7 @@ namespace System.Text.Json
                     }
                     else if (tokenType == JsonTokenType.StartArray)
                     {
-                        if (!readStack.Current.IsProcessingValue)
+                        if (!readStack.Current.IsProcessingValue(tokenType))
                         {
                             HandleStartArray(options, ref reader, ref readStack);
                         }
index b40d502..2cf1d0f 100644 (file)
@@ -18,7 +18,7 @@ namespace System.Text.Json
             ref WriteStack state)
         {
             JsonPropertyInfo jsonPropertyInfo = state.Current.JsonPropertyInfo;
-            if (state.Current.Enumerator == null)
+            if (state.Current.CollectionEnumerator == null)
             {
                 IEnumerable enumerable;
 
@@ -36,37 +36,37 @@ namespace System.Text.Json
 
                 if (enumerable is IDictionary dictionary)
                 {
-                    state.Current.Enumerator = dictionary.GetEnumerator();
+                    state.Current.CollectionEnumerator = dictionary.GetEnumerator();
                 }
                 else
                 {
-                    state.Current.Enumerator = enumerable.GetEnumerator();
+                    state.Current.CollectionEnumerator = enumerable.GetEnumerator();
                 }
 
                 state.Current.WriteObjectOrArrayStart(ClassType.Dictionary, writer);
             }
 
-            if (state.Current.Enumerator.MoveNext())
+            if (state.Current.CollectionEnumerator.MoveNext())
             {
                 // Check for polymorphism.
                 if (elementClassInfo.ClassType == ClassType.Unknown)
                 {
-                    object currentValue = ((IDictionaryEnumerator)state.Current.Enumerator).Entry.Value;
+                    object currentValue = ((IDictionaryEnumerator)state.Current.CollectionEnumerator).Entry.Value;
                     GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
                 }
 
                 if (elementClassInfo.ClassType == ClassType.Value)
                 {
-                    elementClassInfo.GetPolicyProperty().WriteDictionary(ref state, writer);
+                    elementClassInfo.PolicyProperty.WriteDictionary(ref state, writer);
                 }
-                else if (state.Current.Enumerator.Current == null)
+                else if (state.Current.CollectionEnumerator.Current == null)
                 {
                     writer.WriteNull(jsonPropertyInfo.Name);
                 }
                 else
                 {
                     // An object or another enumerator requires a new stack frame.
-                    var enumerator = (IDictionaryEnumerator)state.Current.Enumerator;
+                    var enumerator = (IDictionaryEnumerator)state.Current.CollectionEnumerator;
                     object value = enumerator.Value;
                     state.Push(elementClassInfo, value);
                     state.Current.KeyName = (string)enumerator.Key;
@@ -78,7 +78,7 @@ namespace System.Text.Json
             // We are done enumerating.
             writer.WriteEndObject();
 
-            if (state.Current.PopStackOnEnd)
+            if (state.Current.PopStackOnEndCollection)
             {
                 state.Pop();
             }
@@ -101,25 +101,25 @@ namespace System.Text.Json
                 return;
             }
 
-            Debug.Assert(current.Enumerator != null);
+            Debug.Assert(current.CollectionEnumerator != null);
 
             string key;
             TProperty value;
-            if (current.Enumerator is IEnumerator<KeyValuePair<string, TProperty>> enumerator)
+            if (current.CollectionEnumerator is IEnumerator<KeyValuePair<string, TProperty>> enumerator)
             {
                 // Avoid boxing for strongly-typed enumerators such as returned from IDictionary<string, TRuntimeProperty>
                 value = enumerator.Current.Value;
                 key = enumerator.Current.Key;
             }
-            else if (current.Enumerator is IEnumerator<KeyValuePair<string, object>> polymorphicEnumerator)
+            else if (current.CollectionEnumerator is IEnumerator<KeyValuePair<string, object>> polymorphicEnumerator)
             {
                 value = (TProperty)polymorphicEnumerator.Current.Value;
                 key = polymorphicEnumerator.Current.Key;
             }
             else if (current.IsIDictionaryConstructible || current.IsIDictionaryConstructibleProperty)
             {
-                value = (TProperty)((DictionaryEntry)current.Enumerator.Current).Value;
-                key = (string)((DictionaryEntry)current.Enumerator.Current).Key;
+                value = (TProperty)((DictionaryEntry)current.CollectionEnumerator.Current).Value;
+                key = (string)((DictionaryEntry)current.CollectionEnumerator.Current).Key;
             }
             else
             {
index f7f8b03..dfcfe98 100644 (file)
@@ -17,7 +17,7 @@ namespace System.Text.Json
         {
             Debug.Assert(state.Current.JsonPropertyInfo.ClassType == ClassType.Enumerable);
 
-            if (state.Current.Enumerator == null)
+            if (state.Current.CollectionEnumerator == null)
             {
                 IEnumerable enumerable = (IEnumerable)state.Current.JsonPropertyInfo.GetValueAsObject(state.Current.CurrentValue);
 
@@ -32,25 +32,25 @@ namespace System.Text.Json
                     return true;
                 }
 
-                state.Current.Enumerator = enumerable.GetEnumerator();
+                state.Current.CollectionEnumerator = enumerable.GetEnumerator();
 
                 state.Current.WriteObjectOrArrayStart(ClassType.Enumerable, writer);
             }
 
-            if (state.Current.Enumerator.MoveNext())
+            if (state.Current.CollectionEnumerator.MoveNext())
             {
                 // Check for polymorphism.
                 if (elementClassInfo.ClassType == ClassType.Unknown)
                 {
-                    object currentValue = state.Current.Enumerator.Current;
+                    object currentValue = state.Current.CollectionEnumerator.Current;
                     GetRuntimeClassInfo(currentValue, ref elementClassInfo, options);
                 }
 
                 if (elementClassInfo.ClassType == ClassType.Value)
                 {
-                    elementClassInfo.GetPolicyProperty().WriteEnumerable(ref state, writer);
+                    elementClassInfo.PolicyProperty.WriteEnumerable(ref state, writer);
                 }
-                else if (state.Current.Enumerator.Current == null)
+                else if (state.Current.CollectionEnumerator.Current == null)
                 {
                     // Write a null object or enumerable.
                     writer.WriteNullValue();
@@ -58,7 +58,7 @@ namespace System.Text.Json
                 else
                 {
                     // An object or another enumerator requires a new stack frame.
-                    object nextValue = state.Current.Enumerator.Current;
+                    object nextValue = state.Current.CollectionEnumerator.Current;
                     state.Push(elementClassInfo, nextValue);
                 }
 
@@ -68,7 +68,7 @@ namespace System.Text.Json
             // We are done enumerating.
             writer.WriteEndArray();
 
-            if (state.Current.PopStackOnEnd)
+            if (state.Current.PopStackOnEndCollection)
             {
                 state.Pop();
             }
index 97ab32e..afac8d3 100644 (file)
@@ -2,6 +2,7 @@
 // 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;
 
 namespace System.Text.Json
@@ -17,18 +18,18 @@ namespace System.Text.Json
             if (!state.Current.StartObjectWritten)
             {
                 state.Current.WriteObjectOrArrayStart(ClassType.Object, writer);
+                state.Current.PropertyEnumerator = state.Current.JsonClassInfo.PropertyCache.GetEnumerator();
+                state.Current.NextProperty();
             }
-
-            if (state.Current.MoveToNextProperty)
+            else 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. There is probably
-            // a better way to identify policy properties- maybe not put them in the normal property bag?
+            // If the ClassType is unknown, there will be a policy property applied
             JsonClassInfo classInfo = state.Current.JsonClassInfo;
-            if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyIndex != classInfo.PropertyCount)
+            if (classInfo.ClassType != ClassType.Unknown && state.Current.PropertyEnumeratorActive)
             {
                 HandleObject(options, writer, ref state);
                 return false;
@@ -57,7 +58,8 @@ namespace System.Text.Json
                 state.Current.JsonClassInfo.ClassType == ClassType.Object ||
                 state.Current.JsonClassInfo.ClassType == ClassType.Unknown);
 
-            JsonPropertyInfo jsonPropertyInfo = state.Current.JsonClassInfo.GetProperty(state.Current.PropertyIndex);
+            var kvp = (KeyValuePair<string, JsonPropertyInfo>)state.Current.PropertyEnumerator.Current;
+            JsonPropertyInfo jsonPropertyInfo = kvp.Value;
             if (!jsonPropertyInfo.ShouldSerialize)
             {
                 state.Current.MoveToNextProperty = true;
index 197724d..beb628d 100644 (file)
@@ -38,7 +38,7 @@ namespace System.Text.Json
 
         // For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
         public int PropertyIndex;
-        public HashSet<PropertyRef> PropertyRefCache;
+        public List<PropertyRef> PropertyRefCache;
 
         // The current JSON data for a property does not match a given POCO, so ignore the property (recursively).
         public bool Drain;
@@ -67,46 +67,52 @@ namespace System.Text.Json
         public bool IsProcessingIDictionaryConstructible => IsIDictionaryConstructible || IsIDictionaryConstructibleProperty;
         public bool IsProcessingEnumerable => IsEnumerable || IsEnumerableProperty;
 
-        public bool IsProcessingValue
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        public bool IsProcessingValue(JsonTokenType tokenType)
         {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get
+            if (SkipProperty)
             {
-                if (SkipProperty)
-                {
-                    return false;
-                }
+                return false;
+            }
 
-                if (CollectionPropertyInitialized)
-                {
-                    ClassType elementType;
-
-                    if (IsCollectionForProperty)
-                    {
-                        elementType = JsonPropertyInfo.ElementClassInfo.ClassType;
-                    }
-                    else
-                    {
-                        Debug.Assert(IsCollectionForClass);
-                        elementType = JsonClassInfo.ElementClassInfo.ClassType;
-                    }
+            // Handle array case.
+            if (CollectionPropertyInitialized)
+            {
+                ClassType elementType;
 
+                if (IsCollectionForClass)
+                {
+                    // A custom converter for a class (to handle JSON array).
+                    elementType = JsonClassInfo.ElementClassInfo.ClassType;
                     return (elementType == ClassType.Value || elementType == ClassType.Unknown);
                 }
 
-                ClassType type;
-                if (JsonPropertyInfo == null)
+                Debug.Assert(IsCollectionForProperty);
+
+                if (tokenType == JsonTokenType.StartObject)
                 {
-                    type = JsonClassInfo.ClassType;
+                    elementType = JsonPropertyInfo.ElementClassInfo.ClassType;
+                    return (elementType == ClassType.Value || elementType == ClassType.Unknown);
                 }
                 else
                 {
-                    type = JsonPropertyInfo.ClassType;
+                    // A custom converter for an array element is handled by IsProcessingValueOnStartObject.
+                    return false;
                 }
+            }
 
-                // If we're a Value or polymorphic Value (ClassType.Unknown), return true.
-                return type == ClassType.Value || type == ClassType.Unknown;
+            // Handle object case.
+            ClassType type;
+            if (JsonPropertyInfo == null)
+            {
+                type = JsonClassInfo.ClassType;
             }
+            else
+            {
+                type = JsonPropertyInfo.ClassType;
+            }
+
+            return type == ClassType.Value || type == ClassType.Unknown;
         }
 
         public void Initialize(Type type, JsonSerializerOptions options)
@@ -122,7 +128,7 @@ namespace System.Text.Json
                 JsonClassInfo.ClassType == ClassType.Dictionary ||
                 JsonClassInfo.ClassType == ClassType.IDictionaryConstructible)
             {
-                JsonPropertyInfo = JsonClassInfo.GetPolicyProperty();
+                JsonPropertyInfo = JsonClassInfo.PolicyProperty;
             }
         }
 
@@ -162,7 +168,7 @@ namespace System.Text.Json
                 IList converterList;
                 if (jsonPropertyInfo.ElementClassInfo.ClassType == ClassType.Value)
                 {
-                    converterList = jsonPropertyInfo.ElementClassInfo.GetPolicyProperty().CreateConverterList();
+                    converterList = jsonPropertyInfo.ElementClassInfo.PolicyProperty.CreateConverterList();
                 }
                 else
                 {
index 0b1943b..f19a607 100644 (file)
@@ -48,13 +48,13 @@ namespace System.Text.Json
 
             if (classType == ClassType.Enumerable || nextClassInfo.ClassType == ClassType.Dictionary)
             {
-                Current.PopStackOnEnd = true;
-                Current.JsonPropertyInfo = Current.JsonClassInfo.GetPolicyProperty();
+                Current.PopStackOnEndCollection = true;
+                Current.JsonPropertyInfo = Current.JsonClassInfo.PolicyProperty;
             }
             else if (classType == ClassType.IDictionaryConstructible)
             {
-                Current.PopStackOnEnd = true;
-                Current.JsonPropertyInfo = Current.JsonClassInfo.GetPolicyProperty();
+                Current.PopStackOnEndCollection = true;
+                Current.JsonPropertyInfo = Current.JsonClassInfo.PolicyProperty;
 
                 Current.IsIDictionaryConstructible = true;
             }
index 20fcd2d..ed0ab61 100644 (file)
@@ -16,39 +16,32 @@ namespace System.Text.Json
         // Support Dictionary keys.
         public string KeyName;
 
-        // Whether the current object can be constructed with IDictionary.
+        // The current IEnumerable or IDictionary.
+        public IEnumerator CollectionEnumerator;
+        public bool PopStackOnEndCollection;
         public bool IsIDictionaryConstructible;
         public bool IsIDictionaryConstructibleProperty;
 
-        // The current enumerator for the IEnumerable or IDictionary.
-        public IEnumerator Enumerator;
-
-        // Current property values.
-        public JsonPropertyInfo JsonPropertyInfo;
-
-        // The current property.
-        public int PropertyIndex;
-        public bool MoveToNextProperty;
-
-        // Has the Start tag been written.
+        // The current object.
+        public bool PopStackOnEndObject;
         public bool StartObjectWritten;
+        public bool MoveToNextProperty;
 
-        // Pop the stack when the current array or dictionary is done.
-        public bool PopStackOnEnd;
-
-        // Pop the stack when the current object is done.
-        public bool PopStackOnEndObject;
+        // The current property.
+        public IEnumerator PropertyEnumerator;
+        public bool PropertyEnumeratorActive;
+        public JsonPropertyInfo JsonPropertyInfo;
 
         public void Initialize(Type type, JsonSerializerOptions options)
         {
             JsonClassInfo = options.GetOrAddClass(type);
             if (JsonClassInfo.ClassType == ClassType.Value || JsonClassInfo.ClassType == ClassType.Enumerable || JsonClassInfo.ClassType == ClassType.Dictionary)
             {
-                JsonPropertyInfo = JsonClassInfo.GetPolicyProperty();
+                JsonPropertyInfo = JsonClassInfo.PolicyProperty;
             }
             else if (JsonClassInfo.ClassType == ClassType.IDictionaryConstructible)
             {
-                JsonPropertyInfo = JsonClassInfo.GetPolicyProperty();
+                JsonPropertyInfo = JsonClassInfo.PolicyProperty;
                 IsIDictionaryConstructible = true;
             }
         }
@@ -105,37 +98,37 @@ namespace System.Text.Json
         public void Reset()
         {
             CurrentValue = null;
-            Enumerator = null;
+            CollectionEnumerator = null;
             KeyName = null;
             JsonClassInfo = null;
             JsonPropertyInfo = null;
-            PropertyIndex = 0;
             IsIDictionaryConstructible = false;
             MoveToNextProperty = false;
             PopStackOnEndObject = false;
-            PopStackOnEnd = false;
+            PopStackOnEndCollection = false;
             StartObjectWritten = false;
         }
 
         public void EndObject()
         {
-            PropertyIndex = 0;
-            MoveToNextProperty = false;
-            PopStackOnEndObject = false;
             IsIDictionaryConstructibleProperty = false;
             JsonPropertyInfo = null;
+            MoveToNextProperty = false;
+            PopStackOnEndObject = false;
+            PropertyEnumerator = null;
+            PropertyEnumeratorActive = false;
         }
 
         public void EndDictionary()
         {
-            Enumerator = null;
-            PopStackOnEnd = false;
+            CollectionEnumerator = null;
+            PopStackOnEndCollection = false;
         }
 
         public void EndArray()
         {
-            Enumerator = null;
-            PopStackOnEnd = false;
+            CollectionEnumerator = null;
+            PopStackOnEndCollection = false;
             JsonPropertyInfo = null;
         }
 
@@ -143,7 +136,7 @@ namespace System.Text.Json
         {
             JsonPropertyInfo = null;
             MoveToNextProperty = false;
-            PropertyIndex++;
+            PropertyEnumeratorActive = PropertyEnumerator.MoveNext();
         }
     }
 }
index 5cf0c49..319bbc5 100644 (file)
@@ -88,7 +88,7 @@ namespace System.Text.Json.Serialization.Tests
         }
 
         /// <summary>
-        /// A converter that converters True and False to a bool.
+        /// A converter that converts "true" and "false" tokens to a bool.
         /// </summary>
         private class ObjectToBoolConverter : JsonConverter<object>
         {