See what FrameDoctor delivers
This is a real analysis report for a Unity mobile game running below its frame budget. Your reports will look just like this.
Analysis Report
AndroidAstral Drift(Unity 2022.3.16f1)
Scene: GameplayScene_Level3
Capture
1,200 frames
20.0s duration
Average FPS
24.3 FPS
70.6% over budget
GC Allocated
48.7 MB
Avg Draw Calls
1842
Executive Summary
Bottleneck: CPU-bound
Performance Scores
P50 (Median)
24.8ms
P95
39.6ms
P99
68.7ms
Average
31.2ms
Max
82.3ms
EnemySpawner.SpawnWave() allocates 2.4 MB/frame via List<T> creation — use object pooling and pre-allocated lists
Replace Instantiate/Destroy with Unity's ObjectPool<T>. Pre-allocate your lists once in Awake() instead of creating new ones each frame.
Reference: Read more at Unity docs: “Pool.ObjectPool <T1>”
14 textures are uncompressed RGBA32 at 2048x2048 (224 MB total) — switch to ASTC 6x6 compression for ~85% memory reduction
Select all affected textures in Project window → Inspector → Format: ASTC 6x6 → Apply. Use a Texture Import Preset to enforce this for future imports.
Reference: Read more at Unity docs: “TextureImporter”
1,842 draw calls per frame (target: <1,000 on mobile) — 40% of batches break due to different materials on similar meshes
Enable SRP Batcher in URP Asset settings, use GPU Instancing on particle materials, and merge materials where objects share the same shader.
Reference: Read more at Unity docs: “SRPBatcher”
Physics.ProcessReports consuming 8.2ms/frame with 47 active Rigidbodies at 50Hz — reduce Fixed Timestep to 0.04
Edit > Project Settings > Time → change Fixed Timestep from 0.02 to 0.04. Also add OnBecameInvisible/OnBecameVisible callbacks to sleep off-screen Rigidbodies.
Reference: Read more at Unity docs: “TimeManager”
Total memory climbed 36 MB over the 20-second capture with no corresponding scene changes — investigate persistent allocations
Use the Memory Profiler package to take two snapshots (start and end of gameplay) and diff them. Look for growing collections, event listeners not unsubscribed, or objects not returned to pools.
Reference: Read more at Unity docs: “index”
Implement Object Pooling for Enemy Spawning
Every time an enemy spawns, Unity creates a brand new object from scratch and throws it away when it dies. This is like buying a new plate for every meal and smashing it when you're done. Object pooling reuses the same objects over and over, eliminating the garbage collection pauses that cause your game to stutter.
Before
2.4 MB allocated per frame, GC spikes every ~450ms causing 8-12ms hitches
After
Near-zero allocation in spawn loop, GC spikes eliminated from gameplay
Replace Instantiate() / Destroy() calls in EnemySpawner.SpawnWave() with an object pool. Current implementation allocates a new List<Enemy> every frame and instantiates 3-8 GameObjects per spawn cycle.
| 1 | // Before (allocates every frame) |
| 2 | void SpawnWave() { |
| 3 | var enemies = new List<Enemy>(); |
| 4 | for (int i = 0; i < count; i++) { |
| 5 | enemies.Add(Instantiate(prefab)); |
| 6 | } |
| 7 | } |
| 8 | |
| 9 | // After (zero allocation) |
| 10 | private ObjectPool<Enemy> _pool; |
| 11 | private readonly List<Enemy> _activeEnemies = new(32); |
| 12 | |
| 13 | void Awake() { |
| 14 | _pool = new ObjectPool<Enemy>( |
| 15 | createFunc: () => Instantiate(prefab), |
| 16 | actionOnGet: e => e.gameObject.SetActive(true), |
| 17 | actionOnRelease: e => e.gameObject.SetActive(false), |
| 18 | defaultCapacity: 32 |
| 19 | ); |
| 20 | } |
| 21 | |
| 22 | void SpawnWave() { |
| 23 | _activeEnemies.Clear(); |
| 24 | for (int i = 0; i < count; i++) { |
| 25 | var enemy = _pool.Get(); |
| 26 | enemy.Reset(spawnPoint); |
| 27 | _activeEnemies.Add(enemy); |
| 28 | } |
| 29 | } |
Reference: Read more at Unity docs: “Pool.ObjectPool <T1>”
Compress Textures to ASTC Format
Your textures are stored in raw, uncompressed format — like saving every photo as a full-quality BMP. ASTC is a modern compression format designed for mobile GPUs that makes textures ~6x smaller while looking nearly identical. This single change will free up ~190 MB of memory.
Before
224 MB in RGBA32 uncompressed textures
After
~34 MB with ASTC 6x6 compression (85% reduction)
14 textures are using uncompressed RGBA32 format at 2048x2048. Switch to ASTC 6x6 for Android to reduce memory by ~85% with minimal visual impact.
Reference: Read more at Unity docs: “TextureImporter”
Enable SRP Batcher and GPU Instancing
Unity has to send a separate 'draw this' command to the GPU for each visual object that uses different settings. The SRP Batcher groups objects with compatible shaders together so the GPU can draw them much faster in bulk. Think of it as giving the GPU a shopping list instead of sending it to the store 1,842 separate times.
Before
1,842 draw calls, 40% batch breaks from material differences
After
~900 draw calls with SRP Batcher and GPU Instancing enabled
40% of batch breaks are caused by different material property blocks on similar meshes. Enable the SRP Batcher in URP settings and convert particle systems to use GPU Instancing.
Reference: Read more at Unity docs: “SRPBatcher”
Reduce Physics Fixed Timestep
Unity is recalculating the physics for all 47 objects in your scene 50 times per second. For a mobile game that isn't a physics simulator, 25 times per second is more than enough — players won't notice the difference, but your CPU will thank you.
Before
Physics.ProcessReports: 8.2ms/frame at 50Hz with 47 Rigidbodies
After
~4ms/frame at 25Hz, further reduced by disabling off-screen bodies
Physics simulation is running at 50Hz (0.02s timestep) which is excessive for a mobile action game. Increase to 0.04s (25Hz) for non-precision gameplay. Also consider reducing the Rigidbody count from 47 to ~20 by disabling physics on off-screen objects.
Reference: Read more at Unity docs: “TimeManager”
Optimize Canvas Rebuild Frequency
When any single UI element changes (like a score counter), Unity rebuilds the entire canvas layout — including all the elements that didn't change. By putting your frequently-updating elements on a separate canvas, Unity only rebuilds the small canvas instead of the entire UI.
Before
Single canvas with all UI, rebuilding every frame at 4.1ms
After
Split canvas: static elements never rebuild, dynamic canvas ~0.8ms
UI.Canvas.Rebuild is consuming 4.1ms/frame because the main gameplay canvas is being marked dirty every frame. Split static and dynamic UI elements into separate canvases.
Scene Hierarchy
Reference: Read more at Unity docs: “UICanvas”
Stream Large Audio Clips
Your background music tracks are being loaded entirely into memory before playing — like downloading a whole movie before watching it. Streaming plays the audio as it reads from disk, using only a tiny buffer instead of the full file.
Before
3 audio clips using 42 MB in DecompressOnLoad mode
After
~200 KB streaming buffer per clip
3 background music clips are loaded fully decompressed into memory (42 MB total). Switch to Streaming load type for clips longer than 5 seconds.
Reference: Read more at Unity docs: “AudioClip”
CPU
Main thread is heavily loaded at 41.2ms average. Top offenders: EnemySpawner.SpawnWave (12.3ms), Physics.ProcessReports (8.2ms), UI.Canvas.Rebuild (4.1ms).
- 38% of frame time spent in scripting — primarily EnemySpawner and PlayerController
- GC allocations averaging 2.4 MB/frame causing 8-12ms spikes every ~450ms
- Physics running at 50Hz with 47 active Rigidbodies is 22% of frame time
- Canvas rebuild triggered every frame due to dynamic text on main canvas
GPU
GPU is under moderate pressure. Overdraw is the primary concern at 3.2x average, driven by overlapping transparent particle effects in the VFX system.
- Overdraw ratio of 3.2x vs 2.5x mobile target — particle VFX are the primary cause
- 87 SetPass calls could be reduced by sharing materials across similar objects
- 12 active shader variants — 5 could be stripped without visual impact
Memory
Total memory usage is 512 MB — critically high for mobile. Uncompressed textures account for 44% of the memory footprint. A potential memory leak was detected.
- 512 MB total — many low-end Android devices will kill the app above 400 MB
- 14 RGBA32 textures at 2048x2048 consuming 224 MB (44% of total)
- Memory climbed 36 MB over 20 seconds with no scene changes — potential leak
- Managed heap fragmentation at 38% — causing unnecessarily large heap
The SRP Batcher collects draw calls that use compatible shaders and renders them in one efficient batch. Without it, every object with a different material triggers a separate GPU state change.
- 1Open your URP Renderer Asset in Inspector
- 2Check 'SRP Batcher' under Advanced
- 3Verify materials use URP/Lit or URP/Unlit shaders (custom shaders need CBUFFER declarations)
Reference: Read more at Unity docs: “SRPBatcher”
Uncompressed RGBA32 textures use 16 MB each at 2048x2048. ASTC is decoded natively by modern mobile GPUs — the quality difference is negligible but the memory savings are 6-8x.
- 1Select all affected textures in the Project window
- 2In Inspector, switch to the Android tab
- 3Set Format to 'ASTC 6x6' and click Apply
- 4Create a Preset from this configuration for future imports
Reference: Read more at Unity docs: “TextureImporter”
DecompressOnLoad unpacks the entire audio file into memory before playing. For a 3-minute music track this wastes 18 MB when streaming only needs a 200 KB buffer.
- 1Select long audio clips (>5s) in the Project window
- 2In Inspector, set Load Type to 'Streaming'
- 3Keep short SFX clips as 'Decompress On Load' for instant playback
Reference: Read more at Unity docs: “AudioClip”
Fixed Timestep from 0.02 to 0.04 in Project Settings > TimePhysics runs in a fixed loop independent of frame rate. At 0.02s you're running 50 physics updates per second — overkill for a mobile action game. 25Hz (0.04s) is plenty for non-simulation gameplay.
- 1Edit > Project Settings > Time
- 2Change Fixed Timestep from 0.02 to 0.04
- 3Set Maximum Allowed Timestep to 0.1 to prevent spiral of death
Reference: Read more at Unity docs: “TimeManager”
IL2CPP compiles your C# code to native C++ ahead of time, eliminating the Mono JIT compilation overhead. This significantly improves method call speed and reduces memory usage of the scripting runtime.
- 1Edit > Project Settings > Player
- 2Set Scripting Backend to 'IL2CPP'
- 3Build and profile again to compare (first build will be slower)
Reference: Read more at Unity docs: “IL2CPP”
Application.targetFrameRate = 30 to prevent unnecessary GPU work above targetWithout a frame rate cap, the device renders as fast as possible — generating heat and draining battery for frames players can't perceive. Capping at your target FPS keeps thermal throttling at bay during long play sessions.
- 1Add `Application.targetFrameRate = 30;` in your game initialization script
- 2Also set `QualitySettings.vSyncCount = 0;` (targetFrameRate is ignored when VSync is on)
Reference: Read more at Unity docs: “Application-targetFrameRate”