< Summary

Information
Class: NGql.Core.Pooling.ThreadLocalPool<T>
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Pooling/ThreadLocalPool.cs
Line coverage
100%
Covered lines: 46
Uncovered lines: 0
Coverable lines: 46
Total lines: 153
Line coverage: 100%
Branch coverage
100%
Covered branches: 28
Total branches: 28
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%44100%
.ctor()100%11100%
TryGet(...)100%22100%
TryReturn(...)100%22100%
Get()100%88100%
Return(...)100%1212100%
get_ApproximateCount()100%11100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Pooling/ThreadLocalPool.cs

#LineLine coverage
 1using System.Runtime.CompilerServices;
 2
 3namespace NGql.Core.Pooling;
 4
 5/// <summary>
 6/// Generic thread-local pool with global fallback for any type.
 7/// Provides lock-free pooling with thread-local optimization to eliminate contention.
 8/// </summary>
 9/// <typeparam name="T">Type to pool (must be a reference type)</typeparam>
 10internal sealed class ThreadLocalPool<T> where T : class
 11{
 12    private const int ThreadLocalCacheSize = 4;
 13    private const int GlobalPoolSize = 64;
 14
 15    // Instance-based thread-local storage for per-thread caches to eliminate contention
 16    // CRITICAL FIX: Each instance gets its own ThreadLocal<>, not a shared [ThreadStatic] field
 17    // This prevents different generic types from corrupting each other's caches
 18    private readonly ThreadLocal<ThreadLocalCache> _threadLocalCache;
 19
 20    // Global lock-free fallback pool using atomic operations
 5121    private readonly MonitoredLockFreeStack<T> _globalPool = new();
 22    private volatile int _globalCount;
 23
 24    private readonly Func<T> _factory;
 25    private readonly Action<T> _reset;
 26    private readonly Func<T, bool>? _validateForReturn;
 27    private readonly string _poolName;
 28
 29    /// <summary>
 30    /// Creates a new thread-local pool.
 31    /// </summary>
 32    /// <param name="factory">Factory function to create new instances</param>
 33    /// <param name="reset">Action to reset/clear an instance before returning to pool</param>
 34    /// <param name="validateForReturn">Optional validation - return false to reject item from pool</param>
 35    /// <param name="poolName">Name for diagnostics/metrics</param>
 5136    public ThreadLocalPool(
 5137        Func<T> factory,
 5138        Action<T> reset,
 5139        Func<T, bool>? validateForReturn = null,
 5140        string poolName = "unknown")
 41    {
 5142        _factory = factory ?? throw new ArgumentNullException(nameof(factory));
 4843        _reset = reset ?? throw new ArgumentNullException(nameof(reset));
 4544        _validateForReturn = validateForReturn;
 4545        _poolName = poolName;
 46        // Instance-based ThreadLocal ensures each pool instance has separate per-thread caches
 4547        _threadLocalCache = new ThreadLocal<ThreadLocalCache>();
 4548    }
 49
 50    /// <summary>
 51    /// Thread-local cache to minimize global pool access
 52    /// </summary>
 53    private sealed class ThreadLocalCache
 54    {
 13255        private readonly T?[] _items = new T?[ThreadLocalCacheSize];
 56        private int _count;
 57
 58        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 59        public bool TryGet(out T? item)
 60        {
 872761            if (_count > 0)
 62            {
 750063                var index = --_count;
 750064                item = _items[index]!;
 750065                _items[index] = null; // Clear reference
 750066                return true;
 67            }
 122768            item = null;
 122769            return false;
 70        }
 71
 72        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 73        public bool TryReturn(T item)
 74        {
 870375            if (_count < ThreadLocalCacheSize)
 76            {
 764777                _items[_count++] = item;
 764778                return true;
 79            }
 105680            return false;
 81        }
 82    }
 83
 84    /// <summary>
 85    /// Gets an item from thread-local cache first, then global pool, or creates new
 86    /// </summary>
 87    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 88    public T Get()
 89    {
 90        // ULTRA FAST PATH: Thread-local cache hit (no contention)
 91        // FIXED: Use instance-based ThreadLocal instead of static [ThreadStatic] field
 872792        var cache = _threadLocalCache.Value ?? (_threadLocalCache.Value = new ThreadLocalCache());
 872793        if (cache.TryGet(out var item) && item is not null)
 94        {
 750095            ThreadLocalMemoryManager.RecordThreadLocalHit(_poolName);
 750096            return item;
 97        }
 98
 99        // FAST PATH: Global lock-free pool
 1227100        if (_globalPool.TryPop(out item))
 101        {
 3102            Interlocked.Decrement(ref _globalCount);
 3103            return item;
 104        }
 105
 106        // SLOW PATH: Allocate new instance
 1224107        ThreadLocalMemoryManager.RecordAllocation(_poolName);
 1224108        return _factory();
 109    }
 110
 111    /// <summary>
 112    /// Returns item to thread-local cache first, then global pool
 113    /// </summary>
 114    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 115    public void Return(T? item)
 116    {
 8712117        if (item == null) return;
 118
 119        // Validate item if validator is provided
 8706120        if (_validateForReturn != null && !_validateForReturn(item))
 121        {
 3122            return; // Item rejected, let GC handle it
 123        }
 124
 125        // Reset/clear the item
 8703126        _reset(item);
 127
 128        // ULTRA FAST PATH: Return to thread-local cache (no contention)
 129        // FIXED: Use instance-based ThreadLocal instead of static [ThreadStatic] field
 8703130        var cache = _threadLocalCache.Value ?? (_threadLocalCache.Value = new ThreadLocalCache());
 8703131        if (cache.TryReturn(item))
 132        {
 7647133            return;
 134        }
 135
 136        // FAST PATH: Return to global pool if not full (atomic increment with bounds check)
 1056137        var newCount = Interlocked.Increment(ref _globalCount);
 1056138        if (newCount <= GlobalPoolSize)
 139        {
 564140            _globalPool.Push(item);
 141        }
 142        else
 143        {
 144            // Pool is full, undo the increment and let GC handle it
 492145            Interlocked.Decrement(ref _globalCount);
 146        }
 492147    }
 148
 149    /// <summary>
 150    /// Gets the approximate count of items in the global pool (for diagnostics)
 151    /// </summary>
 3152    public int ApproximateCount => _globalCount;
 153}