@@ -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
0 commit comments