Skip to content

Commit

Permalink
Rewrite the shared JsonSerializerOptions cache implementation. (#76607)
Browse files Browse the repository at this point in the history
* Rewrite the shared JsonSerializerOptions cache implementation.

* address feedback

* Simplify cache implementation& address feedback.
  • Loading branch information
eiriktsarpalis authored Oct 6, 2022
1 parent 835a133 commit 01cc573
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 212 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,146 +163,87 @@ 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="AreEquivalentOptions(JsonSerializerOptions, JsonSerializerOptions)"/>.
/// </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 const int EvictionCountHistory = 16;
private static readonly Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
private static int s_evictionRunsToSkip;
private static readonly WeakReference<CachingContext>?[] s_trackedContexts = new WeakReference<CachingContext>[MaxTrackedContexts];

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 (cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
if (TryGetContext(options, 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);
}

lock (cache)
lock (s_trackedContexts)
{
if (cache.TryGetValue(options, out wr))
if (TryGetContext(options, 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);

if (firstUnpopulatedIndex >= 0)
{
if (!TryEvictDanglingEntries())
// Cache has capacity -- store the context in the first available index.
ref WeakReference<CachingContext>? 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<CachingContext>(ctx));
Debug.Assert(success);

return ctx;
}
}

public static void Clear()
private static bool TryGetContext(
JsonSerializerOptions options,
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));

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

int currentEvictions = 0;
foreach (KeyValuePair<JsonSerializerOptions, WeakReference<CachingContext>> kvp in s_cache)
{
if (!kvp.Value.TryGetTarget(out _))
{
bool result = s_cache.TryRemove(kvp.Key, out _);
Debug.Assert(result);
currentEvictions++;
}
}

s_evictionRunsToSkip = EstimateEvictionRunsToSkip(currentEvictions);
return currentEvictions > 0;
WeakReference<CachingContext>?[] trackedContexts = s_trackedContexts;

// Estimate the number of eviction runs to skip based on recent eviction rates.
static int EstimateEvictionRunsToSkip(int latestEvictionCount)
firstUnpopulatedIndex = -1;
for (int i = 0; i < trackedContexts.Length; i++)
{
Queue<int> recentEvictionCounts = s_recentEvictionCounts;
WeakReference<CachingContext>? weakRef = trackedContexts[i];

if (recentEvictionCounts.Count < EvictionCountHistory - 1)
if (weakRef is null || !weakRef.TryGetTarget(out CachingContext? ctx))
{
// 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 (AreEquivalentOptions(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;
}
}

Expand All @@ -311,99 +252,52 @@ 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 bool AreEquivalentOptions(JsonSerializerOptions left, JsonSerializerOptions right)
{
public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
Debug.Assert(left != null && right != null);

return
left._dictionaryKeyPolicy == right._dictionaryKeyPolicy &&
left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy &&
left._readCommentHandling == right._readCommentHandling &&
left._referenceHandler == right._referenceHandler &&
left._encoder == right._encoder &&
left._defaultIgnoreCondition == right._defaultIgnoreCondition &&
left._numberHandling == right._numberHandling &&
left._unknownTypeHandling == right._unknownTypeHandling &&
left._defaultBufferSize == right._defaultBufferSize &&
left._maxDepth == right._maxDepth &&
left._allowTrailingCommas == right._allowTrailingCommas &&
left._ignoreNullValues == right._ignoreNullValues &&
left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties &&
left._ignoreReadonlyFields == right._ignoreReadonlyFields &&
left._includeFields == right._includeFields &&
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
left._writeIndented == right._writeIndented &&
left._typeInfoResolver == right._typeInfoResolver &&
CompareLists(left._converters, right._converters);

static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
{
Debug.Assert(left != null && right != null);

return
left._dictionaryKeyPolicy == right._dictionaryKeyPolicy &&
left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy &&
left._readCommentHandling == right._readCommentHandling &&
left._referenceHandler == right._referenceHandler &&
left._encoder == right._encoder &&
left._defaultIgnoreCondition == right._defaultIgnoreCondition &&
left._numberHandling == right._numberHandling &&
left._unknownTypeHandling == right._unknownTypeHandling &&
left._defaultBufferSize == right._defaultBufferSize &&
left._maxDepth == right._maxDepth &&
left._allowTrailingCommas == right._allowTrailingCommas &&
left._ignoreNullValues == right._ignoreNullValues &&
left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties &&
left._ignoreReadonlyFields == right._ignoreReadonlyFields &&
left._includeFields == right._includeFields &&
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
left._writeIndented == right._writeIndented &&
left._typeInfoResolver == right._typeInfoResolver &&
CompareLists(left._converters, right._converters);

static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
int n;
if ((n = left.Count) != right.Count)
{
int n;
if ((n = left.Count) != right.Count)
{
return false;
}

for (int i = 0; i < n; i++)
{
if (!left[i]!.Equals(right[i]))
{
return false;
}
}

return true;
return false;
}
}

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 < n; i++)
{
for (int i = 0; i < list.Count; i++)
TValue? leftElem = left[i];
TValue? rightElem = right[i];
bool areEqual = leftElem is null ? rightElem is null : leftElem.Equals(rightElem);
if (!areEqual)
{
hc.Add(list[i]);
return false;
}
}

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;
return true;
}
#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

0 comments on commit 01cc573

Please sign in to comment.