Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
3 changes: 3 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ System.Text.Json.Nodes.JsonValue</PackageDescription>
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Element.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializer.Write.Node.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerContext.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptions.Caching.cs" />
<Compile Include="System\Text\Json\Serialization\ReferenceEqualsWrapper.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterStrategy.cs" />
<Compile Include="System\Text\Json\Serialization\ConverterList.cs" />
Expand Down Expand Up @@ -247,6 +248,8 @@ System.Text.Json.Nodes.JsonValue</PackageDescription>
<Compile Include="System\Text\Json\Serialization\Metadata\MemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ParameterRef.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\PropertyRef.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionEmitCachingMemberAccessor.Cache.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionEmitCachingMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionEmitMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\ReflectionMemberAccessor.cs" />
<Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type run
Debug.Assert(runtimeType != null);

options ??= JsonSerializerOptions.Default;
if (!options.IsInitializedForReflectionSerializer)
if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
{
options.InitializeForReflectionSerializer();
JsonSerializerOptions.InitializeForReflectionSerializer();
}

return options.GetOrAddClassForRootType(runtimeType);
return options.GetOrAddJsonTypeInfoForRootType(runtimeType);
}

private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ public static partial class JsonSerializer
CancellationToken cancellationToken = default)
{
options ??= JsonSerializerOptions.Default;
if (!options.IsInitializedForReflectionSerializer)
if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
{
options.InitializeForReflectionSerializer();
JsonSerializerOptions.InitializeForReflectionSerializer();
}

return CreateAsyncEnumerableDeserializer(utf8Json, options, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json
{
public sealed partial class JsonSerializerOptions
{
/// <summary>
/// Encapsulates all cached metadata referenced by the current <see cref="JsonSerializerOptions" /> instance.
/// Context can be shared across multiple equivalent options instances.
/// </summary>
private CachingContext? _cachingContext;

// Simple LRU cache for the public (de)serialize entry points that avoid some lookups in _cachingContext.
// Although this may be written by multiple threads, 'volatile' was not added since any local affinity is fine.
Comment thread
eiriktsarpalis marked this conversation as resolved.
Outdated
private JsonTypeInfo? _lastTypeInfo;

internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type)
{
if (_cachingContext == null)
{
InitializeCachingContext();
Debug.Assert(_cachingContext != null);
}

return _cachingContext.GetOrAddJsonTypeInfo(type);
}

internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
{
if (_cachingContext == null)
{
typeInfo = null;
return false;
}

return _cachingContext.TryGetJsonTypeInfo(type, out typeInfo);
}

internal bool IsJsonTypeInfoCached(Type type) => _cachingContext?.IsJsonTypeInfoCached(type) == true;

/// <summary>
/// Return the TypeInfo for root API calls.
/// This has an LRU cache that is intended only for public API calls that specify the root type.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Comment thread
eiriktsarpalis marked this conversation as resolved.
internal JsonTypeInfo GetOrAddJsonTypeInfoForRootType(Type type)
{
JsonTypeInfo? jsonTypeInfo = _lastTypeInfo;

if (jsonTypeInfo?.Type != type)
{
jsonTypeInfo = GetOrAddJsonTypeInfo(type);
_lastTypeInfo = jsonTypeInfo;
}

return jsonTypeInfo;
}

internal void ClearCaches()
{
_cachingContext?.Clear();
_lastTypeInfo = null;
}

private void InitializeCachingContext()
{
_cachingContext = TrackedCachingContexts.GetOrCreate(this);
}

/// <summary>
/// Stores and manages all reflection caches for one or more <see cref="JsonSerializerOptions"/> instances.
/// NB the type encapsulates the original options instance and only consults that one when building new types;
/// this is to prevent multiple options instances from leaking into the object graphs of converters which
/// could break user invariants.
/// </summary>
private sealed class CachingContext
{
private readonly ConcurrentDictionary<Type, JsonConverter> _converterCache = new();
private readonly ConcurrentDictionary<Type, JsonTypeInfo> _jsonTypeInfoCache = new();

public CachingContext(JsonSerializerOptions options)
{
Options = options;
_ = Count;
Comment thread
eiriktsarpalis marked this conversation as resolved.
Outdated
}

public JsonSerializerOptions Options { get; }
public int Count => _converterCache.Count + _jsonTypeInfoCache.Count;
public JsonConverter GetOrAddConverter(Type type) => _converterCache.GetOrAdd(type, Options.GetConverterFromType);
public JsonTypeInfo GetOrAddJsonTypeInfo(Type type) => _jsonTypeInfoCache.GetOrAdd(type, Options.GetJsonTypeInfoFromContextOrCreate);
public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) => _jsonTypeInfoCache.TryGetValue(type, out typeInfo);
public bool IsJsonTypeInfoCached(Type type) => _jsonTypeInfoCache.ContainsKey(type);

public void Clear()
{
_converterCache.Clear();
_jsonTypeInfoCache.Clear();
}
}

/// <summary>
/// Defines a cache of CachingContexts; instead of using a ConditionalWeakTable which can be slow to traverse
/// this approach uses a regular dictionary pointing to weak references of <see cref="CachingContext"/>.
Comment thread
eiriktsarpalis marked this conversation as resolved.
/// Relevant caching contexts are looked up using the equality comparison defined by <see cref="EqualityComparer"/>.
/// </summary>
private static class TrackedCachingContexts
{
private const int MaxTrackedContexts = 64;
private static readonly ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> s_cache =
Comment thread
eiriktsarpalis marked this conversation as resolved.
new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());

private const int EvictionCountHistory = 10;
private static Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
private static int s_evictionRunsToSkip;

public static CachingContext GetOrCreate(JsonSerializerOptions options)
{
if (s_cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
{
return ctx;
}

lock (s_cache)
{
if (s_cache.TryGetValue(options, out wr))
{
if (!wr.TryGetTarget(out ctx))
{
Comment thread
eiriktsarpalis marked this conversation as resolved.
Outdated
// Found a dangling weak reference; replenish with a fresh instance.
ctx = new CachingContext(options);
wr.SetTarget(ctx);
}

return ctx;
}

if (s_cache.Count == MaxTrackedContexts)
{
if (!TryEvictDanglingEntries())
{
// Cache is full; return a fresh instance.
return new CachingContext(options);
}
}

Debug.Assert(s_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) { _context = options._context };
Debug.Assert(key._cachingContext == null);

ctx = new CachingContext(options);
bool success = s_cache.TryAdd(key, new(ctx));
Debug.Assert(success);

return ctx;
}
}

private static bool TryEvictDanglingEntries()
{
Comment thread
eiriktsarpalis marked this conversation as resolved.
// Worst case scenario, the cache has been saturated with permanent entries.
// We want to avoid iterating needlessly through the entire cache every time
// a new options instance is created. For this reason we implement a backoff
// strategy to average out the cost of eviction across multiple cache initializations.
// The backoff count is determined by the eviction rates of the most recent eviction runs.

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;

// Estimate the number of eviction runs to skip based on recent eviction rates.
static int EstimateEvictionRunsToSkip(int latestEvictionCount)
{
if (s_recentEvictionCounts.Count < EvictionCountHistory)
{
// Insufficient data to determine a skip count.
s_recentEvictionCounts.Enqueue(latestEvictionCount);
return 0;
}
else
{
s_recentEvictionCounts.Dequeue();
s_recentEvictionCounts.Enqueue(latestEvictionCount);

// Calculate the average evictions per run.
double avgEvictionsPerRun = 0;

foreach (int rate in s_recentEvictionCounts)
{
avgEvictionsPerRun += rate;
}

avgEvictionsPerRun /= EvictionCountHistory;

// - If avgEvictionsPerRun >= 1 we skip no subsequent eviction calls.
// - If avgEvictionsPerRun < 1 we skip ~ `1 / avgEvictionsPerRun` calls.
int runsPerEviction = (int)Math.Round(1 / Math.Min(Math.Max(avgEvictionsPerRun, 0.1), 1));
Debug.Assert(runsPerEviction >= 1 && runsPerEviction <= 10);
return runsPerEviction - 1;
}
}
}
}

/// <summary>
/// Provides a conservative equality comparison for JsonSerializerOptions instances.
/// If two instances are equivalent, they should generate identical metadata caches;
/// the converse however does not necessarily hold.
/// </summary>
private class EqualityComparer : IEqualityComparer<JsonSerializerOptions>
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
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._context == right._context &&
CompareConverters(left.Converters, right.Converters);

static bool CompareConverters(IList<JsonConverter> left, IList<JsonConverter> right)
{
int n;
if ((n = left.Count) != right.Count)
{
return false;
}

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

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._context);

foreach (JsonConverter converter in options.Converters)
Comment thread
eiriktsarpalis marked this conversation as resolved.
{
hc.Add(converter);
}

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
}
}
}
Loading