Skip to content

Commit fe921e0

Browse files
Use hashcodes when looking up the JsonSerializerOptions global cache. (#76782)
* Use hashcodes when looking up the JsonSerializerOptions global cache. * Address feedback.
1 parent 4941a08 commit fe921e0

File tree

2 files changed

+132
-56
lines changed

2 files changed

+132
-56
lines changed

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

Lines changed: 110 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,14 @@ internal sealed class CachingContext
142142
{
143143
private readonly ConcurrentDictionary<Type, JsonTypeInfo?> _jsonTypeInfoCache = new();
144144

145-
public CachingContext(JsonSerializerOptions options)
145+
public CachingContext(JsonSerializerOptions options, int hashCode)
146146
{
147147
Options = options;
148+
HashCode = hashCode;
148149
}
149150

150151
public JsonSerializerOptions Options { get; }
152+
public int HashCode { get; }
151153
// Property only accessed by reflection in testing -- do not remove.
152154
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
153155
public int Count => _jsonTypeInfoCache.Count;
@@ -164,37 +166,39 @@ public void Clear()
164166
/// <summary>
165167
/// Defines a cache of CachingContexts; instead of using a ConditionalWeakTable which can be slow to traverse
166168
/// this approach uses a fixed-size array of weak references of <see cref="CachingContext"/> that can be looked up lock-free.
167-
/// Relevant caching contexts are looked up by linear traversal using the equality comparison defined by
168-
/// <see cref="AreEquivalentOptions(JsonSerializerOptions, JsonSerializerOptions)"/>.
169+
/// Relevant caching contexts are looked up by linear traversal using the equality comparison defined by <see cref="EqualityComparer"/>.
169170
/// </summary>
170171
internal static class TrackedCachingContexts
171172
{
172173
private const int MaxTrackedContexts = 64;
173174
private static readonly WeakReference<CachingContext>?[] s_trackedContexts = new WeakReference<CachingContext>[MaxTrackedContexts];
175+
private static readonly EqualityComparer s_optionsComparer = new();
174176

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

180-
if (TryGetContext(options, out int firstUnpopulatedIndex, out CachingContext? result))
182+
int hashCode = s_optionsComparer.GetHashCode(options);
183+
184+
if (TryGetContext(options, hashCode, out int firstUnpopulatedIndex, out CachingContext? result))
181185
{
182186
return result;
183187
}
184188
else if (firstUnpopulatedIndex < 0)
185189
{
186190
// Cache is full; return a fresh instance.
187-
return new CachingContext(options);
191+
return new CachingContext(options, hashCode);
188192
}
189193

190194
lock (s_trackedContexts)
191195
{
192-
if (TryGetContext(options, out firstUnpopulatedIndex, out result))
196+
if (TryGetContext(options, hashCode, out firstUnpopulatedIndex, out result))
193197
{
194198
return result;
195199
}
196200

197-
var ctx = new CachingContext(options);
201+
var ctx = new CachingContext(options, hashCode);
198202

199203
if (firstUnpopulatedIndex >= 0)
200204
{
@@ -218,6 +222,7 @@ public static CachingContext GetOrCreate(JsonSerializerOptions options)
218222

219223
private static bool TryGetContext(
220224
JsonSerializerOptions options,
225+
int hashCode,
221226
out int firstUnpopulatedIndex,
222227
[NotNullWhen(true)] out CachingContext? result)
223228
{
@@ -235,7 +240,7 @@ private static bool TryGetContext(
235240
firstUnpopulatedIndex = i;
236241
}
237242
}
238-
else if (AreEquivalentOptions(options, ctx.Options))
243+
else if (hashCode == ctx.HashCode && s_optionsComparer.Equals(options, ctx.Options))
239244
{
240245
result = ctx;
241246
return true;
@@ -252,52 +257,114 @@ private static bool TryGetContext(
252257
/// If two instances are equivalent, they should generate identical metadata caches;
253258
/// the converse however does not necessarily hold.
254259
/// </summary>
255-
private static bool AreEquivalentOptions(JsonSerializerOptions left, JsonSerializerOptions right)
260+
private sealed class EqualityComparer : IEqualityComparer<JsonSerializerOptions>
256261
{
257-
Debug.Assert(left != null && right != null);
258-
259-
return
260-
left._dictionaryKeyPolicy == right._dictionaryKeyPolicy &&
261-
left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy &&
262-
left._readCommentHandling == right._readCommentHandling &&
263-
left._referenceHandler == right._referenceHandler &&
264-
left._encoder == right._encoder &&
265-
left._defaultIgnoreCondition == right._defaultIgnoreCondition &&
266-
left._numberHandling == right._numberHandling &&
267-
left._unknownTypeHandling == right._unknownTypeHandling &&
268-
left._defaultBufferSize == right._defaultBufferSize &&
269-
left._maxDepth == right._maxDepth &&
270-
left._allowTrailingCommas == right._allowTrailingCommas &&
271-
left._ignoreNullValues == right._ignoreNullValues &&
272-
left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties &&
273-
left._ignoreReadonlyFields == right._ignoreReadonlyFields &&
274-
left._includeFields == right._includeFields &&
275-
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
276-
left._writeIndented == right._writeIndented &&
277-
left._typeInfoResolver == right._typeInfoResolver &&
278-
CompareLists(left._converters, right._converters);
279-
280-
static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
262+
public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
263+
{
264+
Debug.Assert(left != null && right != null);
265+
266+
return
267+
left._dictionaryKeyPolicy == right._dictionaryKeyPolicy &&
268+
left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy &&
269+
left._readCommentHandling == right._readCommentHandling &&
270+
left._referenceHandler == right._referenceHandler &&
271+
left._encoder == right._encoder &&
272+
left._defaultIgnoreCondition == right._defaultIgnoreCondition &&
273+
left._numberHandling == right._numberHandling &&
274+
left._unknownTypeHandling == right._unknownTypeHandling &&
275+
left._defaultBufferSize == right._defaultBufferSize &&
276+
left._maxDepth == right._maxDepth &&
277+
left._allowTrailingCommas == right._allowTrailingCommas &&
278+
left._ignoreNullValues == right._ignoreNullValues &&
279+
left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties &&
280+
left._ignoreReadonlyFields == right._ignoreReadonlyFields &&
281+
left._includeFields == right._includeFields &&
282+
left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
283+
left._writeIndented == right._writeIndented &&
284+
left._typeInfoResolver == right._typeInfoResolver &&
285+
CompareLists(left._converters, right._converters);
286+
287+
static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationList<TValue> right)
288+
where TValue : class?
289+
{
290+
int n;
291+
if ((n = left.Count) != right.Count)
292+
{
293+
return false;
294+
}
295+
296+
for (int i = 0; i < n; i++)
297+
{
298+
if (left[i] != right[i])
299+
{
300+
return false;
301+
}
302+
}
303+
304+
return true;
305+
}
306+
}
307+
308+
public int GetHashCode(JsonSerializerOptions options)
281309
{
282-
int n;
283-
if ((n = left.Count) != right.Count)
310+
HashCode hc = default;
311+
312+
AddHashCode(ref hc, options._dictionaryKeyPolicy);
313+
AddHashCode(ref hc, options._jsonPropertyNamingPolicy);
314+
AddHashCode(ref hc, options._readCommentHandling);
315+
AddHashCode(ref hc, options._referenceHandler);
316+
AddHashCode(ref hc, options._encoder);
317+
AddHashCode(ref hc, options._defaultIgnoreCondition);
318+
AddHashCode(ref hc, options._numberHandling);
319+
AddHashCode(ref hc, options._unknownTypeHandling);
320+
AddHashCode(ref hc, options._defaultBufferSize);
321+
AddHashCode(ref hc, options._maxDepth);
322+
AddHashCode(ref hc, options._allowTrailingCommas);
323+
AddHashCode(ref hc, options._ignoreNullValues);
324+
AddHashCode(ref hc, options._ignoreReadOnlyProperties);
325+
AddHashCode(ref hc, options._ignoreReadonlyFields);
326+
AddHashCode(ref hc, options._includeFields);
327+
AddHashCode(ref hc, options._propertyNameCaseInsensitive);
328+
AddHashCode(ref hc, options._writeIndented);
329+
AddHashCode(ref hc, options._typeInfoResolver);
330+
AddListHashCode(ref hc, options._converters);
331+
332+
return hc.ToHashCode();
333+
334+
static void AddListHashCode<TValue>(ref HashCode hc, ConfigurationList<TValue> list)
284335
{
285-
return false;
336+
int n = list.Count;
337+
for (int i = 0; i < n; i++)
338+
{
339+
AddHashCode(ref hc, list[i]);
340+
}
286341
}
287342

288-
for (int i = 0; i < n; i++)
343+
static void AddHashCode<TValue>(ref HashCode hc, TValue? value)
289344
{
290-
TValue? leftElem = left[i];
291-
TValue? rightElem = right[i];
292-
bool areEqual = leftElem is null ? rightElem is null : leftElem.Equals(rightElem);
293-
if (!areEqual)
345+
if (typeof(TValue).IsValueType)
294346
{
295-
return false;
347+
hc.Add(value);
348+
}
349+
else
350+
{
351+
Debug.Assert(!typeof(TValue).IsSealed, "Sealed reference types like string should not use this method.");
352+
hc.Add(RuntimeHelpers.GetHashCode(value));
296353
}
297354
}
355+
}
298356

299-
return true;
357+
#if !NETCOREAPP
358+
/// <summary>
359+
/// Polyfill for System.HashCode.
360+
/// </summary>
361+
private struct HashCode
362+
{
363+
private int _hashCode;
364+
public void Add<T>(T? value) => _hashCode = (_hashCode, value).GetHashCode();
365+
public int ToHashCode() => _hashCode;
300366
}
367+
#endif
301368
}
302369
}
303370
}

src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/CacheTests.cs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ static Func<JsonSerializerOptions, int> CreateCacheCountAccessor()
257257

258258
[ActiveIssue("https://github.com/dotnet/runtime/issues/66232", TargetFrameworkMonikers.NetFramework)]
259259
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
260+
[MemberData(nameof(GetJsonSerializerOptions))]
260261
public static void JsonSerializerOptions_ReuseConverterCaches()
261262
{
262263
// This test uses reflection to:
@@ -268,7 +269,7 @@ public static void JsonSerializerOptions_ReuseConverterCaches()
268269
RemoteExecutor.Invoke(static () =>
269270
{
270271
Func<JsonSerializerOptions, JsonSerializerOptions?> getCacheOptions = CreateCacheOptionsAccessor();
271-
Func<JsonSerializerOptions, JsonSerializerOptions, bool> equalityComparer = CreateEqualityComparerAccessor();
272+
IEqualityComparer<JsonSerializerOptions> equalityComparer = CreateEqualityComparerAccessor();
272273

273274
foreach (var args in GetJsonSerializerOptions())
274275
{
@@ -279,7 +280,8 @@ public static void JsonSerializerOptions_ReuseConverterCaches()
279280

280281
JsonSerializerOptions originalCacheOptions = getCacheOptions(options);
281282
Assert.NotNull(originalCacheOptions);
282-
Assert.True(equalityComparer(options, originalCacheOptions));
283+
Assert.True(equalityComparer.Equals(options, originalCacheOptions));
284+
Assert.Equal(equalityComparer.GetHashCode(options), equalityComparer.GetHashCode(originalCacheOptions));
283285

284286
for (int i = 0; i < 5; i++)
285287
{
@@ -288,7 +290,8 @@ public static void JsonSerializerOptions_ReuseConverterCaches()
288290

289291
JsonSerializer.Serialize(42, options2);
290292

291-
Assert.True(equalityComparer(options2, originalCacheOptions));
293+
Assert.True(equalityComparer.Equals(options2, originalCacheOptions));
294+
Assert.Equal(equalityComparer.GetHashCode(options2), equalityComparer.GetHashCode(originalCacheOptions));
292295
Assert.Same(originalCacheOptions, getCacheOptions(options2));
293296
}
294297
}
@@ -324,7 +327,7 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou
324327
// - All public setters in JsonSerializerOptions
325328
//
326329
// If either of them changes, this test will need to be kept in sync.
327-
Func<JsonSerializerOptions, JsonSerializerOptions, bool> equalityComparer = CreateEqualityComparerAccessor();
330+
IEqualityComparer<JsonSerializerOptions> equalityComparer = CreateEqualityComparerAccessor();
328331

329332
(PropertyInfo prop, object value)[] propertySettersAndValues = GetPropertiesWithSettersAndNonDefaultValues().ToArray();
330333

@@ -334,16 +337,19 @@ public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShou
334337
Assert.Fail($"{nameof(GetPropertiesWithSettersAndNonDefaultValues)} missing property declaration for {prop.Name}, please update the method.");
335338
}
336339

337-
Assert.True(equalityComparer(JsonSerializerOptions.Default, JsonSerializerOptions.Default));
340+
Assert.True(equalityComparer.Equals(JsonSerializerOptions.Default, JsonSerializerOptions.Default));
341+
Assert.Equal(equalityComparer.GetHashCode(JsonSerializerOptions.Default), equalityComparer.GetHashCode(JsonSerializerOptions.Default));
338342

339343
foreach ((PropertyInfo prop, object? value) in propertySettersAndValues)
340344
{
341345
var options = new JsonSerializerOptions();
342346
prop.SetValue(options, value);
343347

344-
Assert.True(equalityComparer(options, options));
348+
Assert.True(equalityComparer.Equals(options, options));
349+
Assert.Equal(equalityComparer.GetHashCode(options), equalityComparer.GetHashCode(options));
345350

346-
Assert.False(equalityComparer(JsonSerializerOptions.Default, options));
351+
Assert.False(equalityComparer.Equals(JsonSerializerOptions.Default, options));
352+
Assert.NotEqual(equalityComparer.GetHashCode(JsonSerializerOptions.Default), equalityComparer.GetHashCode(options));
347353
}
348354

349355
static IEnumerable<(PropertyInfo, object)> GetPropertiesWithSettersAndNonDefaultValues()
@@ -389,14 +395,16 @@ public static void JsonSerializerOptions_EqualityComparer_ApplyingJsonSerializer
389395
//
390396
// If either of them changes, this test will need to be kept in sync.
391397

392-
Func<JsonSerializerOptions, JsonSerializerOptions, bool> equalityComparer = CreateEqualityComparerAccessor();
398+
IEqualityComparer<JsonSerializerOptions> equalityComparer = CreateEqualityComparerAccessor();
393399
var options1 = new JsonSerializerOptions { WriteIndented = true };
394400
var options2 = new JsonSerializerOptions { WriteIndented = true };
395401

396-
Assert.True(equalityComparer(options1, options2));
402+
Assert.True(equalityComparer.Equals(options1, options2));
403+
Assert.Equal(equalityComparer.GetHashCode(options1), equalityComparer.GetHashCode(options2));
397404

398405
_ = new MyJsonContext(options1); // Associate copy with a JsonSerializerContext
399-
Assert.False(equalityComparer(options1, options2));
406+
Assert.False(equalityComparer.Equals(options1, options2));
407+
Assert.NotEqual(equalityComparer.GetHashCode(options1), equalityComparer.GetHashCode(options2));
400408
}
401409

402410
private class MyJsonContext : JsonSerializerContext
@@ -408,10 +416,11 @@ public MyJsonContext(JsonSerializerOptions options) : base(options) { }
408416
protected override JsonSerializerOptions? GeneratedSerializerOptions => Options;
409417
}
410418

411-
public static Func<JsonSerializerOptions, JsonSerializerOptions, bool> CreateEqualityComparerAccessor()
419+
public static IEqualityComparer<JsonSerializerOptions> CreateEqualityComparerAccessor()
412420
{
413-
MethodInfo equalityComparerMethod = typeof(JsonSerializerOptions).GetMethod("AreEquivalentOptions", BindingFlags.NonPublic | BindingFlags.Static);
414-
return (Func<JsonSerializerOptions, JsonSerializerOptions, bool>)Delegate.CreateDelegate(typeof(Func<JsonSerializerOptions, JsonSerializerOptions, bool>), equalityComparerMethod);
421+
Type equalityComparerType = typeof(JsonSerializerOptions).GetNestedType("EqualityComparer", BindingFlags.NonPublic);
422+
Assert.NotNull(equalityComparerType);
423+
return (IEqualityComparer<JsonSerializerOptions>)Activator.CreateInstance(equalityComparerType, nonPublic: true);
415424
}
416425

417426
public static IEnumerable<object[]> WriteSuccessCases

0 commit comments

Comments
 (0)