Skip to content

Commit aa2fe3a

Browse files
committed
Merge in 'release/7.0' changes
2 parents 6f8f3db + 5359311 commit aa2fe3a

File tree

2 files changed

+91
-133
lines changed

2 files changed

+91
-133
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs

Lines changed: 91 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,15 @@ internal sealed class CachingContext
143143
private readonly ConcurrentDictionary<Type, JsonTypeInfo?> _jsonTypeInfoCache = new();
144144
private readonly Func<Type, JsonTypeInfo?> _jsonTypeInfoFactory;
145145

146-
public CachingContext(JsonSerializerOptions options)
146+
public CachingContext(JsonSerializerOptions options, int hashCode)
147147
{
148148
Options = options;
149+
HashCode = hashCode;
149150
_jsonTypeInfoFactory = Options.GetTypeInfoNoCaching;
150151
}
151152

152153
public JsonSerializerOptions Options { get; }
154+
public int HashCode { get; }
153155
// Property only accessed by reflection in testing -- do not remove.
154156
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
155157
public int Count => _jsonTypeInfoCache.Count;
@@ -166,146 +168,90 @@ public void Clear()
166168

167169
/// <summary>
168170
/// Defines a cache of CachingContexts; instead of using a ConditionalWeakTable which can be slow to traverse
169-
/// this approach uses a concurrent dictionary pointing to weak references of <see cref="CachingContext"/>.
170-
/// Relevant caching contexts are looked up using the equality comparison defined by <see cref="EqualityComparer"/>.
171+
/// this approach uses a fixed-size array of weak references of <see cref="CachingContext"/> that can be looked up lock-free.
172+
/// Relevant caching contexts are looked up by linear traversal using the equality comparison defined by <see cref="EqualityComparer"/>.
171173
/// </summary>
172174
internal static class TrackedCachingContexts
173175
{
174176
private const int MaxTrackedContexts = 64;
175-
private static readonly ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> s_cache =
176-
new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());
177-
178-
private const int EvictionCountHistory = 16;
179-
private static readonly Queue<int> s_recentEvictionCounts = new(EvictionCountHistory);
180-
private static int s_evictionRunsToSkip;
177+
private static readonly WeakReference<CachingContext>?[] s_trackedContexts = new WeakReference<CachingContext>[MaxTrackedContexts];
178+
private static readonly EqualityComparer s_optionsComparer = new();
181179

182180
public static CachingContext GetOrCreate(JsonSerializerOptions options)
183181
{
184182
Debug.Assert(options.IsImmutable, "Cannot create caching contexts for mutable JsonSerializerOptions instances");
185183
Debug.Assert(options._typeInfoResolver != null);
186184

187-
ConcurrentDictionary<JsonSerializerOptions, WeakReference<CachingContext>> cache = s_cache;
185+
int hashCode = s_optionsComparer.GetHashCode(options);
188186

189-
if (cache.TryGetValue(options, out WeakReference<CachingContext>? wr) && wr.TryGetTarget(out CachingContext? ctx))
187+
if (TryGetContext(options, hashCode, out int firstUnpopulatedIndex, out CachingContext? result))
190188
{
191-
return ctx;
189+
return result;
190+
}
191+
else if (firstUnpopulatedIndex < 0)
192+
{
193+
// Cache is full; return a fresh instance.
194+
return new CachingContext(options, hashCode);
192195
}
193196

194-
lock (cache)
197+
lock (s_trackedContexts)
195198
{
196-
if (cache.TryGetValue(options, out wr))
199+
if (TryGetContext(options, hashCode, out firstUnpopulatedIndex, out result))
197200
{
198-
if (!wr.TryGetTarget(out ctx))
199-
{
200-
// Found a dangling weak reference; replenish with a fresh instance.
201-
ctx = new CachingContext(options);
202-
wr.SetTarget(ctx);
203-
}
204-
205-
return ctx;
201+
return result;
206202
}
207203

208-
if (cache.Count == MaxTrackedContexts)
204+
var ctx = new CachingContext(options, hashCode);
205+
206+
if (firstUnpopulatedIndex >= 0)
209207
{
210-
if (!TryEvictDanglingEntries())
208+
// Cache has capacity -- store the context in the first available index.
209+
ref WeakReference<CachingContext>? weakRef = ref s_trackedContexts[firstUnpopulatedIndex];
210+
211+
if (weakRef is null)
212+
{
213+
weakRef = new(ctx);
214+
}
215+
else
211216
{
212-
// Cache is full; return a fresh instance.
213-
return new CachingContext(options);
217+
Debug.Assert(weakRef.TryGetTarget(out _) is false);
218+
weakRef.SetTarget(ctx);
214219
}
215220
}
216221

217-
Debug.Assert(cache.Count < MaxTrackedContexts);
218-
219-
// Use a defensive copy of the options instance as key to
220-
// avoid capturing references to any caching contexts.
221-
var key = new JsonSerializerOptions(options);
222-
Debug.Assert(key._cachingContext == null);
223-
224-
ctx = new CachingContext(options);
225-
bool success = cache.TryAdd(key, new WeakReference<CachingContext>(ctx));
226-
Debug.Assert(success);
227-
228222
return ctx;
229223
}
230224
}
231225

232-
public static void Clear()
226+
private static bool TryGetContext(
227+
JsonSerializerOptions options,
228+
int hashCode,
229+
out int firstUnpopulatedIndex,
230+
[NotNullWhen(true)] out CachingContext? result)
233231
{
234-
lock (s_cache)
235-
{
236-
s_cache.Clear();
237-
s_recentEvictionCounts.Clear();
238-
s_evictionRunsToSkip = 0;
239-
}
240-
}
241-
242-
private static bool TryEvictDanglingEntries()
243-
{
244-
// Worst case scenario, the cache has been filled with permanent entries.
245-
// Evictions are synchronized and each run is in the order of microseconds,
246-
// so we want to avoid triggering runs every time an instance is initialized,
247-
// For this reason we use a backoff strategy to average out the cost of eviction
248-
// across multiple initializations. The backoff count is determined by the eviction
249-
// rates of the most recent runs.
250-
251-
Debug.Assert(Monitor.IsEntered(s_cache));
232+
WeakReference<CachingContext>?[] trackedContexts = s_trackedContexts;
252233

253-
if (s_evictionRunsToSkip > 0)
234+
firstUnpopulatedIndex = -1;
235+
for (int i = 0; i < trackedContexts.Length; i++)
254236
{
255-
--s_evictionRunsToSkip;
256-
return false;
257-
}
237+
WeakReference<CachingContext>? weakRef = trackedContexts[i];
258238

259-
int currentEvictions = 0;
260-
foreach (KeyValuePair<JsonSerializerOptions, WeakReference<CachingContext>> kvp in s_cache)
261-
{
262-
if (!kvp.Value.TryGetTarget(out _))
239+
if (weakRef is null || !weakRef.TryGetTarget(out CachingContext? ctx))
263240
{
264-
bool result = s_cache.TryRemove(kvp.Key, out _);
265-
Debug.Assert(result);
266-
currentEvictions++;
267-
}
268-
}
269-
270-
s_evictionRunsToSkip = EstimateEvictionRunsToSkip(currentEvictions);
271-
return currentEvictions > 0;
272-
273-
// Estimate the number of eviction runs to skip based on recent eviction rates.
274-
static int EstimateEvictionRunsToSkip(int latestEvictionCount)
275-
{
276-
Queue<int> recentEvictionCounts = s_recentEvictionCounts;
277-
278-
if (recentEvictionCounts.Count < EvictionCountHistory - 1)
279-
{
280-
// Insufficient data points to determine a skip count.
281-
recentEvictionCounts.Enqueue(latestEvictionCount);
282-
return 0;
241+
if (firstUnpopulatedIndex < 0)
242+
{
243+
firstUnpopulatedIndex = i;
244+
}
283245
}
284-
else if (recentEvictionCounts.Count == EvictionCountHistory)
246+
else if (hashCode == ctx.HashCode && s_optionsComparer.Equals(options, ctx.Options))
285247
{
286-
recentEvictionCounts.Dequeue();
248+
result = ctx;
249+
return true;
287250
}
288-
289-
recentEvictionCounts.Enqueue(latestEvictionCount);
290-
291-
// Calculate the total number of eviction in the latest runs
292-
// - If we have at least one eviction per run, on average,
293-
// do not skip any future eviction runs.
294-
// - Otherwise, skip ~the number of runs needed per one eviction.
295-
296-
int totalEvictions = 0;
297-
foreach (int evictionCount in recentEvictionCounts)
298-
{
299-
totalEvictions += evictionCount;
300-
}
301-
302-
int evictionRunsToSkip =
303-
totalEvictions >= EvictionCountHistory ? 0 :
304-
(int)Math.Round((double)EvictionCountHistory / Math.Max(totalEvictions, 1));
305-
306-
Debug.Assert(0 <= evictionRunsToSkip && evictionRunsToSkip <= EvictionCountHistory);
307-
return evictionRunsToSkip;
308251
}
252+
253+
result = null;
254+
return false;
309255
}
310256
}
311257

@@ -342,6 +288,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
342288
CompareLists(left._converters, right._converters);
343289

344290
static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
291+
where TValue : class?
345292
{
346293
int n;
347294
if ((n = left.Count) != right.Count)
@@ -351,7 +298,7 @@ static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationLi
351298

352299
for (int i = 0; i < n; i++)
353300
{
354-
if (!left[i]!.Equals(right[i]))
301+
if (left[i] != right[i])
355302
{
356303
return false;
357304
}
@@ -365,35 +312,49 @@ public int GetHashCode(JsonSerializerOptions options)
365312
{
366313
HashCode hc = default;
367314

368-
hc.Add(options._dictionaryKeyPolicy);
369-
hc.Add(options._jsonPropertyNamingPolicy);
370-
hc.Add(options._readCommentHandling);
371-
hc.Add(options._referenceHandler);
372-
hc.Add(options._encoder);
373-
hc.Add(options._defaultIgnoreCondition);
374-
hc.Add(options._numberHandling);
375-
hc.Add(options._unknownTypeHandling);
376-
hc.Add(options._defaultBufferSize);
377-
hc.Add(options._maxDepth);
378-
hc.Add(options._allowTrailingCommas);
379-
hc.Add(options._ignoreNullValues);
380-
hc.Add(options._ignoreReadOnlyProperties);
381-
hc.Add(options._ignoreReadonlyFields);
382-
hc.Add(options._includeFields);
383-
hc.Add(options._propertyNameCaseInsensitive);
384-
hc.Add(options._writeIndented);
385-
hc.Add(options._typeInfoResolver);
386-
GetHashCode(ref hc, options._converters);
387-
388-
static void GetHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
315+
AddHashCode(ref hc, options._dictionaryKeyPolicy);
316+
AddHashCode(ref hc, options._jsonPropertyNamingPolicy);
317+
AddHashCode(ref hc, options._readCommentHandling);
318+
AddHashCode(ref hc, options._referenceHandler);
319+
AddHashCode(ref hc, options._encoder);
320+
AddHashCode(ref hc, options._defaultIgnoreCondition);
321+
AddHashCode(ref hc, options._numberHandling);
322+
AddHashCode(ref hc, options._unknownTypeHandling);
323+
AddHashCode(ref hc, options._defaultBufferSize);
324+
AddHashCode(ref hc, options._maxDepth);
325+
AddHashCode(ref hc, options._allowTrailingCommas);
326+
AddHashCode(ref hc, options._ignoreNullValues);
327+
AddHashCode(ref hc, options._ignoreReadOnlyProperties);
328+
AddHashCode(ref hc, options._ignoreReadonlyFields);
329+
AddHashCode(ref hc, options._includeFields);
330+
AddHashCode(ref hc, options._propertyNameCaseInsensitive);
331+
AddHashCode(ref hc, options._writeIndented);
332+
AddHashCode(ref hc, options._typeInfoResolver);
333+
AddListHashCode(ref hc, options._converters);
334+
335+
return hc.ToHashCode();
336+
337+
static void AddListHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
389338
{
390-
for (int i = 0; i < list.Count; i++)
339+
int n = list.Count;
340+
for (int i = 0; i < n; i++)
391341
{
392-
hc.Add(list[i]);
342+
AddHashCode(ref hc, list[i]);
393343
}
394344
}
395345

396-
return hc.ToHashCode();
346+
static void AddHashCode<TValue>(ref HashCode hc, TValue? value)
347+
{
348+
if (typeof(TValue).IsValueType)
349+
{
350+
hc.Add(value);
351+
}
352+
else
353+
{
354+
Debug.Assert(!typeof(TValue).IsSealed, "Sealed reference types like string should not use this method.");
355+
hc.Add(RuntimeHelpers.GetHashCode(value));
356+
}
357+
}
397358
}
398359

399360
#if !NETCOREAPP

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ public static void ClearCache(Type[]? types)
2323
options.Key.ClearCaches();
2424
}
2525

26-
// Flush the shared caching contexts
27-
JsonSerializerOptions.TrackedCachingContexts.Clear();
28-
2926
// Flush the dynamic method cache
3027
ReflectionEmitCachingMemberAccessor.Clear();
3128
}

0 commit comments

Comments
 (0)