Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
Expand Down Expand Up @@ -163,146 +162,123 @@ public void Clear()

/// <summary>
/// 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 <see cref="CachingContext"/>.
/// Relevant caching contexts are looked up using the equality comparison defined by <see cref="EqualityComparer"/>.
/// this approach uses a fixed-size array of weak references of <see cref="CachingContext"/> that can be looked up lock-free.
/// Relevant caching contexts are looked up by linear traversal using the equality comparison defined by <see cref="EqualityComparer"/>.
/// </summary>
internal static class TrackedCachingContexts
{
private const int MaxTrackedContexts = 64;
private static readonly ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> s_cache =
new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());
private static readonly WeakReference<CachingContext>?[] s_trackedContexts = new WeakReference<CachingContext>[MaxTrackedContexts];
private static int s_size;

private const int EvictionCountHistory = 16;
private static readonly Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
private static int s_evictionRunsToSkip;
private const int DanglingEntryEvictInterval = 8;
private static int s_lookupsWithDanglingEntries;

public static CachingContext GetOrCreate(JsonSerializerOptions options)
{
Debug.Assert(options.IsReadOnly, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
Debug.Assert(options._typeInfoResolver != null);

ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> cache = s_cache;
if (TryGetSharedContext(options, out bool foundDanglingEntries, out CachingContext? result))
{
// Periodically evict dangling entries in the case of successful lookups
if (foundDanglingEntries && Interlocked.Increment(ref s_lookupsWithDanglingEntries) == DanglingEntryEvictInterval)
{
lock (s_trackedContexts)
{
EvictDanglingEntries();
}
}

return result;
}

if (cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
if (s_size == MaxTrackedContexts && !foundDanglingEntries)
{
return ctx;
// Cache is full; return a fresh instance.
return new CachingContext(options);
}

lock (cache)
lock (s_trackedContexts)
{
if (cache.TryGetValue(options, out wr))
if (TryGetSharedContext(options, out foundDanglingEntries, 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)
if (foundDanglingEntries)
{
if (!TryEvictDanglingEntries())
{
// Cache is full; return a fresh instance.
return new CachingContext(options);
}
// Always run eviction if writing to the cache.
EvictDanglingEntries();
}

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<CachingContext>(ctx));
Debug.Assert(success);
if (s_size == MaxTrackedContexts)
{
// Cache is full; return a fresh instance.
return new CachingContext(options);
}

var ctx = new CachingContext(options);
s_trackedContexts[s_size++] = new WeakReference<CachingContext>(ctx);
return ctx;
}
}

public static void Clear()
private static bool TryGetSharedContext(
JsonSerializerOptions options,
out bool foundDanglingEntries,
[NotNullWhen(true)] out CachingContext? result)
{
lock (s_cache)
{
s_cache.Clear();
s_recentEvictionCounts.Clear();
s_evictionRunsToSkip = 0;
}
}
// When called outside of the lock this method can be subject to races.
// We're fine with this since worst-case scenario the lookup will report
// a false negative and trigger a further lookup after the lock has been acquired.

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.
WeakReference<CachingContext>?[] trackedContexts = s_trackedContexts;
int size = s_size;

Debug.Assert(Monitor.IsEntered(s_cache));

if (s_evictionRunsToSkip > 0)
{
--s_evictionRunsToSkip;
return false;
}

int currentEvictions = 0;
foreach (KeyValuePair<JsonSerializerOptions, WeakReference<CachingContext>> kvp in s_cache)
foundDanglingEntries = false;
for (int i = 0; i < size; i++)
{
if (!kvp.Value.TryGetTarget(out _))
if (trackedContexts[i] is WeakReference<CachingContext> weakRef &&
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<int> recentEvictionCounts = s_recentEvictionCounts;

if (recentEvictionCounts.Count < EvictionCountHistory - 1)
{
// Insufficient data points to determine a skip count.
recentEvictionCounts.Enqueue(latestEvictionCount);
return 0;
if (EqualityComparer.Equals(options, ctx.Options))
{
result = ctx;
return true;
}
}
else if (recentEvictionCounts.Count == EvictionCountHistory)
else
{
recentEvictionCounts.Dequeue();
foundDanglingEntries = true;
}
}

result = null;
return false;
}

recentEvictionCounts.Enqueue(latestEvictionCount);
private static void EvictDanglingEntries()
{
Monitor.IsEntered(s_trackedContexts);

// 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.
WeakReference<CachingContext>?[] trackedOptions = s_trackedContexts;
int size = s_size;

int totalEvictions = 0;
foreach (int evictionCount in recentEvictionCounts)
int nextAvailable = 0;
for (int i = 0; i < size; i++)
{
if (trackedOptions[i] is WeakReference<CachingContext> weakRef &&
weakRef.TryGetTarget(out _))
{
totalEvictions += evictionCount;
trackedOptions[nextAvailable++] = weakRef;
}
}

int evictionRunsToSkip =
totalEvictions >= EvictionCountHistory ? 0 :
(int)Math.Round((double)EvictionCountHistory / Math.Max(totalEvictions, 1));
Array.Clear(trackedOptions, nextAvailable, size - nextAvailable);

Debug.Assert(0 <= evictionRunsToSkip && evictionRunsToSkip <= EvictionCountHistory);
return evictionRunsToSkip;
}
Volatile.Write(ref s_size, nextAvailable);
Volatile.Write(ref s_lookupsWithDanglingEntries, 0);
}
}

Expand All @@ -311,9 +287,9 @@ static int EstimateEvictionRunsToSkip(int latestEvictionCount)
/// If two instances are equivalent, they should generate identical metadata caches;
/// the converse however does not necessarily hold.
/// </summary>
private sealed class EqualityComparer : IEqualityComparer<JsonSerializerOptions>
private static class EqualityComparer
{
public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
public static bool Equals(JsonSerializerOptions left, JsonSerializerOptions right)
{
Debug.Assert(left != null && right != null);

Expand Down Expand Up @@ -357,53 +333,6 @@ static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationLi
return true;
}
}

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<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
{
for (int i = 0; i < list.Count; i++)
{
hc.Add(list[i]);
}
}

return hc.ToHashCode();
}

#if !NETCOREAPP
/// <summary>
/// Polyfill for System.HashCode.
/// </summary>
private struct HashCode
{
private int _hashCode;
public void Add<T>(T? value) => _hashCode = (_hashCode, value).GetHashCode();
public int ToHashCode() => _hashCode;
}
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Loading