// The length of the property name embedded in the key (in bytes).
private const int PropertyNameKeyLength = 6;
- private readonly List<PropertyRef> _property_refs = new List<PropertyRef>();
- private readonly List<PropertyRef> _property_refs_sorted = new List<PropertyRef>();
+ private readonly List<PropertyRef> _propertyRefs = new List<PropertyRef>();
+
+ // Cache of properties by first ordering. Use an array for highest performance.
+ private volatile PropertyRef[] _propertyRefsSorted = null;
internal delegate object ConstructorDelegate();
internal ConstructorDelegate CreateObject { get; private set; }
public Type Type { get; private set; }
+ internal void UpdateSortedPropertyCache(ref ReadStackFrame frame)
+ {
+ // Set the sorted property cache. Overwrite any existing cache which can occur in multi-threaded cases.
+ // Todo: on classes with many properties (about 50) we need to switch to a hashtable.
+ if (frame.PropertyRefCache != null)
+ {
+ List<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];
+
+ bool found = false;
+ int iCacheProperty = 0;
+ for (; iCacheProperty < cache.Count; iCacheProperty++)
+ {
+ if (IsPropertyRefEqual(ref propertyRef, cache[iCacheProperty]))
+ {
+ // The property is already cached, skip to the next property.
+ found = true;
+ break;
+ }
+ }
+
+ if (found == false)
+ {
+ cache.Add(propertyRef);
+ break;
+ }
+ }
+ }
+
+ Debug.Assert(cache.Count == _propertyRefs.Count);
+ _propertyRefsSorted = cache.ToArray();
+ frame.PropertyRefCache = null;
+ }
+ }
+
internal JsonClassInfo(Type type, JsonSerializerOptions options)
{
Type = type;
}
}
- internal JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, int propertyIndex)
+ internal JsonPropertyInfo GetProperty(ReadOnlySpan<byte> propertyName, ref ReadStackFrame frame)
{
ulong key = GetKey(propertyName);
JsonPropertyInfo info = null;
// First try sorted lookup.
- int count = _property_refs_sorted.Count;
- if (count != 0)
+ 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)
{
- int iForward = propertyIndex;
- int iBackward = propertyIndex - 1;
- while (iForward < count || (iBackward >= 0 && iBackward < count))
+ // This .Length is consistent no matter what json data intialized _propertyRefsSorted.
+ int count = _propertyRefsSorted.Length;
+ if (count != 0)
{
- if (iForward < count)
+ int iForward = propertyIndex;
+ int iBackward = propertyIndex - 1;
+ while (iForward < count || iBackward >= 0)
{
- if (TryIsPropertyRefEqual(_property_refs_sorted, propertyName, key, iForward, out info))
+ if (iForward < count)
{
- return info;
+ if (TryIsPropertyRefEqual(ref _propertyRefsSorted[iForward], propertyName, key, ref info))
+ {
+ return info;
+ }
+ ++iForward;
}
- ++iForward;
- }
- if (iBackward >= 0)
- {
- if (TryIsPropertyRefEqual(_property_refs_sorted, propertyName, key, iBackward, out info))
+ if (iBackward >= 0)
{
- return info;
+ if (TryIsPropertyRefEqual(ref _propertyRefsSorted[iBackward], propertyName, key, ref info))
+ {
+ return info;
+ }
+ --iBackward;
}
- --iBackward;
}
}
}
- // Then try fallback
- for (int i = 0; i < _property_refs.Count; i++)
+ // 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++)
{
- if (TryIsPropertyRefEqual(_property_refs, propertyName, key, i, out info))
+ PropertyRef propertyRef = _propertyRefs[i];
+ if (TryIsPropertyRefEqual(ref propertyRef, propertyName, key, ref info))
{
break;
}
}
- if (info != null)
+ if (!hasPropertyCache)
{
- _property_refs_sorted.Add(new PropertyRef(key, info));
+ if (propertyIndex == 0)
+ {
+ // Create the temporary list on first property access to prevent a partially filled List.
+ Debug.Assert(frame.PropertyRefCache == null);
+ frame.PropertyRefCache = new List<PropertyRef>();
+ }
+
+ if (info != null)
+ {
+ Debug.Assert(frame.PropertyRefCache != null);
+ frame.PropertyRefCache.Add(new PropertyRef(key, info));
+ }
}
return info;
internal JsonPropertyInfo GetPolicyProperty()
{
- Debug.Assert(_property_refs.Count == 1);
- return _property_refs[0].Info;
+ Debug.Assert(_propertyRefs.Count == 1);
+ return _propertyRefs[0].Info;
}
internal JsonPropertyInfo GetProperty(int index)
{
- Debug.Assert(index < _property_refs.Count);
- return _property_refs[index].Info;
+ Debug.Assert(index < _propertyRefs.Count);
+ return _propertyRefs[index].Info;
}
internal int PropertyCount
{
get
{
- return _property_refs.Count;
+ return _propertyRefs.Count;
}
}
- private static bool TryIsPropertyRefEqual(List<PropertyRef> list, ReadOnlySpan<byte> propertyName, ulong key, int index, out JsonPropertyInfo info)
+ private static bool TryIsPropertyRefEqual(ref PropertyRef propertyRef, ReadOnlySpan<byte> propertyName, ulong key, ref JsonPropertyInfo info)
{
- if (key == list[index].Key)
+ 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((ReadOnlySpan<byte>)list[index].Info._name))
+ propertyName.SequenceEqual((ReadOnlySpan<byte>)propertyRef.Info._name))
+ {
+ info = propertyRef.Info;
+ return true;
+ }
+ }
+
+ 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.SequenceEqual(other.Info.Name))
{
- info = list[index].Info;
return true;
}
}
- info = null;
return false;
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// 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.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+ public static class CacheTests
+ {
+ [Fact]
+ public static void MultipleThreads()
+ {
+ // Ensure no exceptions are thrown due to caching or other issues.
+ void SerializeAndDeserializeObject(bool useEmptyJson)
+ {
+ // Use localized caching
+ // Todo: localized caching not implemented yet. When implemented, add a run-time attribute to JsonSerializerOptions as that will create a separate cache held by JsonSerializerOptions.
+ var options = new JsonSerializerOptions();
+
+ string json;
+
+ if (useEmptyJson)
+ {
+ json = "{}";
+ }
+ else
+ {
+ SimpleTestClass testObj = new SimpleTestClass();
+ testObj.Initialize();
+ testObj.Verify();
+
+ json = JsonSerializer.ToString(testObj, options);
+ }
+
+ SimpleTestClass testObjDeserialized = JsonSerializer.Parse<SimpleTestClass>(json, options);
+ testObjDeserialized.Verify();
+ };
+
+ Task[] tasks = new Task[8];
+ bool useEmptyJson = false;
+ for (int i = 0; i < tasks.Length; i++)
+ {
+ tasks[i] = Task.Run(() => SerializeAndDeserializeObject(useEmptyJson));
+ useEmptyJson = !useEmptyJson;
+ };
+
+ Task.WaitAll(tasks);
+ }
+
+ [Fact]
+ public static void PropertyCacheWithMinInputsFirst()
+ {
+ // Use localized caching
+ // Todo: localized caching not implemented yet. When implemented, add a run-time attribute to JsonSerializerOptions as that will create a separate cache held by JsonSerializerOptions.
+ var options = new JsonSerializerOptions();
+
+ string json = "{}";
+ JsonSerializer.Parse<SimpleTestClass>(json, options);
+
+ SimpleTestClass testObj = new SimpleTestClass();
+ testObj.Initialize();
+ testObj.Verify();
+
+ json = JsonSerializer.ToString(testObj, options);
+ testObj = JsonSerializer.Parse<SimpleTestClass>(json, options);
+ testObj.Verify();
+ }
+
+ [Fact]
+ public static void PropertyCacheWithMinInputsLast()
+ {
+ // Use localized caching
+ // Todo: localized caching not implemented yet. When implemented, add a run-time attribute to JsonSerializerOptions as that will create a separate cache held by JsonSerializerOptions.
+ var options = new JsonSerializerOptions();
+
+ SimpleTestClass testObj = new SimpleTestClass();
+ testObj.Initialize();
+ testObj.Verify();
+
+ string json = JsonSerializer.ToString(testObj, options);
+ testObj = JsonSerializer.Parse<SimpleTestClass>(json, options);
+ testObj.Verify();
+
+ json = "{}";
+ JsonSerializer.Parse<SimpleTestClass>(json, options);
+ }
+ }
+}