// 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); } } } }