Sample Report

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

Android

Astral 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

This capture reveals a CPU-bound mobile project running at 24.3 FPS — significantly below the 30 FPS target. The main bottleneck is excessive GC allocations in the gameplay loop (48.7 MB over 20 seconds), combined with 1,842 draw calls per frame which is nearly 2x the recommended limit for mobile. Memory pressure from uncompressed textures and un-pooled object instantiation is causing frequent GC spikes of 8-12ms.

Performance Scores

Overall
Critical
38
CPU
Critical
28
GPU
Great
83
Memory
Needs Work
45
Rendering
Good
64

P50 (Median)

24.8ms

P95

39.6ms

P99

68.7ms

Average

31.2ms

Max

82.3ms

Scripts15.7ms(38%)
Rendering10.3ms(25%)
Physics9.1ms(22%)
Animation3.3ms(8%)
UI2.8ms(7%)
Average frame time: 31.20ms(within budget)
CRITICALGC Allocation in Update Loop:CPU
Problem

EnemySpawner.SpawnWave() allocates 2.4 MB/frame via List<T> creation — use object pooling and pre-allocated lists

Fix

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>

CRITICALTexture Memory Explosion:Memory
Problem

14 textures are uncompressed RGBA32 at 2048x2048 (224 MB total) — switch to ASTC 6x6 compression for ~85% memory reduction

Fix

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

CRITICALDraw Call Count 2x Over Budget:Rendering
Problem

1,842 draw calls per frame (target: <1,000 on mobile) — 40% of batches break due to different materials on similar meshes

Fix

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

WARNINGPhysics Timestep Too Aggressive:CPU
Problem

Physics.ProcessReports consuming 8.2ms/frame with 47 active Rigidbodies at 50Hz — reduce Fixed Timestep to 0.04

Fix

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

WARNINGMemory Leak Detected:Memory
Problem

Total memory climbed 36 MB over the 20-second capture with no corresponding scene changes — investigate persistent allocations

Fix

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

1

Implement Object Pooling for Enemy Spawning

HighCPU
In plain English

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.

C#
1// Before (allocates every frame)
2void 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)
10private ObjectPool<Enemy> _pool;
11private readonly List<Enemy> _activeEnemies = new(32);
12
13void 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
22void 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}
Affected files:EnemySpawner.csEnemyPool.csWaveManager.cs
Expected impactReduces GC allocation by ~2.4 MB/frame, saving 4-6ms per frame

Reference: Read more at Unity docs: Pool.ObjectPool <T1>

2

Compress Textures to ASTC Format

HighMemory
In plain English

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.

Affected files:Environment_Diffuse_2048.pngCharacter_Atlas_2048.pngVFX_Smoke_2048.pngUI_Background_2048.png
Expected impactReduces texture memory from 224 MB to ~34 MB

Reference: Read more at Unity docs: TextureImporter

3

Enable SRP Batcher and GPU Instancing

HighRendering
In plain English

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.

Unity Editor Steps
In URP Asset settings:
Enable SRP Batcher: checked
For particle materials in the Inspector:
Enable GPU Instancing: checked
Avoid MaterialPropertyBlock on SRP Batcher-compatible shaders:
BAD: renderer.SetPropertyBlock(mpb)
GOOD: renderer.material.SetColor("_Color", color)
Affected files:URP-HighFidelity-Renderer.assetParticleMaterial_Smoke.matParticleMaterial_Fire.matParticleMaterial_Sparks.mat
Expected impactExpected to reduce draw calls from 1,842 to ~900

Reference: Read more at Unity docs: SRPBatcher

4

Reduce Physics Fixed Timestep

MediumCPU
In plain English

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.

Unity Editor Steps
Edit > Project Settings > Time:
Fixed Timestep: 0.04 (was 0.02)
Maximum Allowed Timestep: 0.1
Disable physics on off-screen objects:
Add OnBecameInvisible callback → rb.Sleep()
Add OnBecameVisible callback → rb.WakeUp()
Expected impactSaves ~4ms/frame from Physics.ProcessReports

Reference: Read more at Unity docs: TimeManager

5

Optimize Canvas Rebuild Frequency

MediumCPU
In plain English

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

Canvas_Static (HUD frame, buttons, backgrounds)
HealthBarFrame
MinimapFrame
ButtonLayout
Canvas_Dynamic (text that changes every frame)
ScoreText
TimerText
DamageNumbers
Affected files:GameplayCanvas.prefabHUDManager.cs
Expected impactReduces UI rebuild cost from 4.1ms to ~0.8ms/frame

Reference: Read more at Unity docs: UICanvas

6

Stream Large Audio Clips

LowMemory
In plain English

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.

Affected files:BGM_MainTheme.ogg (18 MB)BGM_BossMusic.ogg (14 MB)BGM_Ambient.ogg (10 MB)
Expected impactFrees ~42 MB of memory

Reference: Read more at Unity docs: AudioClip

Total potential savings:105.9 MB
5high3medium
4 Textures2 Audios2 Meshs
Uncompressed RGBA32 formatTexture
13.6 MB
Uncompressed RGBA32 formatTexture
13.6 MB
Uncompressed RGBA32, Read/Write enabledTexture
29.6 MB
DecompressOnLoad for long clip (3:42)Audio
17.8 MB
DecompressOnLoad for long clip (2:55)Audio
13.8 MB
High vertex count without LODMesh
~75% GPU cost at distance
Read/Write enabled unnecessarilyMesh
~2.1 MB
Oversized for UI elementTexture
15.4 MB
Total Memory(left axis)
Managed Heap(left axis)
GC Allocations per Frame(right axis)

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).

Key findings
  • 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
avg frame time ms: 41.2gc per frame mb: 2.4main thread utilization: 92%

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.

Key findings
  • 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
overdraw ratio: 3.2shader variants: 12set pass calls: 87

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.

Key findings
  • 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
total memory mb: 512texture memory mb: 224audio memory mb: 42managed heap mb: 89
1
Enable SRP Batcher in URP Asset → instantly reduces draw call overhead
30 seconds~20% fewer draw calls
Why this matters

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.

Steps
  1. 1Open your URP Renderer Asset in Inspector
  2. 2Check 'SRP Batcher' under Advanced
  3. 3Verify materials use URP/Lit or URP/Unlit shaders (custom shaders need CBUFFER declarations)

Reference: Read more at Unity docs: SRPBatcher

2
Set texture compression to ASTC 6x6 for all Android textures via import preset
5 minutes~190 MB memory saved
Why this matters

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.

Steps
  1. 1Select all affected textures in the Project window
  2. 2In Inspector, switch to the Android tab
  3. 3Set Format to 'ASTC 6x6' and click Apply
  4. 4Create a Preset from this configuration for future imports

Reference: Read more at Unity docs: TextureImporter

3
Switch audio clips over 5 seconds to Streaming load type
2 minutes~42 MB memory saved
Why this matters

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.

Steps
  1. 1Select long audio clips (>5s) in the Project window
  2. 2In Inspector, set Load Type to 'Streaming'
  3. 3Keep short SFX clips as 'Decompress On Load' for instant playback

Reference: Read more at Unity docs: AudioClip

4
Change Fixed Timestep from 0.02 to 0.04 in Project Settings > Time
10 seconds~4ms/frame saved
Why this matters

Physics 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.

Steps
  1. 1Edit > Project Settings > Time
  2. 2Change Fixed Timestep from 0.02 to 0.04
  3. 3Set Maximum Allowed Timestep to 0.1 to prevent spiral of death

Reference: Read more at Unity docs: TimeManager

5
Enable IL2CPP scripting backend for release builds to reduce managed overhead
5 minutes (build setting change)~15% scripting performance improvement
Why this matters

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.

Steps
  1. 1Edit > Project Settings > Player
  2. 2Set Scripting Backend to 'IL2CPP'
  3. 3Build and profile again to compare (first build will be slower)

Reference: Read more at Unity docs: IL2CPP

6
Set Application.targetFrameRate = 30 to prevent unnecessary GPU work above target
10 secondsReduced thermal throttling
Why this matters

Without 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.

Steps
  1. 1Add `Application.targetFrameRate = 30;` in your game initialization script
  2. 2Also set `QualitySettings.vSyncCount = 0;` (targetFrameRate is ignored when VSync is on)

Reference: Read more at Unity docs: Application-targetFrameRate

Ready to analyze your own project?

Get your first analysis free — no credit card required.