// MIT License - Copyright (c) 2026 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Utils
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;
///
/// Provides size estimation for pool item types to enable size-aware purging policies.
/// Large objects (above the LOH threshold) are handled more aggressively by the purge system.
///
///
///
/// The .NET Large Object Heap (LOH) threshold is 85,000 bytes. Objects larger than this
/// are allocated on the LOH, which has different garbage collection characteristics:
///
/// - LOH is only collected during Gen2 collections (expensive)
/// - LOH is not compacted by default (fragmentation risk)
/// - Retaining large pooled objects wastes significant memory
///
///
///
/// Size estimation is inherently approximate for managed objects because:
///
/// - Reference types have runtime overhead (vtable, sync block, etc.)
/// - Fields may be padded for alignment
/// - Generic types may have different layouts
/// - Collections have capacity-based sizing
///
///
///
/// Thread safety: All methods are thread-safe. Cached estimates use concurrent collections.
///
///
public static class PoolSizeEstimator
{
///
/// The .NET Large Object Heap (LOH) threshold in bytes.
/// Objects of this size or larger are allocated on the LOH.
///
public const int LargeObjectHeapThreshold = 85000;
///
/// Minimum object overhead for reference types (vtable pointer + sync block index).
/// This is architecture-dependent; using 16 bytes as a conservative 64-bit estimate.
///
private const int MinObjectOverhead = 16;
///
/// Pointer size in bytes for reference calculations.
/// Used as the size for reference type fields and as a fallback for unknown types.
///
private static readonly int PointerSize = IntPtr.Size;
///
/// Default typical capacity used for estimating collection sizes.
/// Collections like List, Dictionary, HashSet, Queue, and Stack
/// are estimated using this capacity multiplied by element size.
///
private const int DefaultTypicalCollectionCapacity = 16;
///
/// Minimum array overhead in bytes (header + length field).
///
private const int MinArrayOverhead = MinObjectOverhead + 8;
///
/// Cache for computed type size estimates to avoid repeated reflection.
///
private static readonly ConcurrentDictionary SizeCache =
new ConcurrentDictionary();
///
/// Cache for LOH classification to avoid repeated size checks.
///
private static readonly ConcurrentDictionary LohCache =
new ConcurrentDictionary();
///
/// Estimates the size in bytes of a single instance of type .
///
/// The type to estimate size for.
///
/// An estimate of the instance size in bytes. For value types, this is exact.
/// For reference types, this is an approximation based on field analysis.
///
///
///
/// For value types, uses for an exact size.
///
///
/// For reference types, estimates based on:
///
/// - Object header overhead (~16 bytes on 64-bit)
/// - Field sizes (value types by size, references by pointer size)
/// - Array element sizes when applicable
///
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int EstimateItemSizeBytes()
{
return EstimateItemSizeBytes(typeof(T));
}
///
/// Estimates the size in bytes of a single instance of the specified type.
///
/// The type to estimate size for.
///
/// An estimate of the instance size in bytes. For value types, this is exact.
/// For reference types, this is an approximation based on field analysis.
///
///
/// If is null, returns as a defensive default.
///
public static int EstimateItemSizeBytes(Type type)
{
if (type == null)
{
return PointerSize;
}
if (SizeCache.TryGetValue(type, out int cachedSize))
{
return cachedSize;
}
int estimatedSize = ComputeEstimatedSize(type);
SizeCache.TryAdd(type, estimatedSize);
return estimatedSize;
}
///
/// Determines whether instances of type would be allocated on the Large Object Heap.
///
/// The type to check.
///
/// true if instances are estimated to be 85,000 bytes or larger; otherwise, false.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsLargeObject()
{
return IsLargeObject(typeof(T));
}
///
/// Determines whether instances of the specified type would be allocated on the Large Object Heap.
///
/// The type to check.
///
/// true if instances are estimated to be 85,000 bytes or larger; otherwise, false.
/// Returns false if is null.
///
public static bool IsLargeObject(Type type)
{
if (type == null)
{
return false;
}
if (LohCache.TryGetValue(type, out bool isLarge))
{
return isLarge;
}
int size = EstimateItemSizeBytes(type);
isLarge = size >= LargeObjectHeapThreshold;
LohCache.TryAdd(type, isLarge);
return isLarge;
}
///
/// Estimates the size in bytes of an array with the specified element type and length.
///
/// The array element type.
/// The number of elements in the array.
/// An estimate of the array size in bytes, including header overhead.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int EstimateArraySizeBytes(int length)
{
return EstimateArraySizeBytes(typeof(T), length);
}
///
/// Estimates the size in bytes of an array with the specified element type and length.
///
/// The array element type.
/// The number of elements in the array.
///
/// An estimate of the array size in bytes, including header overhead.
/// Returns if is null
/// or is negative.
///
public static int EstimateArraySizeBytes(Type elementType, int length)
{
if (elementType == null)
{
return MinArrayOverhead;
}
if (length <= 0)
{
return MinArrayOverhead;
}
int elementSize = GetElementSize(elementType);
return MinArrayOverhead + (elementSize * length);
}
///
/// Determines the array length at which the array would exceed the LOH threshold.
///
/// The array element type.
///
/// The minimum array length that would cause LOH allocation, or
/// if elements are so small that even maximum-length arrays would not reach LOH.
///
public static int GetLohThresholdLength()
{
return GetLohThresholdLength(typeof(T));
}
///
/// Determines the array length at which the array would exceed the LOH threshold.
///
/// The array element type.
///
/// The minimum array length that would cause LOH allocation, or
/// if elements are so small that even maximum-length arrays would not reach LOH,
/// or if is null.
///
public static int GetLohThresholdLength(Type elementType)
{
if (elementType == null)
{
return int.MaxValue;
}
int elementSize = GetElementSize(elementType);
if (elementSize <= 0)
{
return int.MaxValue;
}
int availableForElements = LargeObjectHeapThreshold - MinArrayOverhead;
if (availableForElements <= 0)
{
return 0;
}
return availableForElements / elementSize;
}
///
/// Clears the internal caches. Primarily used for testing.
///
internal static void ClearCaches()
{
SizeCache.Clear();
LohCache.Clear();
}
private static int ComputeEstimatedSize(Type type)
{
// Value types: use Unsafe.SizeOf for exact measurement
if (type.IsValueType)
{
return ComputeValueTypeSize(type);
}
// Arrays: estimate based on element type
if (type.IsArray)
{
return EstimateArrayTypeSize(type);
}
// Reference types: estimate based on fields
return EstimateReferenceTypeSize(type);
}
private static int ComputeValueTypeSize(Type type)
{
// Try to get the actual size using Marshal.SizeOf for blittable types
try
{
return Marshal.SizeOf(type);
}
catch (Exception e)
{
// Non-blittable types - estimate based on fields
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning(
$"[PoolSizeEstimator] Failed to get Marshal.SizeOf for {type.Name}, using field-based estimate: {e.Message}"
);
#endif
_ = e;
return EstimateFieldBasedSize(type);
}
}
private static int EstimateArrayTypeSize(Type arrayType)
{
Type elementType = arrayType.GetElementType();
if (elementType == null)
{
return MinObjectOverhead;
}
return EstimateArraySizeBytes(elementType, DefaultTypicalCollectionCapacity);
}
private static int EstimateReferenceTypeSize(Type type)
{
int size = MinObjectOverhead;
// Check for common collection types and estimate based on typical capacity
if (IsCollectionType(type, out int estimatedCollectionSize))
{
return estimatedCollectionSize;
}
// Add field sizes
size += EstimateFieldBasedSize(type);
return size;
}
private static int EstimateFieldBasedSize(Type type)
{
int size = 0;
const BindingFlags flags =
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
try
{
FieldInfo[] fields = type.GetFields(flags);
for (int i = 0; i < fields.Length; i++)
{
FieldInfo field = fields[i];
Type fieldType = field.FieldType;
if (fieldType.IsValueType)
{
size += GetElementSize(fieldType);
}
else
{
// Reference types are stored as pointers
size += PointerSize;
}
}
}
catch (Exception e)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogWarning(
$"[PoolSizeEstimator] Failed to estimate field-based size for {type.Name}: {e.Message}"
);
#endif
_ = e;
// If reflection fails, use a conservative estimate
size = PointerSize * 4;
}
return size;
}
private static bool IsCollectionType(Type type, out int estimatedSize)
{
estimatedSize = 0;
// Check for generic collection types
if (!type.IsGenericType)
{
return false;
}
Type genericDefinition = type.GetGenericTypeDefinition();
Type[] genericArgs = type.GetGenericArguments();
// List
if (genericDefinition == typeof(List<>) && genericArgs.Length == 1)
{
int elementSize = GetElementSize(genericArgs[0]);
estimatedSize =
MinObjectOverhead
+ (PointerSize * 3)
+ (elementSize * DefaultTypicalCollectionCapacity);
return true;
}
// Dictionary
if (genericDefinition == typeof(Dictionary<,>) && genericArgs.Length == 2)
{
int keySize = GetElementSize(genericArgs[0]);
int valueSize = GetElementSize(genericArgs[1]);
int entrySize = keySize + valueSize + 8; // Entry includes hash and next pointer
estimatedSize =
MinObjectOverhead
+ (PointerSize * 5)
+ (entrySize * DefaultTypicalCollectionCapacity);
return true;
}
// HashSet
if (genericDefinition == typeof(HashSet<>) && genericArgs.Length == 1)
{
int elementSize = GetElementSize(genericArgs[0]);
int slotSize = elementSize + 8; // Slot includes hash and next
estimatedSize =
MinObjectOverhead
+ (PointerSize * 4)
+ (slotSize * DefaultTypicalCollectionCapacity);
return true;
}
// Queue
if (genericDefinition == typeof(Queue<>) && genericArgs.Length == 1)
{
int elementSize = GetElementSize(genericArgs[0]);
estimatedSize =
MinObjectOverhead
+ (PointerSize * 4)
+ (elementSize * DefaultTypicalCollectionCapacity);
return true;
}
// Stack
if (genericDefinition == typeof(Stack<>) && genericArgs.Length == 1)
{
int elementSize = GetElementSize(genericArgs[0]);
estimatedSize =
MinObjectOverhead
+ (PointerSize * 3)
+ (elementSize * DefaultTypicalCollectionCapacity);
return true;
}
return false;
}
private static int GetElementSize(Type type)
{
if (type == null)
{
return PointerSize;
}
if (!type.IsValueType)
{
return PointerSize;
}
// Primitive types - known sizes
if (type == typeof(byte) || type == typeof(sbyte) || type == typeof(bool))
{
return 1;
}
if (type == typeof(short) || type == typeof(ushort) || type == typeof(char))
{
return 2;
}
if (type == typeof(int) || type == typeof(uint) || type == typeof(float))
{
return 4;
}
if (type == typeof(long) || type == typeof(ulong) || type == typeof(double))
{
return 8;
}
if (type == typeof(decimal))
{
return 16;
}
if (type == typeof(IntPtr) || type == typeof(UIntPtr))
{
return PointerSize;
}
if (type == typeof(Guid))
{
return 16;
}
if (type == typeof(DateTime) || type == typeof(TimeSpan))
{
return 8;
}
// Enum types - size based on underlying type
if (type.IsEnum)
{
return GetElementSize(Enum.GetUnderlyingType(type));
}
// Other value types - try Marshal.SizeOf
try
{
return Marshal.SizeOf(type);
}
catch (Exception e)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.Log(
$"[PoolSizeEstimator] Failed to get element size for {type.Name}, using field-based estimate: {e.Message}"
);
#endif
_ = e;
// Non-blittable struct - estimate based on fields
return EstimateFieldBasedSize(type);
}
}
}
}