Skip to content

Intelligent Pooling System

TL;DR — Why Use This

  • Automatic memory management with intelligent purging that adapts to usage patterns.
  • Avoid GC spikes by spreading purges across frames and responding to memory pressure.
  • Type-specific policies for different object lifetimes (short-lived lists vs long-lived audio sources).
  • Zero-configuration defaults that "just work" with opt-in customization.

Contents


Overview

The intelligent pooling system provides automatic memory management for WallstopGenericPool<T> instances. Instead of pools growing unbounded or requiring manual purge calls, the system:

  1. Tracks usage patterns - Monitors high-water marks and access frequency
  2. Purges intelligently - Only removes items unlikely to be needed soon
  3. Spreads work - Limits purges per operation to avoid GC spikes
  4. Responds to pressure - Aggressive cleanup when memory is low
  5. Respects object size - Large objects get stricter policies
flowchart TB
    subgraph "Intelligent Purging Flow"
        Access[Pool Access] --> Track[Track Usage]
        Track --> Check{Purge Trigger?}
        Check -->|Yes| Eligible{Items Eligible?}
        Eligible -->|Idle Timeout Exceeded| Purge[Purge Items]
        Eligible -->|No| Skip[Skip Purge]
        Purge --> Limit{Max Purges/Op?}
        Limit -->|Reached| Pending[Mark Pending]
        Limit -->|Not Reached| Continue[Continue]
    end

Quick Start

Basic Usage (Zero Configuration)

By default, intelligent purging is enabled with conservative settings:

C#
using WallstopStudios.UnityHelpers.Utils;

// Pools automatically use intelligent purging
var pool = new WallstopGenericPool<List<int>>(
    createFunc: () => new List<int>(),
    actionOnGet: list => list.Clear()
);

// Rent and return as usual - purging happens automatically
var list = pool.Get();
list.Add(1);
list.Add(2);
pool.Release(list);

Disable Globally (One-Liner Opt-Out)

C#
// Disable all intelligent purging
PoolPurgeSettings.DisableGlobally();

Per-Type Configuration

C#
using WallstopStudios.UnityHelpers.Utils;

// Configure specific type behavior
PoolPurgeSettings.Configure<ExpensiveObject>(options =>
{
    options.IdleTimeoutSeconds = 600f;  // 10 minutes
    options.MinRetainCount = 5;         // Always keep 5
    options.WarmRetainCount = 10;       // Keep 10 when active
});

// Configure all List<T> variants
PoolPurgeSettings.ConfigureGeneric(typeof(List<>), options =>
{
    options.IdleTimeoutSeconds = 120f;  // 2 minutes
    options.BufferMultiplier = 1.5f;    // 50% buffer
});

// Disable purging for specific types
PoolPurgeSettings.Disable<CriticalResource>();

PoolOptions Configuration

PoolOptions<T> provides per-pool configuration:

C#
using WallstopStudios.UnityHelpers.Utils;

var options = new PoolOptions<MyObject>
{
    // Size limits
    MaxPoolSize = 100,           // Hard cap on pool size
    MinRetainCount = 0,          // Absolute minimum to keep
    WarmRetainCount = 2,         // Keep 2 when active

    // Timing
    IdleTimeoutSeconds = 300f,   // 5 minutes before eligible
    PurgeIntervalSeconds = 60f,  // Periodic check interval

    // Intelligent purging
    UseIntelligentPurging = true,
    BufferMultiplier = 2.0f,     // 2x peak usage buffer
    RollingWindowSeconds = 300f, // 5 minute window
    HysteresisSeconds = 120f,    // 2 minute spike cooldown
    SpikeThresholdMultiplier = 2.5f, // 2.5x average = spike
    MaxPurgesPerOperation = 10,  // Spread large purges

    // Triggers
    Triggers = PurgeTrigger.OnRent | PurgeTrigger.OnReturn,

    // Callbacks
    OnPurge = (item, reason) => Debug.Log($"Purged: {reason}")
};

var pool = new WallstopGenericPool<MyObject>(
    createFunc: () => new MyObject(),
    options: options
);

PurgeTrigger Flags

Trigger Description
OnRent Check when item is rented (lazy cleanup)
OnReturn Check when item is returned
Periodic Timer-based checks at PurgeIntervalSeconds
Explicit Only purge when Purge() is called manually

PurgeReason Values

Reason Description
IdleTimeout Item was idle longer than IdleTimeoutSeconds
CapacityExceeded Pool exceeded MaxPoolSize
MemoryPressure System memory pressure detected
AppBackgrounded Application went to background
SceneUnloaded Scene was unloaded
Explicit Manual Purge() call
BudgetExceeded Global pool budget exceeded

Global Settings (PoolPurgeSettings)

Configure system-wide defaults:

C#
using WallstopStudios.UnityHelpers.Utils;

// Enable/disable globally
PoolPurgeSettings.GlobalEnabled = true;

// Configure defaults
PoolPurgeSettings.DefaultGlobalIdleTimeoutSeconds = 300f;
PoolPurgeSettings.DefaultGlobalMinRetainCount = 0;
PoolPurgeSettings.DefaultGlobalWarmRetainCount = 2;
PoolPurgeSettings.DefaultGlobalBufferMultiplier = 2.0f;
PoolPurgeSettings.DefaultGlobalRollingWindowSeconds = 300f;
PoolPurgeSettings.DefaultGlobalHysteresisSeconds = 120f;
PoolPurgeSettings.DefaultGlobalSpikeThresholdMultiplier = 2.5f;
PoolPurgeSettings.DefaultGlobalMaxPurgesPerOperation = 10;

// Lifecycle hooks
PoolPurgeSettings.PurgeOnLowMemory = true;       // Application.lowMemory
PoolPurgeSettings.PurgeOnAppBackground = true;   // Application.focusChanged
PoolPurgeSettings.PurgeOnSceneUnload = true;     // SceneManager.sceneUnloaded

Retention Model

The system uses a two-tier retention model:

  • MinRetainCount: Absolute floor. Pool never purges below this, even when completely idle.
  • WarmRetainCount: Floor for "active" pools (accessed within IdleTimeoutSeconds). Prevents cold-start allocations.
Text Only
Effective Floor = max(MinRetainCount, isActive ? WarmRetainCount : 0)

Example:

  • MinRetainCount = 0, WarmRetainCount = 2
  • Active pool: keeps at least 2 items warm
  • Idle pool (no access for IdleTimeoutSeconds): can purge to 0

Eviction Policies

Comfortable Size Calculation

The "comfortable size" determines when purging is needed:

Text Only
ComfortableSize = max(EffectiveMinRetain, RollingHighWaterMark * BufferMultiplier)

Items that have been idle longer than IdleTimeoutSeconds are purged regardless of comfortable size. The comfortable size primarily influences the target retention during non-idle purges and memory pressure events.

Hysteresis Protection

After a usage spike, purging is suppressed for HysteresisSeconds to prevent purge-allocate cycles:

sequenceDiagram
    participant App as Application
    participant Pool as Pool

    Note over App,Pool: Normal usage period
    App->>Pool: Get items (low volume)
    Pool->>Pool: Track high-water mark

    Note over App,Pool: Usage spike detected
    App->>Pool: Get many items rapidly
    Pool->>Pool: Spike! Start hysteresis

    Note over App,Pool: Hysteresis period (2 min default)
    Pool->>Pool: Purging suppressed

    Note over App,Pool: After hysteresis
    Pool->>Pool: Resume normal purging

Gradual Purging

Large purge operations are spread across multiple calls:

C#
// Configure max items purged per operation
options.MaxPurgesPerOperation = 10;

// Pool tracks pending purges
if (pool.HasPendingPurges)
{
    // More items to purge on next trigger
}

// Force immediate full purge (bypasses limit)
pool.ForceFullPurge();

Memory Pressure Detection

The system monitors memory pressure and adjusts purging aggressiveness:

C#
using WallstopStudios.UnityHelpers.Utils;

// Check current pressure level
MemoryPressureLevel level = MemoryPressureMonitor.CurrentPressure;

switch (level)
{
    case MemoryPressureLevel.None:
        // Normal operation
        break;
    case MemoryPressureLevel.Low:
        // Minor pressure, slightly more aggressive
        break;
    case MemoryPressureLevel.Medium:
        // Moderate pressure, reduced buffers
        break;
    case MemoryPressureLevel.High:
        // Significant pressure, aggressive purging
        break;
    case MemoryPressureLevel.Critical:
        // Emergency cleanup, bypass limits
        break;
}

Pressure Detection Sources

Metric Threshold
Absolute Memory Managed heap exceeds threshold
GC Collection Rate Frequent GC collections detected
Memory Growth Rate Rapid memory increase
Application.lowMemory Unity's low memory callback

Size-Aware Policies

Large objects (allocated on the Large Object Heap) get stricter policies:

C#
using WallstopStudios.UnityHelpers.Utils;

// Enable size-aware policies
PoolPurgeSettings.SizeAwarePoliciesEnabled = true;

// Configure thresholds
PoolPurgeSettings.LargeObjectThresholdBytes = 85000;  // .NET LOH threshold
PoolPurgeSettings.LargeObjectBufferMultiplier = 1.0f; // No buffer (vs 2.0x)
PoolPurgeSettings.LargeObjectIdleTimeoutMultiplier = 0.5f; // 50% shorter
PoolPurgeSettings.LargeObjectWarmRetainCount = 1;     // Keep 1 (vs 2)

PoolSizeEstimator

Estimate object sizes for policy decisions:

C#
using WallstopStudios.UnityHelpers.Utils;

// Estimate single item size
long size = PoolSizeEstimator.EstimateItemSizeBytes<MyLargeObject>();

// Estimate array size
long arraySize = PoolSizeEstimator.EstimateArraySizeBytes<byte>(length: 100000);

// Check if on LOH
bool isLargeObject = size >= PoolPurgeSettings.LargeObjectThresholdBytes;

Access Frequency Tracking

Pools track access patterns for intelligent decisions:

C#
using WallstopStudios.UnityHelpers.Utils;

// Get frequency statistics
PoolFrequencyStatistics stats = pool.FrequencyStatistics;

// Access metrics
float rentalsPerMinute = stats.RentalsPerMinute;
float avgInterRentalTime = stats.AverageInterRentalTimeSeconds;
float lastAccess = stats.LastAccessTime;

// Helper properties
bool isHighFrequency = stats.IsHighFrequency;  // > 60 rentals/min
bool isLowFrequency = stats.IsLowFrequency;    // <= 1 rental/min
bool isUnused = stats.IsUnused;                // No recent access

Application Lifecycle Hooks

The system responds to application lifecycle events:

C#
1
2
3
4
5
6
using WallstopStudios.UnityHelpers.Utils;

// Configure lifecycle responses
PoolPurgeSettings.PurgeOnLowMemory = true;     // Application.lowMemory
PoolPurgeSettings.PurgeOnAppBackground = true; // Application loses focus
PoolPurgeSettings.PurgeOnSceneUnload = true;   // Scene unloaded

Mobile Considerations

On mobile platforms:

  • App backgrounded: Aggressive purge to reduce memory footprint
  • Low memory: Emergency purge, bypasses gradual limits
  • Scene unload: Clean up scene-specific pools

Global Pool Registry

Track and manage all pools system-wide:

C#
using WallstopStudios.UnityHelpers.Utils;

// Configure global budget
PoolPurgeSettings.GlobalMaxPooledItems = 50000;

// Get global statistics
GlobalPoolStatistics globalStats = GlobalPoolRegistry.GetStatistics();
int totalPooled = globalStats.TotalPooledItems;
float budgetUtilization = globalStats.BudgetUtilization;
int registeredPools = globalStats.RegisteredPoolCount;

// Force budget enforcement
GlobalPoolRegistry.EnforceBudget();

// Try non-blocking budget check
if (GlobalPoolRegistry.TryEnforceBudgetIfNeeded())
{
    // Budget was over, items purged
}

LRU Cross-Pool Eviction

When the global budget is exceeded, items are evicted across all pools using LRU ordering based on pool access times.


Best Practices

Configuration Hierarchy

Settings are resolved in priority order:

  1. Per-instance PoolOptions (highest priority)
  2. Programmatic type configuration (PoolPurgeSettings.Configure<T>)
  3. Generic type pattern (PoolPurgeSettings.ConfigureGeneric)
  4. Attribute-based ([PoolPurgePolicy] on type)
  5. Settings asset configuration
  6. Built-in type defaults
  7. Global defaults (lowest priority)

Type-Specific Recommendations

C#
// Short-lived temporary collections
PoolPurgeSettings.Configure<List<int>>(o =>
{
    o.IdleTimeoutSeconds = 60f;
    o.WarmRetainCount = 5;
});

// Long-lived expensive objects
PoolPurgeSettings.Configure<AudioSource>(o =>
{
    o.IdleTimeoutSeconds = 600f;
    o.MinRetainCount = 2;
    o.WarmRetainCount = 4;
});

// Large buffers (be aggressive)
PoolPurgeSettings.Configure<byte[]>(o =>
{
    o.IdleTimeoutSeconds = 30f;
    o.BufferMultiplier = 1.0f;
    o.WarmRetainCount = 1;
});

Performance Tips

  1. Use gradual purging - Default MaxPurgesPerOperation = 10 prevents GC spikes
  2. Size buffers appropriately - 2x buffer is conservative, 1.5x for memory-constrained
  3. Monitor frequency stats - Use FrequencyStatistics to tune per-type settings
  4. Enable size-aware policies - Large objects need stricter handling
  5. Use lifecycle hooks - Let the system handle mobile backgrounding

Debugging

C#
// Log purge events
var options = new PoolOptions<MyObject>
{
    OnPurge = (item, reason) =>
    {
        Debug.Log($"[Pool] Purged {typeof(MyObject).Name}: {reason}");
    }
};

// Check global stats periodically
void OnGUI()
{
    var stats = GlobalPoolRegistry.Statistics;
    GUILayout.Label($"Pools: {stats.RegisteredPoolCount}");
    GUILayout.Label($"Items: {stats.TotalPooledItems}/{PoolPurgeSettings.GlobalMaxPooledItems}");
    GUILayout.Label($"Budget: {stats.BudgetUtilization:P0}");
}