From 53593111dc016e802b9f4a09c674fdb1aba58e40 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 13 Jan 2023 18:25:49 +0000 Subject: [PATCH] Remove ConcurrentDictionary from the shared JsonSerializerOptions cache implementation. (#80576) --- .../JsonSerializerOptions.Caching.cs | 221 ++++++++---------- .../JsonSerializerOptionsUpdateHandler.cs | 3 - 2 files changed, 91 insertions(+), 133 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs index 7a10c4d12c79b..497eb3e36dfff 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs @@ -143,13 +143,15 @@ internal sealed class CachingContext private readonly ConcurrentDictionary _jsonTypeInfoCache = new(); private readonly Func _jsonTypeInfoFactory; - public CachingContext(JsonSerializerOptions options) + public CachingContext(JsonSerializerOptions options, int hashCode) { Options = options; + HashCode = hashCode; _jsonTypeInfoFactory = Options.GetTypeInfoNoCaching; } public JsonSerializerOptions Options { get; } + public int HashCode { get; } // Property only accessed by reflection in testing -- do not remove. // If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date. public int Count => _jsonTypeInfoCache.Count; @@ -166,146 +168,90 @@ public void Clear() /// /// Defines a cache of CachingContexts; instead of using a ConditionalWeakTable which can be slow to traverse - /// this approach uses a concurrent dictionary pointing to weak references of . - /// Relevant caching contexts are looked up using the equality comparison defined by . + /// this approach uses a fixed-size array of weak references of that can be looked up lock-free. + /// Relevant caching contexts are looked up by linear traversal using the equality comparison defined by . /// internal static class TrackedCachingContexts { private const int MaxTrackedContexts = 64; - private static readonly ConcurrentDictionary> s_cache = - new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer()); - - private const int EvictionCountHistory = 16; - private static readonly Queue s_recentEvictionCounts = new(EvictionCountHistory); - private static int s_evictionRunsToSkip; + private static readonly WeakReference?[] s_trackedContexts = new WeakReference[MaxTrackedContexts]; + private static readonly EqualityComparer s_optionsComparer = new(); public static CachingContext GetOrCreate(JsonSerializerOptions options) { Debug.Assert(options.IsImmutable, "Cannot create caching contexts for mutable JsonSerializerOptions instances"); Debug.Assert(options._typeInfoResolver != null); - ConcurrentDictionary> cache = s_cache; + int hashCode = s_optionsComparer.GetHashCode(options); - if (cache.TryGetValue(options, out WeakReference? wr) && wr.TryGetTarget(out CachingContext? ctx)) + if (TryGetContext(options, hashCode, out int firstUnpopulatedIndex, out CachingContext? result)) { - return ctx; + return result; + } + else if (firstUnpopulatedIndex < 0) + { + // Cache is full; return a fresh instance. + return new CachingContext(options, hashCode); } - lock (cache) + lock (s_trackedContexts) { - if (cache.TryGetValue(options, out wr)) + if (TryGetContext(options, hashCode, out firstUnpopulatedIndex, out result)) { - if (!wr.TryGetTarget(out ctx)) - { - // Found a dangling weak reference; replenish with a fresh instance. - ctx = new CachingContext(options); - wr.SetTarget(ctx); - } - - return ctx; + return result; } - if (cache.Count == MaxTrackedContexts) + var ctx = new CachingContext(options, hashCode); + + if (firstUnpopulatedIndex >= 0) { - if (!TryEvictDanglingEntries()) + // Cache has capacity -- store the context in the first available index. + ref WeakReference? weakRef = ref s_trackedContexts[firstUnpopulatedIndex]; + + if (weakRef is null) + { + weakRef = new(ctx); + } + else { - // Cache is full; return a fresh instance. - return new CachingContext(options); + Debug.Assert(weakRef.TryGetTarget(out _) is false); + weakRef.SetTarget(ctx); } } - Debug.Assert(cache.Count < MaxTrackedContexts); - - // Use a defensive copy of the options instance as key to - // avoid capturing references to any caching contexts. - var key = new JsonSerializerOptions(options); - Debug.Assert(key._cachingContext == null); - - ctx = new CachingContext(options); - bool success = cache.TryAdd(key, new WeakReference(ctx)); - Debug.Assert(success); - return ctx; } } - public static void Clear() + private static bool TryGetContext( + JsonSerializerOptions options, + int hashCode, + out int firstUnpopulatedIndex, + [NotNullWhen(true)] out CachingContext? result) { - lock (s_cache) - { - s_cache.Clear(); - s_recentEvictionCounts.Clear(); - s_evictionRunsToSkip = 0; - } - } - - private static bool TryEvictDanglingEntries() - { - // Worst case scenario, the cache has been filled with permanent entries. - // Evictions are synchronized and each run is in the order of microseconds, - // so we want to avoid triggering runs every time an instance is initialized, - // For this reason we use a backoff strategy to average out the cost of eviction - // across multiple initializations. The backoff count is determined by the eviction - // rates of the most recent runs. - - Debug.Assert(Monitor.IsEntered(s_cache)); + WeakReference?[] trackedContexts = s_trackedContexts; - if (s_evictionRunsToSkip > 0) + firstUnpopulatedIndex = -1; + for (int i = 0; i < trackedContexts.Length; i++) { - --s_evictionRunsToSkip; - return false; - } + WeakReference? weakRef = trackedContexts[i]; - int currentEvictions = 0; - foreach (KeyValuePair> kvp in s_cache) - { - if (!kvp.Value.TryGetTarget(out _)) + if (weakRef is null || !weakRef.TryGetTarget(out CachingContext? ctx)) { - bool result = s_cache.TryRemove(kvp.Key, out _); - Debug.Assert(result); - currentEvictions++; - } - } - - s_evictionRunsToSkip = EstimateEvictionRunsToSkip(currentEvictions); - return currentEvictions > 0; - - // Estimate the number of eviction runs to skip based on recent eviction rates. - static int EstimateEvictionRunsToSkip(int latestEvictionCount) - { - Queue recentEvictionCounts = s_recentEvictionCounts; - - if (recentEvictionCounts.Count < EvictionCountHistory - 1) - { - // Insufficient data points to determine a skip count. - recentEvictionCounts.Enqueue(latestEvictionCount); - return 0; + if (firstUnpopulatedIndex < 0) + { + firstUnpopulatedIndex = i; + } } - else if (recentEvictionCounts.Count == EvictionCountHistory) + else if (hashCode == ctx.HashCode && s_optionsComparer.Equals(options, ctx.Options)) { - recentEvictionCounts.Dequeue(); + result = ctx; + return true; } - - recentEvictionCounts.Enqueue(latestEvictionCount); - - // Calculate the total number of eviction in the latest runs - // - If we have at least one eviction per run, on average, - // do not skip any future eviction runs. - // - Otherwise, skip ~the number of runs needed per one eviction. - - int totalEvictions = 0; - foreach (int evictionCount in recentEvictionCounts) - { - totalEvictions += evictionCount; - } - - int evictionRunsToSkip = - totalEvictions >= EvictionCountHistory ? 0 : - (int)Math.Round((double)EvictionCountHistory / Math.Max(totalEvictions, 1)); - - Debug.Assert(0 <= evictionRunsToSkip && evictionRunsToSkip <= EvictionCountHistory); - return evictionRunsToSkip; } + + result = null; + return false; } } @@ -342,6 +288,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right) CompareLists(left._converters, right._converters); static bool CompareLists(ConfigurationList left, ConfigurationList right) + where TValue : class? { int n; if ((n = left.Count) != right.Count) @@ -351,7 +298,7 @@ static bool CompareLists(ConfigurationList left, ConfigurationLi for (int i = 0; i < n; i++) { - if (!left[i]!.Equals(right[i])) + if (left[i] != right[i]) { return false; } @@ -365,35 +312,49 @@ public int GetHashCode(JsonSerializerOptions options) { HashCode hc = default; - hc.Add(options._dictionaryKeyPolicy); - hc.Add(options._jsonPropertyNamingPolicy); - hc.Add(options._readCommentHandling); - hc.Add(options._referenceHandler); - hc.Add(options._encoder); - hc.Add(options._defaultIgnoreCondition); - hc.Add(options._numberHandling); - hc.Add(options._unknownTypeHandling); - hc.Add(options._defaultBufferSize); - hc.Add(options._maxDepth); - hc.Add(options._allowTrailingCommas); - hc.Add(options._ignoreNullValues); - hc.Add(options._ignoreReadOnlyProperties); - hc.Add(options._ignoreReadonlyFields); - hc.Add(options._includeFields); - hc.Add(options._propertyNameCaseInsensitive); - hc.Add(options._writeIndented); - hc.Add(options._typeInfoResolver); - GetHashCode(ref hc, options._converters); - - static void GetHashCode(ref HashCode hc, ConfigurationList list) + AddHashCode(ref hc, options._dictionaryKeyPolicy); + AddHashCode(ref hc, options._jsonPropertyNamingPolicy); + AddHashCode(ref hc, options._readCommentHandling); + AddHashCode(ref hc, options._referenceHandler); + AddHashCode(ref hc, options._encoder); + AddHashCode(ref hc, options._defaultIgnoreCondition); + AddHashCode(ref hc, options._numberHandling); + AddHashCode(ref hc, options._unknownTypeHandling); + AddHashCode(ref hc, options._defaultBufferSize); + AddHashCode(ref hc, options._maxDepth); + AddHashCode(ref hc, options._allowTrailingCommas); + AddHashCode(ref hc, options._ignoreNullValues); + AddHashCode(ref hc, options._ignoreReadOnlyProperties); + AddHashCode(ref hc, options._ignoreReadonlyFields); + AddHashCode(ref hc, options._includeFields); + AddHashCode(ref hc, options._propertyNameCaseInsensitive); + AddHashCode(ref hc, options._writeIndented); + AddHashCode(ref hc, options._typeInfoResolver); + AddListHashCode(ref hc, options._converters); + + return hc.ToHashCode(); + + static void AddListHashCode(ref HashCode hc, ConfigurationList list) { - for (int i = 0; i < list.Count; i++) + int n = list.Count; + for (int i = 0; i < n; i++) { - hc.Add(list[i]); + AddHashCode(ref hc, list[i]); } } - return hc.ToHashCode(); + static void AddHashCode(ref HashCode hc, TValue? value) + { + if (typeof(TValue).IsValueType) + { + hc.Add(value); + } + else + { + Debug.Assert(!typeof(TValue).IsSealed, "Sealed reference types like string should not use this method."); + hc.Add(RuntimeHelpers.GetHashCode(value)); + } + } } #if !NETCOREAPP diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs index 8f7f94140f0c4..53031f1a3c34e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs @@ -23,9 +23,6 @@ public static void ClearCache(Type[]? types) options.Key.ClearCaches(); } - // Flush the shared caching contexts - JsonSerializerOptions.TrackedCachingContexts.Clear(); - // Flush the dynamic method cache ReflectionEmitCachingMemberAccessor.Clear(); }