From e4d9e266b7b06890037a32ac4e79d54da8dba802 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 8 Jul 2024 21:16:32 +0100 Subject: [PATCH] Clean up JSON property lookup logic and add alternate key lookup support. (#103836) * Clean up JSON property lookup logic and add alternate lookup support. * Ensure PropertyRef cache doesn't contain duplicates. * Remove usings. * Revert back to using original caching algorithm. * Incorporate suggestions to key generation algorithm. * Address feedback. * Simplify more PropertyRef methods. --- .../src/System.Text.Json.csproj | 1 + .../src/System/Text/Json/JsonHelpers.cs | 36 +++ .../Object/ObjectDefaultConverter.cs | 16 +- ...ctWithParameterizedConstructorConverter.cs | 10 +- .../JsonSerializer.Read.HandlePropertyName.cs | 12 +- .../Metadata/JsonPropertyInfo.cs | 4 +- .../Metadata/JsonTypeInfo.Cache.cs | 264 ++++-------------- .../Serialization/Metadata/JsonTypeInfo.cs | 34 ++- .../Serialization/Metadata/PropertyRef.cs | 97 ++++++- .../Metadata/PropertyRefCacheBuilder.cs | 36 +++ .../Text/Json/Serialization/ReadStackFrame.cs | 4 +- 11 files changed, 268 insertions(+), 246 deletions(-) create mode 100644 src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 0fb5fcd5acf..a38e38ceb6a 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -158,6 +158,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index 760d169f35c..9547edf23b0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -6,6 +6,7 @@ using System.Buffers.Text; using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -213,6 +214,41 @@ namespace System.Text.Json #endif } + public static bool TryLookupUtf8Key( + this Dictionary dictionary, + ReadOnlySpan utf8Key, + [MaybeNullWhen(false)] out TValue result) + { +#if NET9_0_OR_GREATER + Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer, string>); + + Dictionary.AlternateLookup> spanLookup = + dictionary.GetAlternateLookup>(); + + char[]? rentedBuffer = null; + + Span charBuffer = utf8Key.Length <= JsonConstants.StackallocCharThreshold ? + stackalloc char[JsonConstants.StackallocCharThreshold] : + (rentedBuffer = ArrayPool.Shared.Rent(utf8Key.Length)); + + int charsWritten = Encoding.UTF8.GetChars(utf8Key, charBuffer); + Span decodedKey = charBuffer[0..charsWritten]; + + bool success = spanLookup.TryGetValue(decodedKey, out result); + + if (rentedBuffer != null) + { + decodedKey.Clear(); + ArrayPool.Shared.Return(rentedBuffer); + } + + return success; +#else + string key = Utf8GetString(utf8Key); + return dictionary.TryGetValue(key, out result); +#endif + } + /// /// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index c235fa45339..ee374daf4ec 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -171,6 +171,7 @@ namespace System.Text.Json.Serialization.Converters // Read method would have thrown if otherwise. Debug.Assert(tokenType == JsonTokenType.PropertyName); + jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization(); ReadOnlySpan unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty); if (isAlreadyReadMetadataProperty) { @@ -185,7 +186,6 @@ namespace System.Text.Json.Serialization.Converters unescapedPropertyName, ref state, options, - out byte[] _, out bool useExtensionProperty); state.Current.UseExtensionProperty = useExtensionProperty; @@ -257,10 +257,10 @@ namespace System.Text.Json.Serialization.Converters Debug.Assert(obj != null); value = (T)obj; - // Check if we are trying to build the sorted cache. - if (state.Current.PropertyRefCache != null) + // Check if we are trying to update the UTF-8 property cache. + if (state.Current.PropertyRefCacheBuilder != null) { - jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); + jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current); } return true; @@ -292,12 +292,12 @@ namespace System.Text.Json.Serialization.Converters ReadOnlySpan unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty); Debug.Assert(!isAlreadyReadMetadataProperty, "Only possible for types that can read metadata, which do not call into the fast-path method."); + jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization(); JsonPropertyInfo jsonPropertyInfo = JsonSerializer.LookupProperty( obj, unescapedPropertyName, ref state, options, - out byte[] _, out bool useExtensionProperty); ReadPropertyValue(obj, ref state, ref reader, jsonPropertyInfo, useExtensionProperty); @@ -306,10 +306,10 @@ namespace System.Text.Json.Serialization.Converters jsonTypeInfo.OnDeserialized?.Invoke(obj); state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo); - // Check if we are trying to build the sorted cache. - if (state.Current.PropertyRefCache != null) + // Check if we are trying to update the UTF-8 property cache. + if (state.Current.PropertyRefCacheBuilder != null) { - jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); + jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index d0d357903fc..6ae225b5745 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -255,10 +255,10 @@ namespace System.Text.Json.Serialization.Converters Debug.Assert(obj != null); value = (T)obj; - // Check if we are trying to build the sorted cache. - if (state.Current.PropertyRefCache != null) + // Check if we are trying to update the UTF-8 property cache. + if (state.Current.PropertyRefCacheBuilder != null) { - state.Current.JsonTypeInfo.UpdateSortedPropertyCache(ref state.Current); + jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current); } return true; @@ -603,13 +603,9 @@ namespace System.Text.Json.Serialization.Converters unescapedPropertyName, ref state, options, - out byte[] utf8PropertyName, out bool useExtensionProperty, createExtensionProperty: false); - // For case insensitive and missing property support of JsonPath, remember the value on the temporary stack. - state.Current.JsonPropertyName = utf8PropertyName; - jsonParameterInfo = jsonPropertyInfo.AssociatedParameter; if (jsonParameterInfo != null) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index cf26e662c33..64a9de11e89 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -21,17 +21,16 @@ namespace System.Text.Json ReadOnlySpan unescapedPropertyName, ref ReadStack state, JsonSerializerOptions options, - out byte[] utf8PropertyName, out bool useExtensionProperty, bool createExtensionProperty = true) { JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; useExtensionProperty = false; - JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.GetProperty( + JsonPropertyInfo? jsonPropertyInfo = jsonTypeInfo.GetProperty( unescapedPropertyName, ref state.Current, - out utf8PropertyName); + out byte[] utf8PropertyName); // Increment PropertyIndex so GetProperty() checks the next property first when called again. state.Current.PropertyIndex++; @@ -40,7 +39,7 @@ namespace System.Text.Json state.Current.JsonPropertyName = utf8PropertyName; // Handle missing properties - if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty) + if (jsonPropertyInfo is null) { if (jsonTypeInfo.EffectiveUnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow) { @@ -63,6 +62,11 @@ namespace System.Text.Json jsonPropertyInfo = dataExtProperty; useExtensionProperty = true; } + else + { + // Populate with a placeholder value required by JsonPath calculations + jsonPropertyInfo = JsonPropertyInfo.s_missingProperty; + } } state.Current.JsonPropertyInfo = jsonPropertyInfo; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 0adea37c3c6..4077c89fe97 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -800,12 +800,12 @@ namespace System.Text.Json.Serialization.Metadata /// /// Utf8 version of Name. /// - internal byte[] NameAsUtf8Bytes { get; set; } = null!; + internal byte[] NameAsUtf8Bytes { get; private set; } = null!; /// /// The escaped name passed to the writer. /// - internal byte[] EscapedNameSection { get; set; } = null!; + internal byte[] EscapedNameSection { get; private set; } = null!; /// /// Gets the value associated with the current contract instance. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index ef778611fbd..13f0821f8b3 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -3,10 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Text.Json.Reflection; namespace System.Text.Json.Serialization.Metadata { @@ -17,14 +14,6 @@ namespace System.Text.Json.Serialization.Metadata /// internal static readonly Type ObjectType = typeof(object); - // The length of the property name embedded in the key (in bytes). - // The key is a ulong (8 bytes) containing the first 7 bytes of the property name - // followed by a byte representing the length. - private const int PropertyNameKeyLength = 7; - - // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache. - private const int PropertyNameCountCacheThreshold = 64; - // The number of parameters the deserialization constructor has. If this is not equal to ParameterCache.Count, this means // that not all parameters are bound to object properties, and an exception will be thrown if deserialization is attempted. internal int ParameterCount { get; private protected set; } @@ -74,64 +63,38 @@ namespace System.Text.Json.Serialization.Metadata private Dictionary? _propertyIndex; - // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache. - // Use an array (instead of List) for highest performance. - private volatile PropertyRef[]? _propertyRefsSorted; - - [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] - [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType, Type? declaringType) - { - JsonPropertyInfo jsonPropertyInfo; - - if (Options.TryGetTypeInfoCached(propertyType, out JsonTypeInfo? jsonTypeInfo)) - { - // If a JsonTypeInfo has already been cached for the property type, - // avoid reflection-based initialization by delegating construction - // of JsonPropertyInfo construction to the property type metadata. - jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, declaringType, Options); - } - else - { - // Metadata for `propertyType` has not been registered yet. - // Use reflection to instantiate the correct JsonPropertyInfo - Type propertyInfoType = typeof(JsonPropertyInfo<>).MakeGenericType(propertyType); - jsonPropertyInfo = (JsonPropertyInfo)propertyInfoType.CreateInstanceNoWrapExceptions( - parameterTypes: new Type[] { typeof(Type), typeof(JsonTypeInfo), typeof(JsonSerializerOptions) }, - parameters: new object[] { declaringType ?? Type, this, Options })!; - } - - Debug.Assert(jsonPropertyInfo.PropertyType == propertyType); - return jsonPropertyInfo; - } + /// + /// Stores a cache of UTF-8 encoded property names and their associated JsonPropertyInfo, if available. + /// Consulted before the lookup to avoid added allocations and decoding costs. + /// The cache is grown on-demand appending encountered unbounded properties or alternative casings. + /// + private PropertyRef[] _utf8PropertyCache = []; /// - /// Creates a JsonPropertyInfo whose property type matches the type of this JsonTypeInfo instance. + /// Defines the core property lookup logic for a given unescaped UTF-8 encoded property name. /// - private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, Type? declaringType, JsonSerializerOptions options); - - // AggressiveInlining used although a large method it is only called from one location and is on a hot path. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal JsonPropertyInfo GetProperty( - ReadOnlySpan propertyName, - ref ReadStackFrame frame, - out byte[] utf8PropertyName) + internal JsonPropertyInfo? GetProperty(ReadOnlySpan propertyName, ref ReadStackFrame frame, out byte[] utf8PropertyName) { - PropertyRef propertyRef; + Debug.Assert(IsConfigured); - ValidateCanBeUsedForPropertyMetadataSerialization(); - ulong key = GetKey(propertyName); + // The logic can be broken up into roughly three stages: + // 1. Look up the UTF-8 property cache for potential exact matches in the encoding. + // 2. If no match is found, decode to UTF-16 and look up the primary dictionary. + // 3. Store the new result for potential inclusion to the UTF-8 cache once deserialization is complete. - // Keep a local copy of the cache in case it changes by another thread. - PropertyRef[]? localPropertyRefsSorted = _propertyRefsSorted; + PropertyRef[] utf8PropertyCache = _utf8PropertyCache; // Keep a local copy of the cache in case it changes by another thread. + ReadOnlySpan utf8PropertyCacheSpan = utf8PropertyCache; + ulong key = PropertyRef.GetKey(propertyName); - // If there is an existing cache, then use it. - if (localPropertyRefsSorted != null) + if (!utf8PropertyCacheSpan.IsEmpty) { + PropertyRef propertyRef; + // Start with the current property index, and then go forwards\backwards. int propertyIndex = frame.PropertyIndex; - int count = localPropertyRefsSorted.Length; + int count = utf8PropertyCacheSpan.Length; int iForward = Math.Min(propertyIndex, count); int iBackward = iForward - 1; @@ -139,10 +102,10 @@ namespace System.Text.Json.Serialization.Metadata { if (iForward < count) { - propertyRef = localPropertyRefsSorted[iForward]; - if (IsPropertyRefEqual(propertyRef, propertyName, key)) + propertyRef = utf8PropertyCacheSpan[iForward]; + if (propertyRef.Equals(propertyName, key)) { - utf8PropertyName = propertyRef.NameFromJson; + utf8PropertyName = propertyRef.Utf8PropertyName; return propertyRef.Info; } @@ -150,10 +113,10 @@ namespace System.Text.Json.Serialization.Metadata if (iBackward >= 0) { - propertyRef = localPropertyRefsSorted[iBackward]; - if (IsPropertyRefEqual(propertyRef, propertyName, key)) + propertyRef = utf8PropertyCacheSpan[iBackward]; + if (propertyRef.Equals(propertyName, key)) { - utf8PropertyName = propertyRef.NameFromJson; + utf8PropertyName = propertyRef.Utf8PropertyName; return propertyRef.Info; } @@ -162,10 +125,10 @@ namespace System.Text.Json.Serialization.Metadata } else if (iBackward >= 0) { - propertyRef = localPropertyRefsSorted[iBackward]; - if (IsPropertyRefEqual(propertyRef, propertyName, key)) + propertyRef = utf8PropertyCacheSpan[iBackward]; + if (propertyRef.Equals(propertyName, key)) { - utf8PropertyName = propertyRef.NameFromJson; + utf8PropertyName = propertyRef.Utf8PropertyName; return propertyRef.Info; } @@ -180,176 +143,49 @@ namespace System.Text.Json.Serialization.Metadata } // No cached item was found. Try the main dictionary which has all of the properties. - if (PropertyIndex.TryGetValue(JsonHelpers.Utf8GetString(propertyName), out JsonPropertyInfo? info)) + if (PropertyIndex.TryLookupUtf8Key(propertyName, out JsonPropertyInfo? info) && + (!Options.PropertyNameCaseInsensitive || propertyName.SequenceEqual(info.NameAsUtf8Bytes))) { - Debug.Assert(info != null, "PropertyCache contains null JsonPropertyInfo"); - - if (Options.PropertyNameCaseInsensitive) - { - if (propertyName.SequenceEqual(info.NameAsUtf8Bytes)) - { - // Use the existing byte[] reference instead of creating another one. - utf8PropertyName = info.NameAsUtf8Bytes!; - } - else - { - // Make a copy of the original Span. - utf8PropertyName = propertyName.ToArray(); - } - } - else - { - utf8PropertyName = info.NameAsUtf8Bytes; - } + // We have an exact match in UTF8 encoding. + utf8PropertyName = info.NameAsUtf8Bytes; } else { - info = JsonPropertyInfo.s_missingProperty; - // Make a copy of the original Span. utf8PropertyName = propertyName.ToArray(); } - // Check if we should add this to the cache. - // Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache. - int cacheCount = 0; - if (localPropertyRefsSorted != null) + // Assuming there is capacity, store the new result for potential + // inclusion to the UTF-8 cache once deserialization is complete. + + ref PropertyRefCacheBuilder? cacheBuilder = ref frame.PropertyRefCacheBuilder; + if ((cacheBuilder?.TotalCount ?? utf8PropertyCache.Length) < PropertyRefCacheBuilder.MaxCapacity) { - cacheCount = localPropertyRefsSorted.Length; - } - - // Do a quick check for the stable (after warm-up) case. - if (cacheCount < PropertyNameCountCacheThreshold) - { - // Do a slower check for the warm-up case. - if (frame.PropertyRefCache != null) - { - cacheCount += frame.PropertyRefCache.Count; - } - - // Check again to append the cache up to the threshold. - if (cacheCount < PropertyNameCountCacheThreshold) - { - frame.PropertyRefCache ??= new List(); - - Debug.Assert(info != null); - - propertyRef = new PropertyRef(key, info, utf8PropertyName); - frame.PropertyRefCache.Add(propertyRef); - } + (cacheBuilder ??= new(utf8PropertyCache)).TryAdd(new(key, info, utf8PropertyName)); } return info; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan propertyName, ulong key) - { - if (key == propertyRef.Key) - { - // We compare the whole name, although we could skip the first 7 bytes (but it's not any faster) - if (propertyName.Length <= PropertyNameKeyLength || - propertyName.SequenceEqual(propertyRef.NameFromJson)) - { - return true; - } - } - - return false; - } - /// - /// Get a key from the property name. - /// The key consists of the first 7 bytes of the property name and then the length. + /// Attempts to update the UTF-8 property cache with the results gathered in the current deserialization operation. + /// The update operation is done optimistically and results are discarded if the cache was updated by another thread. /// - // AggressiveInlining used since this method is only called from two locations and is on a hot path. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong GetKey(ReadOnlySpan name) + internal void UpdateUtf8PropertyCache(ref ReadStackFrame frame) { - ulong key; + Debug.Assert(frame.PropertyRefCacheBuilder is { Count: > 0 }); - ref byte reference = ref MemoryMarshal.GetReference(name); - int length = name.Length; + PropertyRef[]? currentCache = _utf8PropertyCache; + PropertyRefCacheBuilder cacheBuilder = frame.PropertyRefCacheBuilder; - if (length > 7) + if (currentCache == cacheBuilder.OriginalCache) { - key = Unsafe.ReadUnaligned(ref reference) & 0x00ffffffffffffffL; - key |= (ulong)Math.Min(length, 0xff) << 56; - } - else - { - key = - length > 5 ? Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32 : - length > 3 ? Unsafe.ReadUnaligned(ref reference) : - length > 1 ? Unsafe.ReadUnaligned(ref reference) : 0UL; - key |= (ulong)length << 56; - - if ((length & 1) != 0) - { - var offset = length - 1; - key |= (ulong)Unsafe.Add(ref reference, offset) << (offset * 8); - } + PropertyRef[] newCache = cacheBuilder.ToArray(); + Debug.Assert(newCache.Length <= PropertyRefCacheBuilder.MaxCapacity); + _utf8PropertyCache = cacheBuilder.ToArray(); } -#if DEBUG - // Verify key contains the embedded bytes as expected. - // Note: the expected properties do not hold true on big-endian platforms - if (BitConverter.IsLittleEndian) - { - const int BitsInByte = 8; - Debug.Assert( - // Verify embedded property name. - (name.Length < 1 || name[0] == ((key & ((ulong)0xFF << BitsInByte * 0)) >> BitsInByte * 0)) && - (name.Length < 2 || name[1] == ((key & ((ulong)0xFF << BitsInByte * 1)) >> BitsInByte * 1)) && - (name.Length < 3 || name[2] == ((key & ((ulong)0xFF << BitsInByte * 2)) >> BitsInByte * 2)) && - (name.Length < 4 || name[3] == ((key & ((ulong)0xFF << BitsInByte * 3)) >> BitsInByte * 3)) && - (name.Length < 5 || name[4] == ((key & ((ulong)0xFF << BitsInByte * 4)) >> BitsInByte * 4)) && - (name.Length < 6 || name[5] == ((key & ((ulong)0xFF << BitsInByte * 5)) >> BitsInByte * 5)) && - (name.Length < 7 || name[6] == ((key & ((ulong)0xFF << BitsInByte * 6)) >> BitsInByte * 6)) && - // Verify embedded length. - (name.Length >= 0xFF || (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == (ulong)name.Length) && - (name.Length < 0xFF || (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == 0xFF), - "Embedded bytes not as expected"); - } -#endif - - return key; - } - - internal void UpdateSortedPropertyCache(ref ReadStackFrame frame) - { - Debug.Assert(frame.PropertyRefCache != null); - - // frame.PropertyRefCache is only read\written by a single thread -- the thread performing - // the deserialization for a given object instance. - - List listToAppend = frame.PropertyRefCache; - - // _propertyRefsSorted can be accessed by multiple threads, so replace the reference when - // appending to it. No lock() is necessary. - - if (_propertyRefsSorted != null) - { - List replacementList = new List(_propertyRefsSorted); - Debug.Assert(replacementList.Count <= PropertyNameCountCacheThreshold); - - // Verify replacementList will not become too large. - while (replacementList.Count + listToAppend.Count > PropertyNameCountCacheThreshold) - { - // This code path is rare; keep it simple by using RemoveAt() instead of RemoveRange() which requires calculating index\count. - listToAppend.RemoveAt(listToAppend.Count - 1); - } - - // Add the new items; duplicates are possible but that is tolerated during property lookup. - replacementList.AddRange(listToAppend); - _propertyRefsSorted = replacementList.ToArray(); - } - else - { - _propertyRefsSorted = listToAppend.ToArray(); - } - - frame.PropertyRefCache = null; + frame.PropertyRefCacheBuilder = null; } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index f337adcb567..b647ea4384b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -966,7 +966,7 @@ namespace System.Text.Json.Serialization.Metadata { Type jsonTypeInfoType = typeof(JsonTypeInfo<>).MakeGenericType(type); jsonTypeInfo = (JsonTypeInfo)jsonTypeInfoType.CreateInstanceNoWrapExceptions( - parameterTypes: new Type[] { typeof(JsonConverter), typeof(JsonSerializerOptions) }, + parameterTypes: [typeof(JsonConverter), typeof(JsonSerializerOptions)], parameters: new object[] { converter, options })!; } @@ -1009,6 +1009,38 @@ namespace System.Text.Json.Serialization.Metadata return propertyInfo; } + [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)] + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] + internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType, Type? declaringType) + { + JsonPropertyInfo jsonPropertyInfo; + + if (Options.TryGetTypeInfoCached(propertyType, out JsonTypeInfo? jsonTypeInfo)) + { + // If a JsonTypeInfo has already been cached for the property type, + // avoid reflection-based initialization by delegating construction + // of JsonPropertyInfo construction to the property type metadata. + jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, declaringType, Options); + } + else + { + // Metadata for `propertyType` has not been registered yet. + // Use reflection to instantiate the correct JsonPropertyInfo + Type propertyInfoType = typeof(JsonPropertyInfo<>).MakeGenericType(propertyType); + jsonPropertyInfo = (JsonPropertyInfo)propertyInfoType.CreateInstanceNoWrapExceptions( + parameterTypes: [typeof(Type), typeof(JsonTypeInfo), typeof(JsonSerializerOptions)], + parameters: new object[] { declaringType ?? Type, this, Options })!; + } + + Debug.Assert(jsonPropertyInfo.PropertyType == propertyType); + return jsonPropertyInfo; + } + + /// + /// Creates a JsonPropertyInfo whose property type matches the type of this JsonTypeInfo instance. + /// + private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, Type? declaringType, JsonSerializerOptions options); + private protected Dictionary? _parameterInfoValuesIndex; // Untyped, root-level serialization methods diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs index a3e0a2aa4af..d1f4c0afaf8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs @@ -1,21 +1,100 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + namespace System.Text.Json.Serialization.Metadata { - internal readonly struct PropertyRef + /// + /// Represents a UTF-8 encoded JSON property name and its associated , if available. + /// PropertyRefs use byte sequence equality, so equal JSON strings with alternate encodings or casings are not equal. + /// Used as a first-level cache for property lookups before falling back to UTF decoding and string comparison. + /// + internal readonly struct PropertyRef(ulong key, JsonPropertyInfo? info, byte[] utf8PropertyName) : IEquatable { - public PropertyRef(ulong key, JsonPropertyInfo info, byte[] nameFromJson) + // The length of the property name embedded in the key (in bytes). + // The key is a ulong (8 bytes) containing the first 7 bytes of the property name + // followed by a byte representing the length. + private const int PropertyNameKeyLength = 7; + + /// + /// A custom hashcode produced from the UTF-8 encoded property name. + /// + public readonly ulong Key = key; + + /// + /// The associated with the property name, if available. + /// + public readonly JsonPropertyInfo? Info = info; + + /// + /// Caches a heap allocated copy of the UTF-8 encoded property name. + /// + public readonly byte[] Utf8PropertyName = utf8PropertyName; + + public bool Equals(PropertyRef other) => Equals(other.Utf8PropertyName, other.Key); + public override bool Equals(object? obj) => obj is PropertyRef other && Equals(other); + public override int GetHashCode() => Key.GetHashCode(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(ReadOnlySpan propertyName, ulong key) { - Key = key; - Info = info; - NameFromJson = nameFromJson; + // If the property name is less than 8 bytes, it is embedded in the key so no further comparison is necessary. + return key == Key && (propertyName.Length <= PropertyNameKeyLength || propertyName.SequenceEqual(Utf8PropertyName)); } - public readonly ulong Key; - public readonly JsonPropertyInfo Info; + /// + /// Get a key from the property name. + /// The key consists of the first 7 bytes of the property name and then the least significant bits of the length. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong GetKey(ReadOnlySpan name) + { + ref byte reference = ref MemoryMarshal.GetReference(name); + int length = name.Length; + ulong key = (ulong)(byte)length << 56; - // NameFromJson may be different than Info.NameAsUtf8Bytes when case insensitive is enabled. - public readonly byte[] NameFromJson; + switch (length) + { + case 0: goto ComputedKey; + case 1: goto OddLength; + case 2: key |= Unsafe.ReadUnaligned(ref reference); goto ComputedKey; + case 3: key |= Unsafe.ReadUnaligned(ref reference); goto OddLength; + case 4: key |= Unsafe.ReadUnaligned(ref reference); goto ComputedKey; + case 5: key |= Unsafe.ReadUnaligned(ref reference); goto OddLength; + case 6: key |= Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32; goto ComputedKey; + case 7: key |= Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32; goto OddLength; + default: key |= Unsafe.ReadUnaligned(ref reference) & 0x00ffffffffffffffL; goto ComputedKey; + } + + OddLength: + int offset = length - 1; + key |= (ulong)Unsafe.Add(ref reference, offset) << (offset * 8); + + ComputedKey: +#if DEBUG + // Verify key contains the embedded bytes as expected. + // Note: the expected properties do not hold true on big-endian platforms + if (BitConverter.IsLittleEndian) + { + const int BitsInByte = 8; + Debug.Assert( + // Verify embedded property name. + (name.Length < 1 || name[0] == ((key & ((ulong)0xFF << BitsInByte * 0)) >> BitsInByte * 0)) && + (name.Length < 2 || name[1] == ((key & ((ulong)0xFF << BitsInByte * 1)) >> BitsInByte * 1)) && + (name.Length < 3 || name[2] == ((key & ((ulong)0xFF << BitsInByte * 2)) >> BitsInByte * 2)) && + (name.Length < 4 || name[3] == ((key & ((ulong)0xFF << BitsInByte * 3)) >> BitsInByte * 3)) && + (name.Length < 5 || name[4] == ((key & ((ulong)0xFF << BitsInByte * 4)) >> BitsInByte * 4)) && + (name.Length < 6 || name[5] == ((key & ((ulong)0xFF << BitsInByte * 5)) >> BitsInByte * 5)) && + (name.Length < 7 || name[6] == ((key & ((ulong)0xFF << BitsInByte * 6)) >> BitsInByte * 6)) && + // Verify embedded length. + (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == (byte)name.Length, + "Embedded bytes not as expected"); + } +#endif + return key; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs new file mode 100644 index 00000000000..7aacd45beb5 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace System.Text.Json.Serialization.Metadata +{ + /// + /// Defines builder type for constructing updated caches. + /// + internal sealed class PropertyRefCacheBuilder(PropertyRef[] originalCache) + { + public const int MaxCapacity = 64; + private readonly List _propertyRefs = []; + private readonly HashSet _added = []; + + /// + /// Stores a reference to the original cache off which the current list is being built. + /// + public readonly PropertyRef[] OriginalCache = originalCache; + public int Count => _propertyRefs.Count; + public int TotalCount => OriginalCache.Length + _propertyRefs.Count; + public PropertyRef[] ToArray() => [.. OriginalCache, .. _propertyRefs]; + + public void TryAdd(PropertyRef propertyRef) + { + Debug.Assert(TotalCount < MaxCapacity, "Should have been checked by the caller."); + + if (_added.Add(propertyRef)) + { + _propertyRefs.Add(propertyRef); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs index 065e03ed4c6..0c973ea2b23 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs @@ -65,7 +65,9 @@ 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 List? PropertyRefCache; + + // Tracks newly encounentered UTF-8 encoded properties during the current deserialization, to be appended to the cache. + public PropertyRefCacheBuilder? PropertyRefCacheBuilder; // Holds relevant state when deserializing objects with parameterized constructors. public ArgumentState? CtorArgumentState;