Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created October 19, 2025 12:34
Show Gist options
  • Save adammyhre/51aed0c1168ed7fcedf3d6ca6f5c036b to your computer and use it in GitHub Desktop.
Save adammyhre/51aed0c1168ed7fcedf3d6ca6f5c036b to your computer and use it in GitHub Desktop.
Allocation Analyzer
using System;
using UnityEngine.Profiling;
// See UnityEngine.TestTools.Constraints.AllocatingGCMemoryConstraint
// and https://maingauche.games/devlog/20230504-counting-allocations-in-unity/
public class AllocCounter {
UnityEngine.Profiling.Recorder rec;
public AllocCounter() {
rec = Recorder.Get("GC.Alloc");
rec.enabled = false;
#if !UNITY_WEBGL
rec.FilterToCurrentThread();
#endif
rec.enabled = true;
}
public int Stop() {
if (rec == null) throw new InvalidOperationException("AllocCounter was not started.");
rec.enabled = false;
#if !UNITY_WEBGL
rec.CollectFromAllThreads();
#endif
int result = rec.sampleBlockCount;
rec = null;
return result;
}
}
using System;
using UnityEngine;
using UnityEngine.Profiling;
public class AllocatorDemo : MonoBehaviour {
const int BYTES_TO_MB = 1024 * 1024;
void Start() {
var ac = new AllocCounter();
long memUsed = System.GC.GetTotalMemory(false);
long monoBytes = Profiler.GetMonoUsedSizeLong();
byte[] junk = new byte[BYTES_TO_MB];
var allocCount = ac.Stop();
long memUsedAfter = GC.GetTotalMemory(false);
long monoBytesAfter = Profiler.GetMonoUsedSizeLong();
Debug.Log("Allocations during Start: " + allocCount);
Debug.Log("Memory used before: " + memUsed + ", after: " + memUsedAfter + ", diff: " + (memUsedAfter - memUsed));
Debug.Log("Mono bytes before: " + monoBytes + ", after: " + monoBytesAfter + ", diff: " + (monoBytesAfter - monoBytes));
}
void Update() {
if (Time.frameCount % 60 == 0) {
byte[] junk = new byte[BYTES_TO_MB * 10];
Logwin.Log("Allocated 1 MB of junk data.", Time.frameCount);
}
}
}
using System;
// A fixed-size ring buffer that overwrites the oldest entries when full.
public class CircularBuffer<T> {
readonly T[] buffer;
int head, tail;
public int Count { get; private set; }
public int Capacity => buffer.Length;
public CircularBuffer(int size) {
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
buffer = new T[size];
}
public void Enqueue(T item) {
buffer[head] = item;
head = (head + 1) % Capacity;
if (Count == Capacity) tail = (tail + 1) % Capacity;
else Count++;
}
public T Dequeue() {
if (Count == 0) throw new InvalidOperationException("Buffer is empty");
var item = buffer[tail];
tail = (tail + 1) % Capacity;
Count--;
return item;
}
// Access elements by logical index (0 = oldest, Count-1 = newest)
public T this[int index] {
get {
if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index));
if (Capacity == 0 || buffer == null) throw new InvalidOperationException();
return buffer[(tail + index) % Capacity];
}
}
}
using UnityEngine;
using UnityEngine.Profiling;
using System;
using TMPro;
using UnityEngine.UI;
public class MemoryMonitor : MonoBehaviour {
#region Fields
public TextMeshProUGUI allocatedRamText, reservedRamText, monoRamText, gcCountText;
public RawImage memoryGraphImage;
public int historyLength = 300, graphHeight = 100;
public Color32 allocatedColor = new(0, 255, 0, 255),
monoColor = new(0, 150, 255, 255),
reservedColor = new(200, 200, 200, 255),
gcEventColor = new(255, 0, 0, 255);
const float BYTES_TO_MB = 1024f * 1024f;
static readonly Color32 backgroundColor = new(0, 0, 0, 255);
CircularBuffer<long> allocated, reserved, mono, gcAlloc;
CircularBuffer<bool> gcEvents;
Texture2D graphTexture;
Color32[] pixels;
Recorder rec;
int lastGCCount;
long maxReserved;
#endregion
void Start() {
allocated = new(historyLength);
reserved = new(historyLength);
mono = new(historyLength);
gcAlloc = new(historyLength);
gcEvents = new(historyLength);
rec = Recorder.Get("GC.Alloc");
rec.enabled = false;
rec.FilterToCurrentThread();
rec.enabled = true;
graphTexture = new Texture2D(historyLength, graphHeight + 1, TextureFormat.RGBA32, false)
{ wrapMode = TextureWrapMode.Clamp };
pixels = new Color32[graphTexture.width * graphTexture.height];
memoryGraphImage.texture = graphTexture;
}
void Update() {
long allocBytes = Profiler.GetTotalAllocatedMemoryLong();
long resBytes = Profiler.GetTotalReservedMemoryLong();
long monoBytes = Profiler.GetMonoUsedSizeLong();
long gcAllocs = rec.sampleBlockCount;
int gcCount = GC.CollectionCount(0); // Only one generation in Unity at index 0
bool gcHappened = gcCount != lastGCCount;
lastGCCount = gcCount;
UpdateText(allocatedRamText, allocBytes / BYTES_TO_MB, allocatedColor);
UpdateText(reservedRamText, resBytes / BYTES_TO_MB, reservedColor);
UpdateText(monoRamText, monoBytes / BYTES_TO_MB, monoColor);
UpdateText(gcCountText, gcCount, gcHappened ? Color.red : Color.white);
if (resBytes > maxReserved) maxReserved = resBytes;
allocated.Enqueue(allocBytes);
reserved.Enqueue(resBytes);
mono.Enqueue(monoBytes);
gcAlloc.Enqueue(gcAllocs);
gcEvents.Enqueue(gcHappened);
DrawGraph();
}
void DrawGraph() {
if (!graphTexture) return;
Array.Fill(pixels, backgroundColor);
int width = graphTexture.width;
for (int i = 0; i < allocated.Count; i++) {
int x = i;
float scale = graphHeight / (float)Math.Max(maxReserved, 1);
int hMono = (int)(mono[i] * scale);
int hAlloc = (int)(allocated[i] * scale);
int hRes = (int)(reserved[i] * scale);
for (int y = 0; y < hRes; y++) {
int idx = x + y * width;
pixels[idx] = y < hMono ? monoColor :
y < hAlloc ? allocatedColor : reservedColor;
}
if (gcEvents[i]) {
for (int y = 0; y < graphHeight; y++) {
pixels[x + y * width] = gcEventColor;
}
}
}
if (gcEvents.Count > 2 && gcEvents[gcEvents.Count - 1]) {
long monoDiff = mono[mono.Count - 2] - mono[mono.Count - 1];
Debug.Log($"GC Event detected! Mono memory change: {monoDiff / BYTES_TO_MB:F2} MB");
}
graphTexture.SetPixels32(pixels);
graphTexture.Apply(false);
}
void UpdateText(TMPro.TextMeshProUGUI text, float value, Color color) {
if (!text) return;
text.text = $"{value:F1} MB";
text.color = color;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment