| File: System\Windows\Forms\RefCountedCache.cs | Web Access |
| Project: src\winforms\src\System.Windows.Forms.Primitives\src\System.Windows.Forms.Primitives.csproj (System.Windows.Forms.Primitives) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; namespace System.Windows.Forms; /// <summary> /// Cache that ref counts handed-out objects via "scopes" <see cref="Scope"/>. /// </summary> /// <typeparam name="TObject"> /// The target object the cache represents. If you're caching <see cref="Drawing.Pen"/> that would be the /// type you would use here. <see cref="Scope"/> is implicitly convertible to this type. /// </typeparam> /// <typeparam name="TCacheEntryData"> /// The type of data to associate with a cache entry. For a simple cache this can be the same type as /// <typeparamref name="TKey"/>. /// </typeparam> /// <typeparam name="TKey"> /// The type of key used to look up cache entries. /// </typeparam> internal abstract partial class RefCountedCache<TObject, TCacheEntryData, TKey> : IDisposable { private readonly SinglyLinkedList<CacheEntry> _list = new(); private readonly int _softLimit; private readonly int _hardLimit; // Retrieving any node takes at least 300ns (locking, complex matching, etc. can add more time). It can take // 5 to 10ns for every step through the linked list with a simple key match. Shifting cost isn't expensive // (as SinglyLinkedList is optimized for this). It costs around 30-50ns to move an object to the front of the // list. We'll try to move to the front of the list when we think we'll get significant improvements in future // accesses that make up for the cost of moving without unduly impacting other frequently accessed items. // // Doing this also has a "de-aging" effect as we cull from the back of the list when looking for new space. private const int MoveToFront = 10; public RefCountedCache(int softLimit = 20, int hardLimit = 40) { Debug.Assert(softLimit > 0 && hardLimit > 0); Debug.Assert(softLimit <= hardLimit); _softLimit = softLimit; _hardLimit = hardLimit; } /// <summary> /// Override this to create a new <see cref="CacheEntry"/> for the given <paramref name="key"/>. /// </summary> /// <param name="cached"> /// True if the entry is actually kept in the cache. When the cache hits the hard limit entries aren't /// kept in the cache and need to be cleaned up when the ref count drops to zero. /// </param> protected abstract CacheEntry CreateEntry(TKey key, bool cached); /// <summary> /// Return true if the given <paramref name="key"/> matches the given <paramref name="entry"/>. /// </summary> protected abstract bool IsMatch(TKey key, CacheEntry entry); /// <summary> /// Find or create the entry for <paramref name="key"/>. /// </summary> /// <remarks> /// <para> /// Override if you want to modify behavior or lock cache access. /// </para> /// </remarks> public virtual CacheEntry GetEntry(TKey key) { // NOTE: Measure carefully when changing logic in this method. Code has been optimized for performance. ArgumentNullException.ThrowIfNull(key); if (!Find(key, out CacheEntry entry)) { entry = Add(key); } return entry; [SkipLocalsInit] bool Find(TKey key, out CacheEntry entry) { bool success = false; entry = default!; int position = MoveToFront; var enumerator = _list.GetEnumerator(); while (enumerator.MoveNext()) { var node = enumerator.Current; CacheEntry currentEntry = node.Value; if (IsMatch(key, currentEntry)) { entry = currentEntry; if (position < 0) { // Moving to the front as the cost of walking this far in outweighs the cost of moving // the node to the front of the list. enumerator.MoveCurrentToFront(); } success = true; break; } position--; } return success; } [SkipLocalsInit] CacheEntry Add(TKey key) { CacheEntry entry; if (_list.Count >= _softLimit) { // Try to free up space Clean(); } if (_list.Count < _hardLimit) { // We've got space, add to the cache entry = CreateEntry(key, cached: true); _list.AddFirst(entry); } else { entry = CreateEntry(key, cached: false); } return entry; } void Clean() { // If the count is over the soft limit, try to get back under int overage = _list.Count - _softLimit; if (overage > 0) { // Skip to the last part of the list and try to remove what we can int skip = _list.Count - overage; var enumerator = _list.GetEnumerator(); int removed = 0; while (enumerator.MoveNext()) { skip--; if (skip < 0) { var node = enumerator.Current; if (node!.Value.RefCount == 0) { enumerator.RemoveCurrent(); node.Value.Dispose(); removed++; } } } // All of the end of the list is in use? Are we leaking ref counts? Debug.Assert(removed != 0 || _softLimit < 20); } } } protected virtual void Dispose(bool disposing) { if (disposing) { var enumerator = _list.GetEnumerator(); while (enumerator.MoveNext()) { enumerator.Current!.Value.Dispose(); enumerator.RemoveCurrent(); } } } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } }