using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Converters;
internal sealed partial class JsonClassInfo
{
// The length of the property name embedded in the key (in bytes).
- private const int PropertyNameKeyLength = 6;
+ // 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;
return property;
}
+ // AggressiveInlining used although a large method it is only called from one location and is on a hot path.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
{
JsonPropertyInfo info = null;
// Keep a local copy of the cache in case it changes by another thread.
PropertyRef[] localPropertyRefsSorted = _propertyRefsSorted;
+ ulong key = GetKey(propertyName);
+
// If there is an existing cache, then use it.
if (localPropertyRefsSorted != null)
{
- ulong key = GetKey(propertyName);
-
// Start with the current property index, and then go forwards\backwards.
int propertyIndex = frame.PropertyIndex;
int iForward = Math.Min(propertyIndex, count);
int iBackward = iForward - 1;
- while (iForward < count || iBackward >= 0)
+ while (true)
{
if (iForward < count)
{
- if (TryIsPropertyRefEqual(localPropertyRefsSorted[iForward], propertyName, key, ref info))
+ PropertyRef propertyRef = localPropertyRefsSorted[iForward];
+ if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
{
return info;
}
+
++iForward;
- }
- if (iBackward >= 0)
+ if (iBackward >= 0)
+ {
+ propertyRef = localPropertyRefsSorted[iBackward];
+ if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
+ {
+ return info;
+ }
+
+ --iBackward;
+ }
+ }
+ else if (iBackward >= 0)
{
- if (TryIsPropertyRefEqual(localPropertyRefsSorted[iBackward], propertyName, key, ref info))
+ PropertyRef propertyRef = localPropertyRefsSorted[iBackward];
+ if (TryIsPropertyRefEqual(propertyRef, propertyName, key, ref info))
{
return info;
}
+
--iBackward;
}
+ else
+ {
+ // Property was not found.
+ break;
+ }
}
}
// No cached item was found. Try the main list which has all of the properties.
string stringPropertyName = JsonHelpers.Utf8GetString(propertyName);
- if (PropertyCache.TryGetValue(stringPropertyName, out info))
+ if (!PropertyCache.TryGetValue(stringPropertyName, out info))
{
- // 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 count;
- if (localPropertyRefsSorted != null)
- {
- count = localPropertyRefsSorted.Length;
- }
- else
+ info = JsonPropertyInfo.s_missingProperty;
+ }
+
+ Debug.Assert(info != null);
+
+ // Three code paths to get here:
+ // 1) info == s_missingProperty. Property not found.
+ // 2) key == info.PropertyNameKey. Exact match found.
+ // 3) key != info.PropertyNameKey. Match found due to case insensitivity.
+ Debug.Assert(info == JsonPropertyInfo.s_missingProperty || key == info.PropertyNameKey || Options.PropertyNameCaseInsensitive);
+
+ // 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)
+ {
+ 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)
{
- count = 0;
+ cacheCount += frame.PropertyRefCache.Count;
}
- // Do a quick check for the stable (after warm-up) case.
- if (count < PropertyNameCountCacheThreshold)
+ // Check again to append the cache up to the threshold.
+ if (cacheCount < PropertyNameCountCacheThreshold)
{
- // Do a slower check for the warm-up case.
- if (frame.PropertyRefCache != null)
+ if (frame.PropertyRefCache == null)
{
- count += frame.PropertyRefCache.Count;
+ frame.PropertyRefCache = new List<PropertyRef>();
}
- // Check again to append the cache up to the threshold.
- 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);
- }
+ PropertyRef propertyRef = new PropertyRef(key, info);
+ frame.PropertyRefCache.Add(propertyRef);
}
}
public JsonPropertyInfo PolicyProperty { get; private set; }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryIsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
{
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 ||
- // We compare the whole name, although we could skip the first 6 bytes (but it's likely not any faster)
propertyName.SequenceEqual(propertyRef.Info.Name))
{
info = propertyRef.Info;
return false;
}
- private static bool IsPropertyRefEqual(ref PropertyRef propertyRef, PropertyRef other)
- {
- if (propertyRef.Key == other.Key)
- {
- if (propertyRef.Info.Name.Length <= PropertyNameKeyLength ||
- propertyRef.Info.Name.AsSpan().SequenceEqual(other.Info.Name.AsSpan()))
- {
- return true;
- }
- }
-
- return false;
- }
-
+ /// <summary>
+ /// Get a key from the property name.
+ /// The key consists of the first 7 bytes of the property name and then the length.
+ /// </summary>
public static ulong GetKey(ReadOnlySpan<byte> propertyName)
{
+ const int BitsInByte = 8;
ulong key;
int length = propertyName.Length;
- // Embed the propertyName in the first 6 bytes of the key.
- if (length > 3)
+ if (length > 7)
+ {
+ key = MemoryMarshal.Read<ulong>(propertyName);
+
+ // Max out the length byte.
+ // The comparison logic tests for equality against the full contents instead of just
+ // the key if the property name length is >7.
+ key |= 0xFF00000000000000;
+ }
+ else if (length > 3)
{
key = MemoryMarshal.Read<uint>(propertyName);
- if (length > 4)
+
+ if (length == 7)
+ {
+ key |= (ulong) propertyName[6] << (6 * BitsInByte)
+ | (ulong) propertyName[5] << (5 * BitsInByte)
+ | (ulong) propertyName[4] << (4 * BitsInByte)
+ | (ulong) 7 << (7 * BitsInByte);
+ }
+ else if (length == 6)
+ {
+ key |= (ulong) propertyName[5] << (5 * BitsInByte)
+ | (ulong) propertyName[4] << (4 * BitsInByte)
+ | (ulong) 6 << (7 * BitsInByte);
+ }
+ else if (length == 5)
{
- key |= (ulong)propertyName[4] << 32;
+ key |= (ulong) propertyName[4] << (4 * BitsInByte)
+ | (ulong) 5 << (7 * BitsInByte);
}
- if (length > 5)
+ else
{
- key |= (ulong)propertyName[5] << 40;
+ key |= (ulong) 4 << (7 * BitsInByte);
}
}
else if (length > 1)
{
key = MemoryMarshal.Read<ushort>(propertyName);
- if (length > 2)
+
+ if (length == 3)
{
- key |= (ulong)propertyName[2] << 16;
+ key |= (ulong) propertyName[2] << (2 * BitsInByte)
+ | (ulong) 3 << (7 * BitsInByte);
+ }
+ else
+ {
+ key |= (ulong) 2 << (7 * BitsInByte);
}
}
else if (length == 1)
{
- key = propertyName[0];
+ key = propertyName[0]
+ | (ulong) 1 << (7 * BitsInByte);
}
else
{
key = 0;
}
- // Embed the propertyName length in the last two bytes.
- key |= (ulong)propertyName.Length << 48;
+ // Verify key contains the embedded bytes as expected.
+ Debug.Assert(
+ (length < 1 || propertyName[0] == ((key & ((ulong)0xFF << 8 * 0)) >> 8 * 0)) &&
+ (length < 2 || propertyName[1] == ((key & ((ulong)0xFF << 8 * 1)) >> 8 * 1)) &&
+ (length < 3 || propertyName[2] == ((key & ((ulong)0xFF << 8 * 2)) >> 8 * 2)) &&
+ (length < 4 || propertyName[3] == ((key & ((ulong)0xFF << 8 * 3)) >> 8 * 3)) &&
+ (length < 5 || propertyName[4] == ((key & ((ulong)0xFF << 8 * 4)) >> 8 * 4)) &&
+ (length < 6 || propertyName[5] == ((key & ((ulong)0xFF << 8 * 5)) >> 8 * 5)) &&
+ (length < 7 || propertyName[6] == ((key & ((ulong)0xFF << 8 * 6)) >> 8 * 6)));
+
return key;
}
Assert.False(obj.MyOverflow.ContainsKey("key1"));
Assert.Equal(1, obj.MyOverflow["Key1"].GetInt32());
}
+
+ private class ClassWithPropertyNamePermutations
+ {
+ public int a { get; set; }
+ public int aa { get; set; }
+ public int aaa { get; set; }
+ public int aaaa { get; set; }
+ public int aaaaa { get; set; }
+ public int aaaaaa { get; set; }
+
+ // 7 characters - caching code only keys up to 7.
+ public int aaaaaaa { get; set; }
+ public int aaaaaab { get; set; }
+
+ // 8 characters.
+ public int aaaaaaaa { get; set; }
+ public int aaaaaaab { get; set; }
+
+ // 9 characters.
+ public int aaaaaaaaa { get; set; }
+ public int aaaaaaaab { get; set; }
+
+ public int \u0467 { get; set; }
+ public int \u0467\u0467 { get; set; }
+ public int \u0467\u0467a { get; set; }
+ public int \u0467\u0467b { get; set; }
+ public int \u0467\u0467\u0467 { get; set; }
+ public int \u0467\u0467\u0467a { get; set; }
+ public int \u0467\u0467\u0467b { get; set; }
+ public int \u0467\u0467\u0467\u0467 { get; set; }
+ public int \u0467\u0467\u0467\u0467a { get; set; }
+ public int \u0467\u0467\u0467\u0467b { get; set; }
+ }
+
+ [Fact]
+ public static void CachingKeys()
+ {
+ ClassWithPropertyNamePermutations obj;
+
+ void Verify()
+ {
+ Assert.Equal(1, obj.a);
+ Assert.Equal(2, obj.aa);
+ Assert.Equal(3, obj.aaa);
+ Assert.Equal(4, obj.aaaa);
+ Assert.Equal(5, obj.aaaaa);
+ Assert.Equal(6, obj.aaaaaa);
+ Assert.Equal(7, obj.aaaaaaa);
+ Assert.Equal(7, obj.aaaaaab);
+ Assert.Equal(8, obj.aaaaaaaa);
+ Assert.Equal(8, obj.aaaaaaab);
+ Assert.Equal(9, obj.aaaaaaaaa);
+ Assert.Equal(9, obj.aaaaaaaab);
+
+ Assert.Equal(2, obj.\u0467);
+ Assert.Equal(4, obj.\u0467\u0467);
+ Assert.Equal(5, obj.\u0467\u0467a);
+ Assert.Equal(5, obj.\u0467\u0467b);
+ Assert.Equal(6, obj.\u0467\u0467\u0467);
+ Assert.Equal(7, obj.\u0467\u0467\u0467a);
+ Assert.Equal(7, obj.\u0467\u0467\u0467b);
+ Assert.Equal(8, obj.\u0467\u0467\u0467\u0467);
+ Assert.Equal(9, obj.\u0467\u0467\u0467\u0467a);
+ Assert.Equal(9, obj.\u0467\u0467\u0467\u0467b);
+ }
+
+ obj = new ClassWithPropertyNamePermutations
+ {
+ a = 1,
+ aa = 2,
+ aaa = 3,
+ aaaa = 4,
+ aaaaa = 5,
+ aaaaaa = 6,
+ aaaaaaa = 7,
+ aaaaaab = 7,
+ aaaaaaaa = 8,
+ aaaaaaab = 8,
+ aaaaaaaaa = 9,
+ aaaaaaaab = 9,
+ \u0467 = 2,
+ \u0467\u0467 = 4,
+ \u0467\u0467a = 5,
+ \u0467\u0467b = 5,
+ \u0467\u0467\u0467 = 6,
+ \u0467\u0467\u0467a = 7,
+ \u0467\u0467\u0467b = 7,
+ \u0467\u0467\u0467\u0467 = 8,
+ \u0467\u0467\u0467\u0467a = 9,
+ \u0467\u0467\u0467\u0467b = 9,
+ };
+
+ // Verify baseline.
+ Verify();
+
+ string json = JsonSerializer.Serialize(obj);
+
+ // Verify the length is consistent with a verified value.
+ Assert.Equal(354, json.Length);
+
+ obj = JsonSerializer.Deserialize<ClassWithPropertyNamePermutations>(json);
+
+ // Verify round-tripped object.
+ Verify();
+ }
+
+ [Theory]
+ [InlineData(0x1, 'v')]
+ [InlineData(0x1, '\u0467')]
+ [InlineData(0x10, 'v')]
+ [InlineData(0x10, '\u0467')]
+ [InlineData(0x100, 'v')]
+ [InlineData(0x100, '\u0467')]
+ [InlineData(0x1000, 'v')]
+ [InlineData(0x1000, '\u0467')]
+ [InlineData(0x10000, 'v')]
+ [InlineData(0x10000, '\u0467')]
+ public static void LongPropertyNames(int propertyLength, char ch)
+ {
+ // Although the CLR may limit member length to 1023 bytes, the serializer doesn't have a hard limit.
+
+ string val = new string(ch, propertyLength);
+ string json = @"{""" + val + @""":1}";
+
+ EmptyClassWithExtensionProperty obj = JsonSerializer.Deserialize<EmptyClassWithExtensionProperty>(json);
+
+ Assert.True(obj.MyOverflow.ContainsKey(val));
+
+ var options = new JsonSerializerOptions
+ {
+ // Avoid escaping '\u0467'.
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ string jsonRoundTripped = JsonSerializer.Serialize(obj, options);
+ Assert.Equal(json, jsonRoundTripped);
+ }
}
public class OverridePropertyNameDesignTime_TestClass