scenario-unity-performance
Unity performance (performance engineer)
Expert performance work is a measurement discipline: a budget in milliseconds, the bounding thread named before any change, the biggest measured cost fixed first, structure before tuning, and every fix proven with before/after numbers from a development player. An agent does all of it headless: AgentProfile in Editor Play mode to iterate, PerfProbe in a batch-mode player for verdicts, PerfCapture to attribute costs from a Profiler capture, metal_trace for GPU time, PerfBuild for size. Target: Unity 6000.3.21f1, URP 17.3, macOS Apple Silicon. If a sibling skill named here is missing from your available skills, ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and flag the gap.
REQUIRED BACKGROUND: scenario-unity-expert (channels, review loop, 6.3 traps, ut_* toolkit).
Stance (the expert delta)
- Name the bound before touching anything. "If you speed up the GPU when your profile looks like this it's not going to run any faster" (Peter Hall, Unity Profiler team, _cV1B2hqXGI [00:05:50]). Observed: removing 1.36 MB of garbage per frame did not move a frame bound by 5,946 SetPass calls. GPU bound needs GPU timing; the Frame Debugger has none (Hall [frame 00:09:18]).
- Players decide, the Editor iterates. The authority is a development build on the lowest target device (profiling e-book p. 18). GRD: 4.4x in a 2M-instance stress test, 2.07x in the Editor but 1.45x standalone in a real game (Oc6T4hh5gaI [frame 00:12:41]); here the Editor swung 1.5x to 3.0x, players 1.9x to 2.8x on a stress fixture. Forecast about 1.5x.
- GC.Alloc sample time is not the cost. Unity records only timestamp and size (e-book p. 27): 54,021 allocations shown as 1.01 ms caused 19.97 ms of
GC.Collect(Hall [frame 00:21:52]). Observed: 27,000 allocations per frame displayed 0.8 to 1.1 ms; the allocating code cost 3.8 to 5.4 ms. Count bytes and calls, measure collections, target 0 B per gameplay frame, and keep the probe itself allocation-free (git-amend ham_w48aRJ4 [00:11:30]). - 6.3 URP draw calls = SRP Batcher + GPU Resident Drawer; the 2024 advice is Built-in-only. Material "Enable GPU Instancing" off, static batching off, no MaterialPropertyBlock (6.3 Manual table). GRD silently does nothing unless BatchRendererGroup Variants = Keep All and every renderer of the URP asset is Forward+ or Deferred+ (URP 17.3 source, observed). Then prove coverage: GRD draws are named "Hybrid Batch Group" in the Frame Debugger (GUI editor only), each other draw is a renderer that fell out (MPB, LPPV, realtime GI, non-DOTS shader, skinned; 6.3 Manual); headless, check eligibility per renderer after
Start. - Data layout first; spreading work over frames hides cost. 200,000 objects: MonoBehaviour Update 27 ms to 0.13 ms through manager, structs, Burst, SoA; "it's just spreading the work around" (Jason Booth, NAVbI1HIzCE [00:06:38]-[00:18:17]).
- Attribute every hitch before fixing it, with markers kept in code. Streaming and spawn systems carry their own ProfilerMarkers and counters; Deep Profile only finds what fills a gap (Hall [00:18:39]-[00:19:12]). Compiles show as
Shader.CreateGPUProgram(warm a traced GraphicsStateCollection during loading, Nicolas Borromeo CmD8MVGkDxQ [00:11:14]-[00:14:35]); here warm-up removed all 18 first-use compiles, yet the first frame stayed about 250 ms onMono.JIT. - Resident memory kills, untracked usually does not. 0.75 GB untracked was 140 MB resident (Borromeo [00:32:22]); Editor memory is about 5x the player's (Uuzd39AjFWQ [00:10:03]). On a laptop, paging and VRAM overflow are stutter causes of their own.
- Counts are exact, times drift [added, observed]. On a shared machine (load average 50 to 210 here), interleave A/B runs and require a consistent sign.