@@ -143,13 +143,15 @@ internal sealed class CachingContext
143
143
private readonly ConcurrentDictionary < Type , JsonTypeInfo ? > _jsonTypeInfoCache = new ( ) ;
144
144
private readonly Func < Type , JsonTypeInfo ? > _jsonTypeInfoFactory ;
145
145
146
- public CachingContext ( JsonSerializerOptions options )
146
+ public CachingContext ( JsonSerializerOptions options , int hashCode )
147
147
{
148
148
Options = options ;
149
+ HashCode = hashCode ;
149
150
_jsonTypeInfoFactory = Options . GetTypeInfoNoCaching ;
150
151
}
151
152
152
153
public JsonSerializerOptions Options { get ; }
154
+ public int HashCode { get ; }
153
155
// Property only accessed by reflection in testing -- do not remove.
154
156
// If changing please ensure that src/ILLink.Descriptors.LibraryBuild.xml is up-to-date.
155
157
public int Count => _jsonTypeInfoCache . Count ;
@@ -166,146 +168,90 @@ public void Clear()
166
168
167
169
/// <summary>
168
170
/// 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"/>.
171
173
/// </summary>
172
174
internal static class TrackedCachingContexts
173
175
{
174
176
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 ( ) ;
181
179
182
180
public static CachingContext GetOrCreate ( JsonSerializerOptions options )
183
181
{
184
182
Debug . Assert ( options . IsImmutable , "Cannot create caching contexts for mutable JsonSerializerOptions instances" ) ;
185
183
Debug . Assert ( options . _typeInfoResolver != null ) ;
186
184
187
- ConcurrentDictionary < JsonSerializerOptions , WeakReference < CachingContext > > cache = s_cache ;
185
+ int hashCode = s_optionsComparer . GetHashCode ( options ) ;
188
186
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 ) )
190
188
{
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 ) ;
192
195
}
193
196
194
- lock ( cache )
197
+ lock ( s_trackedContexts )
195
198
{
196
- if ( cache . TryGetValue ( options , out wr ) )
199
+ if ( TryGetContext ( options , hashCode , out firstUnpopulatedIndex , out result ) )
197
200
{
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 ;
206
202
}
207
203
208
- if ( cache . Count == MaxTrackedContexts )
204
+ var ctx = new CachingContext ( options , hashCode ) ;
205
+
206
+ if ( firstUnpopulatedIndex >= 0 )
209
207
{
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
211
216
{
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 ) ;
214
219
}
215
220
}
216
221
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
-
228
222
return ctx ;
229
223
}
230
224
}
231
225
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 )
233
231
{
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 ;
252
233
253
- if ( s_evictionRunsToSkip > 0 )
234
+ firstUnpopulatedIndex = - 1 ;
235
+ for ( int i = 0 ; i < trackedContexts . Length ; i ++ )
254
236
{
255
- -- s_evictionRunsToSkip ;
256
- return false ;
257
- }
237
+ WeakReference < CachingContext > ? weakRef = trackedContexts [ i ] ;
258
238
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 ) )
263
240
{
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
+ }
283
245
}
284
- else if ( recentEvictionCounts . Count == EvictionCountHistory )
246
+ else if ( hashCode == ctx . HashCode && s_optionsComparer . Equals ( options , ctx . Options ) )
285
247
{
286
- recentEvictionCounts . Dequeue ( ) ;
248
+ result = ctx ;
249
+ return true ;
287
250
}
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 ;
308
251
}
252
+
253
+ result = null ;
254
+ return false ;
309
255
}
310
256
}
311
257
@@ -342,6 +288,7 @@ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
342
288
CompareLists ( left . _converters , right . _converters ) ;
343
289
344
290
static bool CompareLists < TValue > ( ConfigurationList < TValue > left , ConfigurationList < TValue > right )
291
+ where TValue : class ?
345
292
{
346
293
int n ;
347
294
if ( ( n = left . Count ) != right . Count )
@@ -351,7 +298,7 @@ static bool CompareLists<TValue>(ConfigurationList<TValue> left, ConfigurationLi
351
298
352
299
for ( int i = 0 ; i < n ; i ++ )
353
300
{
354
- if ( ! left [ i ] ! . Equals ( right [ i ] ) )
301
+ if ( left [ i ] != right [ i ] )
355
302
{
356
303
return false ;
357
304
}
@@ -365,35 +312,49 @@ public int GetHashCode(JsonSerializerOptions options)
365
312
{
366
313
HashCode hc = default ;
367
314
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 )
389
338
{
390
- for ( int i = 0 ; i < list . Count ; i ++ )
339
+ int n = list . Count ;
340
+ for ( int i = 0 ; i < n ; i ++ )
391
341
{
392
- hc . Add ( list [ i ] ) ;
342
+ AddHashCode ( ref hc , list [ i ] ) ;
393
343
}
394
344
}
395
345
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
+ }
397
358
}
398
359
399
360
#if ! NETCOREAPP
0 commit comments