# 2. CPU Allocations and Incremental GC
Part of [[Profiling and Optimization]]. Previous: [[1. Profiler Window and a Repeatable Workflow]]. Next: [[3. Memory Profiler Snapshots]].
> [!note] Verification status
> `ProfilerMarker` and `ProfilerRecorder` are in the 6000.5 Scripting API (`Unity.Profiling`). Incremental GC is a Player setting (Incremental Garbage Collection) with `GarbageCollector` scripting hooks. **Code not compiled in Unity.** Exact Incremental GC inspector wording in 6000.5 Project Settings is **not click-verified**.
## 2.1 The hitch students actually ship
A game that "runs at 200 FPS" in the Editor and stutters every couple of seconds is usually allocating. The managed garbage collector pauses (or, with Incremental GC, spreads work across frames). Either way, a spike in the CPU module that lines up with GC markers is your first suspect.
Common allocators in gameplay code:
- `new` of classes or boxed structs in `Update`
- LINQ on hot paths (`foreach` over a `Where` iterator)
- String concatenation (`"hp: " + health`) every frame
- `GetComponent` is not an allocation, but `GetComponents` into a new array is
- `UnityEngine.Random` is fine; `System.Guid.NewGuid().ToString()` in a loop is not
- Closures (`() => x`) that capture and allocate a display class
- `Debug.Log` in `Update` (strings, and it is also just slow)
A useful lab: open the inventory UI every frame in Play Mode with allocation call stacks on, and read the first stack that is yours.
## 2.2 Incremental GC
Enable **Incremental Garbage Collection** in the Player settings so a collect can be split across frames instead of landing as one spike. It is the right default for almost every interactive project, including Quest. It is *not* a license to allocate: you still want zero (or near-zero) allocations in the standing gameplay loop.
You can also cap how long incremental collections may run in a frame via the `GarbageCollector` API. If you use that, measure it — a cap that is too tight defers work until you hitch later.
## 2.3 Mark your own code
`ProfilerMarker` is how your systems show up with a stable name in the CPU module, without Deep Profile.
```csharp
using Unity.Profiling;
using UnityEngine;
public sealed class InventoryRebuild : MonoBehaviour
{
static readonly ProfilerMarker RebuildMarker = new("Inventory.RebuildVisible");
public void RebuildVisible()
{
using (RebuildMarker.Auto())
{
// fill slots, refresh icons...
}
}
}
```
Use a dotted name (`Inventory.RebuildVisible`) so related markers sort together. Do not create a `new ProfilerMarker` inside the method; make it `static readonly`.
## 2.4 Read numbers from code
`ProfilerRecorder` samples a built-in or custom counter without opening the window. Useful for an in-game HUD during a playtest, or for a test you will later hook to the Performance testing API.
```csharp
using System.Text;
using Unity.Profiling;
using UnityEngine;
public sealed class FrameStatsOverlay : MonoBehaviour
{
ProfilerRecorder _gcReserved;
ProfilerRecorder _mainThread;
readonly StringBuilder _text = new(128);
void OnEnable()
{
_gcReserved = ProfilerRecorder.StartNew(ProfilerCategory.Memory, "GC Reserved Memory");
_mainThread = ProfilerRecorder.StartNew(ProfilerCategory.Internal, "Main Thread", 1);
}
void OnDisable()
{
_gcReserved.Dispose();
_mainThread.Dispose();
}
void Update()
{
_text.Clear();
if (_gcReserved.Valid)
_text.Append("GC reserved: ").Append(_gcReserved.LastValue / 1024 / 1024).Append(" MiB\n");
if (_mainThread.Valid)
_text.Append("Main thread: ").Append(_mainThread.LastValue / 1_000_000f).Append(" ms");
// Assign _text to a UI Toolkit label, or Debug.Log occasionally — not every frame in a shipping build.
}
}
```
Built-in recorder names are version-sensitive. If a recorder is not `.Valid`, the name may have changed; look it up in the Profiler module's counter list for your Editor rather than inventing a new string. Wrap this overlay in `#if DEVELOPMENT_BUILD || UNITY_EDITOR`.
## 2.5 Fix order that actually works
1. Remove allocations in `Update` / `LateUpdate` / tight loops (reuse lists with `Clear`, cache components, pre-size collections, pool the things you spawn — [[Interactable Audio]] already pools `AudioSource`s).
2. Move work off the main thread only after you know it is expensive: Jobs + Burst ([[2. Jobs and Burst for GameObject Projects]]), or `Awaitable.BackgroundThreadAsync` for long non-Unity work (the Async Code tutorial later in this queue).
3. Reduce how often the work runs (dirty flags, not "rebuild the HUD every frame").
4. Then look at systems: physics tick rate, animation, UI rebuilds, nav, particles ([[Particle Systems and Layered Effects]], [[VFX Graph]]).
## Official docs
- Profiling tools reference: https://docs.unity.com/en-us/engine/6000.5/manual/analysis/performance-profiling-tools
- Profiling Core package: https://docs.unity.com/en-us/engine/6000.5/manual/packages-list/packages-all/pack-safe/com-unity-profiling-core