< Summary

Information
Class: Trailblazer.Pathing.ReusableSurveyResultCache<T>
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/Survey/ReusableSurveyResultCache.cs
Line coverage
100%
Covered lines: 199
Uncovered lines: 0
Coverable lines: 199
Total lines: 468
Line coverage: 100%
Branch coverage
94%
Covered branches: 134
Total branches: 142
Branch coverage: 94.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_Count()100%11100%
get_CountInUse()100%11100%
set_CountInUse(...)100%11100%
TryGetOrCreate(...)100%1212100%
Return(...)92.85%1414100%
EvictStaleEntries(...)100%1212100%
TryCheckout(...)100%44100%
TrySeed(...)100%2020100%
CountIndexedEntriesForChart(...)100%44100%
InvalidateForChart(...)92.85%1414100%
InvalidateWhere(...)100%1212100%
InvalidateAll()100%44100%
TryGetLeastRecentlyUsedReusableEntry(...)100%1010100%
AddCachedResult(...)100%22100%
CheckoutCachedResult(...)100%11100%
InvalidateCachedResult(...)100%22100%
RemoveCachedResult(...)100%11100%
AddToChartIndex(...)83.33%1212100%
RemoveFromChartIndex(...)78.57%1414100%
ContainsPriorChartKey(...)100%44100%
Dispose()50%22100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/Survey/ReusableSurveyResultCache.cs

#LineLine coverage
 1using SwiftCollections;
 2using System;
 3using System.Collections.Generic;
 4using System.Threading;
 5
 6namespace Trailblazer.Pathing;
 7
 8/// <summary>
 9/// Caches and reuses <see cref="ISurveyResult"/> instances to reduce allocation overhead
 10/// and improve pathfinding performance.
 11/// Supports LRU eviction and optional pooling of released guides.
 12/// </summary>
 13internal class ReusableSurveyResultCache<T> : IDisposable where T : SurveyResult
 14{
 15    /// <summary>
 16    /// Maximum number of <see cref="SurveyResult"/>s allowed in the cache before eviction occurs.
 17    /// </summary>
 18    private const int MaxCacheSize = 128;
 19
 20    /// <summary>
 21    /// Active <see cref="SurveyResult"/>s cache indexed by the request's cache key.
 22    /// </summary>
 385623    private readonly SwiftDictionary<int, T> _cache = new();
 24
 25    /// <summary>
 26    /// Reverse lookup from chart key to cached request keys that reference the chart.
 27    /// </summary>
 385628    private readonly SwiftDictionary<string, SwiftList<int>> _chartIndex = new(8, StringComparer.Ordinal);
 29
 385630    private readonly SwiftList<int> _staleKeys = new(MaxCacheSize);
 31
 385632    private readonly ReaderWriterLockSlim _lock = new();
 33
 34    /// <summary>
 35    /// Gets the total number of cached and pooled <see cref="SurveyResult"/> instances.
 36    /// </summary>
 1567537    public int Count => _cache.Count;
 38
 39    private int _countInUse;
 40
 41    public int CountInUse
 42    {
 454243        get => _countInUse;
 641444        private set => _countInUse = Math.Max(0, value);
 45    }
 46
 47    /// <summary>
 48    /// Attempts to retrieve a valid <see cref="SurveyResult"/> from the cache,
 49    /// or creates and initializes a new one if none are reusable.
 50    /// Evicts the least recently used guide if the cache is at capacity.
 51    /// </summary>
 52    /// <param name="request">The path request used as the cache key.</param>
 53    /// <param name="create">Factory method to create a new guide instance.</param>
 54    /// <param name="result">The resulting <see cref="SurveyResult"/> instance.</param>
 55    /// <returns>True if a valid <see cref="SurveyResult"/> was obtained; otherwise, false.</returns>
 56    public bool TryGetOrCreate(IPathRequest request, Func<T> create, out T result)
 57    {
 72958        int key = request.RequestCacheKey;
 59
 72960        _lock.EnterUpgradeableReadLock();
 61        try
 62        {
 72963            if (_cache.TryGetValue(key, out result) && result.HasPath)
 64            {
 465                result.Context = request.Context;
 466                CheckoutCachedResult(result);
 467                return true;
 68            }
 69
 72570            if (_cache.Count >= MaxCacheSize)
 71            {
 672                if (TryGetLeastRecentlyUsedReusableEntry(out int evictKey, out T evictCandidate))
 73                {
 474                    _lock.EnterWriteLock();
 1675                    try { RemoveCachedResult(evictKey, evictCandidate); } finally { _lock.ExitWriteLock(); }
 76                }
 77            }
 78
 72579            result = create();
 72580            if (result.HasPath)
 81            {
 71082                result.Context = request.Context;
 71083                _lock.EnterWriteLock();
 84                try
 85                {
 71086                    if (_cache.Count < MaxCacheSize)
 70887                        AddCachedResult(key, result);
 88
 71089                    result.Checkout();
 71090                    CountInUse++;
 71091                }
 142092                finally { _lock.ExitWriteLock(); }
 93
 71094                return true;
 95            }
 96
 1597            return false;
 98        }
 145899        finally { _lock.ExitUpgradeableReadLock(); }
 729100    }
 101
 102    /// <summary>
 103    /// Returns a <see cref="SurveyResult"/> to the pool or disposes it based on the given flag.
 104    /// Also removes invalid guides from the active cache.
 105    /// </summary>
 106    /// <param name="result">The <see cref="SurveyResult"/> to return.</param>
 107    /// <param name="dispose">Whether the <see cref="SurveyResult"/> should be disposed and not pooled.</param>
 108    public void Return(T result, bool dispose)
 109    {
 2216110        if (result == null) return;
 111
 2216112        _lock.EnterWriteLock();
 113        try
 114        {
 2216115            if (result.IsInUse)
 2215116                CountInUse--;
 117
 2216118            if (result.HasPath && !dispose)
 119            {
 2208120                result.Release();
 2208121                return;
 122            }
 123
 8124            if (result.RequestHashKey >= 0
 8125                && _cache.TryGetValue(result.RequestHashKey, out T cached)
 8126                && ReferenceEquals(cached, result))
 127            {
 2128                RemoveCachedResult(result.RequestHashKey, result);
 129            }
 8130        }
 4432131        finally { _lock.ExitWriteLock(); }
 2216132    }
 133
 134    /// <summary>
 135    /// Evicts <see cref="SurveyResult"/> from the cache that have not been used within the specified expiration window.
 136    /// <see cref="SurveyResult"/> that are not in use are optionally returned to the pool.
 137    /// </summary>
 138    /// <param name="currentFrame">The current simulation frame.</param>
 139    /// <param name="expiration">The number of frames after which a <see cref="SurveyResult"/> is considered stale.</par
 140    internal void EvictStaleEntries(int currentFrame, int expiration)
 141    {
 58142        _lock.EnterUpgradeableReadLock();
 143        try
 144        {
 58145            _staleKeys.Clear();
 276146            foreach (KeyValuePair<int, T> kvp in _cache)
 147            {
 80148                if (!kvp.Value.IsInUse && currentFrame - kvp.Value.LastUsedFrame > expiration)
 33149                    _staleKeys.Add(kvp.Key);
 150            }
 151
 58152            if (_staleKeys.Count == 0)
 56153                return;
 154
 2155            _lock.EnterWriteLock();
 156            try
 157            {
 70158                for (int i = 0; i < _staleKeys.Count; i++)
 159                {
 33160                    int key = _staleKeys[i];
 33161                    if (_cache.TryGetValue(key, out T result))
 33162                        RemoveCachedResult(key, result);
 163                }
 2164            }
 4165            finally { _lock.ExitWriteLock(); }
 166
 167        }
 168        finally
 169        {
 58170            _staleKeys.Clear();
 58171            _lock.ExitUpgradeableReadLock();
 58172        }
 58173    }
 174
 175    /// <summary>
 176    /// Attempts to check out an existing cached result without invoking a creation callback.
 177    /// </summary>
 178    /// <param name="request">The path request used as the cache key.</param>
 179    /// <param name="result">The checked-out cached result.</param>
 180    /// <returns><c>true</c> when a valid cached result was found; otherwise, <c>false</c>.</returns>
 181    public bool TryCheckout(IPathRequest request, out T result)
 182    {
 1696183        int key = request.RequestCacheKey;
 184
 1696185        _lock.EnterUpgradeableReadLock();
 186        try
 187        {
 1696188            if (_cache.TryGetValue(key, out result) && result.HasPath)
 189            {
 1552190                result.Context = request.Context;
 1552191                CheckoutCachedResult(result);
 1552192                return true;
 193            }
 194
 144195            result = null!;
 144196            return false;
 197        }
 3392198        finally { _lock.ExitUpgradeableReadLock(); }
 1696199    }
 200
 201    /// <summary>
 202    /// Seeds a valid result directly into the cache for internal benchmark and test fixtures.
 203    /// </summary>
 204    /// <remarks>
 205    /// This intentionally does not evict entries; callers use it to create exact cache-pressure
 206    /// shapes and should choose unique request keys up to the cache capacity.
 207    /// </remarks>
 208    internal bool TrySeed(T result, bool checkout)
 209    {
 149210        if (result == null || !result.HasPath || result.RequestHashKey < 0 || result.Context == null)
 4211            return false;
 212
 145213        int key = result.RequestHashKey;
 214
 145215        _lock.EnterWriteLock();
 216        try
 217        {
 144218            if (_cache.TryGetValue(key, out T existing))
 219            {
 2220                if (existing.IsInUse)
 2221                    CountInUse--;
 222
 2223                RemoveCachedResult(key, existing);
 2224                if (!ReferenceEquals(existing, result))
 1225                    existing.Reset();
 226            }
 142227            else if (_cache.Count >= MaxCacheSize)
 228            {
 1229                return false;
 230            }
 231
 143232            if (result.IsInUse)
 1233                result.Release();
 234
 143235            if (checkout)
 236            {
 3237                result.Checkout();
 3238                CountInUse++;
 239            }
 240
 143241            AddCachedResult(key, result);
 143242            return true;
 243        }
 288244        finally { _lock.ExitWriteLock(); }
 144245    }
 246
 247    internal int CountIndexedEntriesForChart(string chartKey)
 248    {
 11249        if (string.IsNullOrEmpty(chartKey))
 1250            return 0;
 251
 10252        _lock.EnterReadLock();
 253        try
 254        {
 10255            return _chartIndex.TryGetValue(chartKey, out SwiftList<int> keys)
 10256                ? keys.Count
 10257                : 0;
 258        }
 20259        finally { _lock.ExitReadLock(); }
 10260    }
 261
 262    /// <summary>
 263    /// Invalidates cached results that reference the specified chart key by using the chart reverse index.
 264    /// </summary>
 265    /// <param name="chartKey">The chart key whose dependent cached results should be removed.</param>
 266    public void InvalidateForChart(string chartKey)
 267    {
 4932268        if (string.IsNullOrEmpty(chartKey)) return;
 269
 4932270        _lock.EnterUpgradeableReadLock();
 271        try
 272        {
 4932273            if (!_chartIndex.TryGetValue(chartKey, out SwiftList<int> indexedKeys)
 4932274                || indexedKeys.Count == 0)
 275            {
 4878276                return;
 277            }
 278
 54279            _lock.EnterWriteLock();
 280            try
 281            {
 240282                while (_chartIndex.TryGetValue(chartKey, out indexedKeys)
 240283                    && indexedKeys.Count > 0)
 284                {
 186285                    int key = indexedKeys[indexedKeys.Count - 1];
 186286                    if (_cache.TryGetValue(key, out T result))
 287                    {
 185288                        InvalidateCachedResult(key, result);
 289                    }
 290                    else
 291                    {
 1292                        indexedKeys.Remove(key);
 1293                        if (indexedKeys.Count == 0)
 1294                            _chartIndex.Remove(chartKey);
 295                    }
 296                }
 54297            }
 108298            finally { _lock.ExitWriteLock(); }
 299        }
 9864300        finally { _lock.ExitUpgradeableReadLock(); }
 4932301    }
 302
 303    public void InvalidateWhere(Func<T, bool> predicate)
 304    {
 2305        SwiftList<int> toRemove = new();
 2306        _lock.EnterUpgradeableReadLock();
 307        try
 308        {
 8309            foreach (KeyValuePair<int, T> kvp in _cache)
 310            {
 2311                if (kvp.Value == null || !predicate(kvp.Value))
 312                    continue;
 313
 2314                toRemove.Add(kvp.Key);
 315            }
 316
 2317            if (toRemove.Count == 0)
 1318                return;
 319
 1320            _lock.EnterWriteLock();
 321            try
 322            {
 6323                foreach (int key in toRemove)
 324                {
 2325                    if (_cache.TryGetValue(key, out T result))
 2326                        InvalidateCachedResult(key, result);
 327                }
 328            }
 2329            finally { _lock.ExitWriteLock(); }
 330        }
 4331        finally { _lock.ExitUpgradeableReadLock(); }
 2332    }
 333
 334    public void InvalidateAll()
 335    {
 1915336        _lock.EnterWriteLock();
 337        try
 338        {
 3982339            foreach (KeyValuePair<int, T> kvp in _cache)
 340            {
 76341                if (kvp.Value == null) continue;
 342
 76343                kvp.Value.Reset();
 344            }
 345
 1915346            _cache.Clear();
 1915347            _chartIndex.Clear();
 1915348            CountInUse = 0;
 1915349        }
 3830350        finally { _lock.ExitWriteLock(); }
 1915351    }
 352
 353    private bool TryGetLeastRecentlyUsedReusableEntry(out int key, out T result)
 354    {
 6355        key = -1;
 6356        result = null!;
 357
 1548358        foreach (KeyValuePair<int, T> kvp in _cache)
 359        {
 768360            T candidate = kvp.Value;
 768361            if (candidate == null || candidate.IsInUse)
 362                continue;
 363
 512364            if (result == null || candidate.LastUsedFrame < result.LastUsedFrame)
 365            {
 6366                key = kvp.Key;
 6367                result = candidate;
 368            }
 369        }
 370
 6371        return result != null;
 372    }
 373
 374    private void AddCachedResult(int key, T result)
 375    {
 852376        if (_cache.TryGetValue(key, out T existing))
 1377            RemoveFromChartIndex(key, existing.ChartsUtilized);
 378
 852379        _cache[key] = result;
 852380        AddToChartIndex(key, result.ChartsUtilized);
 852381    }
 382
 383    private void CheckoutCachedResult(T result)
 384    {
 1556385        _lock.EnterWriteLock();
 386        try
 387        {
 1556388            result.Checkout();
 1556389            CountInUse++;
 1556390        }
 3112391        finally { _lock.ExitWriteLock(); }
 1556392    }
 393
 394    private void InvalidateCachedResult(int key, T result)
 395    {
 187396        if (result.IsInUse)
 13397            CountInUse--;
 398
 187399        RemoveCachedResult(key, result);
 187400        result.Reset();
 187401    }
 402
 403    private void RemoveCachedResult(int key, T result)
 404    {
 228405        RemoveFromChartIndex(key, result.ChartsUtilized);
 228406        _cache.Remove(key);
 228407    }
 408
 409    private void AddToChartIndex(int cacheKey, string[] chartKeys)
 410    {
 852411        if (chartKeys == null || chartKeys.Length == 0)
 592412            return;
 413
 1514414        for (int i = 0; i < chartKeys.Length; i++)
 415        {
 497416            string chartKey = chartKeys[i];
 497417            if (string.IsNullOrEmpty(chartKey)
 497418                || ContainsPriorChartKey(chartKeys, i, chartKey))
 419            {
 420                continue;
 421            }
 422
 496423            if (!_chartIndex.TryGetValue(chartKey, out SwiftList<int> keys))
 424            {
 235425                keys = new SwiftList<int>(1);
 235426                _chartIndex[chartKey] = keys;
 427            }
 428
 496429            keys.Add(cacheKey);
 430        }
 260431    }
 432
 433    private void RemoveFromChartIndex(int cacheKey, string[] chartKeys)
 434    {
 229435        if (chartKeys == null || chartKeys.Length == 0)
 38436            return;
 437
 1040438        for (int i = 0; i < chartKeys.Length; i++)
 439        {
 329440            string chartKey = chartKeys[i];
 329441            if (string.IsNullOrEmpty(chartKey)
 329442                || ContainsPriorChartKey(chartKeys, i, chartKey))
 443            {
 444                continue;
 445            }
 446
 328447            if (!_chartIndex.TryGetValue(chartKey, out SwiftList<int> keys))
 448                continue;
 449
 328450            keys.Remove(cacheKey);
 328451            if (keys.Count == 0)
 69452                _chartIndex.Remove(chartKey);
 453        }
 191454    }
 455
 456    private static bool ContainsPriorChartKey(string[] chartKeys, int exclusiveEnd, string chartKey)
 457    {
 2724458        for (int i = 0; i < exclusiveEnd; i++)
 459        {
 538460            if (string.Equals(chartKeys[i], chartKey, StringComparison.Ordinal))
 2461                return true;
 462        }
 463
 824464        return false;
 465    }
 466
 3855467    public void Dispose() => _lock?.Dispose();
 468}