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.

Revisions

  1. adammyhre created this gist Oct 19, 2025.
    33 changes: 33 additions & 0 deletions AllocCounter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    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;
    }
    }
    32 changes: 32 additions & 0 deletions AllocDemo.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    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);
    }
    }
    }
    38 changes: 38 additions & 0 deletions CircularBuffer.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,38 @@
    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];
    }
    }
    }
    117 changes: 117 additions & 0 deletions MemoryMonitor.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,117 @@
    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;
    }
    }