Editörde 200 fps gösteren bir sahne, orta segment bir Android telefonda 22 fps'e düşebilir. Bu yazıda, o düşüşün nereden geldiğini tahminle değil ölçümle bulmak için kurduğumuz teşhis sırasını aktarıyoruz: hangi build ile bağlanılır, CPU mu GPU mu sınırlıyor sorusu nasıl cevaplanır, draw call ve garbage collection ne zaman şüpheli olur.
Önce bütçeyi rakama çevir
"Oyun takılıyor" bir teşhis değil. Kare süresi bütçesi ise rakamdır: 60 fps istiyorsan bir karede toplam 16,6 ms, 30 fps istiyorsan 33,3 ms'in var. Bu süre CPU'nun oyun mantığı, GPU'nun çizim işi ve sistemin geri kalanı arasında paylaşılır — üstelik mobilde bu bütçe sabit değildir, cihaz ısındıkça kırpılır.
Bu yüzden ilk adım hedefi yazmak: "Bu sahne hedef cihazda 33 ms altında kalacak." Hedef yoksa profiler'da gördüğün her sayı ilginç görünür ama hiçbiri karar üretmez.
Editörde sorun görünmemesinin sebepleri
Editör, hedef cihazın koşullarını taklit etmez. Aradaki farkın en sık kaynakları şunlar:
- İşlemci sınıfı: Masaüstü tek çekirdek performansı, mobil çekirdeklerin kat kat üstünde. Editörde 2 ms olan bir döngü telefonda 10 ms olabilir.
- Bellek bant genişliği ve dolgu oranı: Mobil GPU'lar yüksek çözünürlükte saydam katman üst üste bindiğinde çok daha çabuk tıkanır.
- Termal kısıtlama: İlk 30 saniye iyi, üçüncü dakikada kötü. Editörde bu eğri hiç yok.
- Editör gürültüsü: Editörde ölçtüğün profil, editörün kendi yükünü de içerir; scene view, inspector çizimi, domain reload sonrası ilk kareler.
Sonuç: mobil performans kararı mobil cihazda alınır. Editör profili sadece kaba bir ön tarama aracı.
Cihaza bağlanmadan ölçüm başlamaz
Unity Profiler'ı cihazda çalışan bir yapıya bağlayabilirsin; Build Settings içindeki Development Build ve Autoconnect Profiler seçenekleri bu iş için var, cihaz aynı ağda değilse Android'de USB üzerinden port yönlendirme kullanılır (Unity Manual: Profiling your application).
Burada bilinçli bir ödünç var: development build, release build'den yavaştır. Yani ölçtüğün mutlak sayılar son sürümün sayıları değil. Bizim yaklaşımımız şu: oranları development build'de, mutlak kare süresini release build'de okumak. Hangi fonksiyonun payının büyük olduğunu development build söyler; "16 ms'in altında mıyız" sorusunu release build'de cihazın kendi fps sayacıyla ya da platform araçlarıyla cevaplarız.
Birinci soru: CPU mu GPU mu sınırlıyor
Bu ayrımı yapmadan atılan her optimizasyon adımı kumar. Profiler'da baktığımız işaretler:
- CPU sınırlı: Kare süresinin büyük kısmı PlayerLoop içindeki script'lerde, fizikte, animasyonda ya da UI yeniden inşasında geçiyor. Ana iş parçacığı dolu, GPU bekliyor.
- GPU sınırlı: Ana iş parçacığında işi olmayan bekleme örnekleri kabarıyor —
Gfx.WaitForPresentOnGfxThreadve benzeri bekleme kalemleri kare süresinin yarısını yiyorsa CPU işini bitirmiş, GPU'yu bekliyordur.
Hızlı bir çapraz kontrol daha var: çözünürlüğü düşür. Cihazda render ölçeğini yarıya indirdiğinde kare süresi belirgin şekilde iyileşiyorsa sorun GPU tarafında (dolgu oranı, shader maliyeti, overdraw). Hiç değişmiyorsa CPU tarafındasın. Bu tek deney, saatlerce yanlış yerde kazma yapmayı önlüyor.
CPU tarafı: hangi satır pahalı
Hierarchy görünümünde sıralamayı toplam süreye göre yapıp ilk üç kaleme bakmak genelde yeterli. İki sütun kritik: Time ms ve GC Alloc. İkinci sütun sıfır değilse, o kare için çöp üretiyorsun.
Deep Profile cazip görünür ama her metoda ölçüm eklediği için kare süresini şişirir ve dağılımı bozar. Biz onu son çare olarak, sadece daraltılmış bir şüpheliyi doğrulamak için açıyoruz. Daha ucuz yol, kodun içine kendi işaretlerini koymak:
using Unity.Profiling;
using UnityEngine;
public class TargetScanner : MonoBehaviour
{
static readonly ProfilerMarker s_ScanMarker = new ProfilerMarker("Tower.ScanTargets");
[SerializeField] float radius = 5f;
[SerializeField] LayerMask enemyMask;
readonly Collider[] _hits = new Collider[16];
void Update()
{
using (s_ScanMarker.Auto())
{
int count = Physics.OverlapSphereNonAlloc(
transform.position, radius, _hits, enemyMask);
for (int i = 0; i < count; i++)
{
// hedef seçimi
}
}
}
} İki şey yapıyor: profiler'da kendi adıyla görünen bir blok açıyor ve OverlapSphereNonAlloc ile önceden ayrılmış diziyi kullanarak her karede yeni dizi üretmiyor. Thornguard: Tower Defense gibi kule savunma oyunlarında sahnede aynı işi yapan onlarca nesne olur; böyle bir aramanın maliyeti nesne sayısıyla çarpılarak büyür, o yüzden ilk aday listesinde hep bu tür döngüler bulunur.
GC dikeni: ortalama iyi, kare kötü
Garbage collection sorunları ortalamada görünmez; grafiği testere dişine çevirir. Ortalama 14 ms, her iki saniyede bir 45 ms'lik bir kare — oyuncunun "takılıyor" dediği şey tam olarak bu.
Mobilde en sık gördüğümüz çöp kaynakları: her karede string birleştirme (skor, süre, sayaç metinleri), foreach içinde boxing'e yol açan arayüz kullanımları, dizi döndüren API'ler, LINQ, ve GetComponent sonuçlarını önbelleğe almamak. Çözüm tek tek küçük: metni sadece değer değiştiğinde güncelle, listeleri yeniden kullan, sonuçları alana al. Toplamda 45 ms'lik dikenler kaybolur.
Draw call ve Frame Debugger
Sorun GPU ya da render tarafındaysa sıradaki araç Frame Debugger. Bir kareyi çizim çağrılarına ayırıp adım adım gezdiriyor, her adımda ekranın o ana kadar ne kadarının çizildiğini gösteriyor (Unity Manual: Debug a frame).
Bakılacak üç şey:
- Toplam çağrı sayısı ve gruplanma: Aynı görünen nesneler ayrı ayrı çiziliyorsa batch kırılıyor. Frame Debugger seçili adımda batch'in neden ayrıldığını yazar; farklı malzeme örneği, farklı ölçek işareti, araya giren bir katman gibi gerekçeler burada okunur.
- UI katmanları: Tam ekran saydam paneller ve üst üste binen arka planlar aynı pikseli birden çok kez boyar. Mobilde bunun bedeli doğrudan kare süresine yansır. Kart arayüzü tasarımında okunabilirlik için eklenen katmanların performans faturasını da bu pencerede görürsün.
- Sıra: Saydam nesneler derinlik testinden yararlanamaz; opak geometriden sonra çizilirler ve üst üste geldiklerinde maliyet katlanır.
Teşhis sırası
Aynı sırayı her seferinde tekrarlıyoruz, çünkü sıra bozulunca yanlış şeyi optimize etmek çok kolay:
- Hedef cihazı ve kare bütçesini yaz (örn. 33 ms).
- Development build ile cihaza bağlan, sorunun görüldüğü senaryoyu 60-90 saniye kaydet — termal düşüşü de yakalamak için.
- Çözünürlük deneyiyle CPU/GPU ayrımını yap.
- CPU ise: hierarchy'de ilk üç kalem + GC Alloc sütunu. Şüpheliyi
ProfilerMarkerile daralt. - GPU ise: Frame Debugger'da çağrı sayısı, batch kırılma gerekçesi, saydam katmanlar.
- Tek bir değişiklik yap, aynı senaryoyu tekrar kaydet, iki kaydı karşılaştır. Kaydedilmiş profilleri toplu karşılaştırmak için Profile Analyzer paketi işe yarıyor.
Altıncı adım en çok atlanan adım. Aynı anda üç iyileştirme yaparsan hangisinin işe yaradığını bilemezsin; bazen biri kazandırırken diğeri kaybettirir ve toplam değişmediği için ikisini de "işe yaramadı" diye kenara atarsın.
Ölçüm hijyeni
Karşılaştırılabilir olmayan ölçüm, ölçüm değil. Kendimize koyduğumuz kurallar:
- Aynı cihaz, aynı senaryo, aynı süre. Farklı sahnede alınan iki kayıt karşılaştırılmaz.
- Cihazı ölçüm öncesi soğut; arka planda açık uygulama bırakma.
- İlk 2-3 saniyeyi ayıkla: ısınma, shader derlemesi, ilk atlas yüklemeleri.
- Ortalamaya değil yüzdelik dilimlere bak. Oyuncunun hissettiği şey en kötü %1'lik karelerdir.
- Kazanımı rakamla yaz: "17,8 ms → 12,4 ms". "Daha akıcı oldu" bir sonraki hafta anlamsızlaşır.
Bu iş, aslında bir bütçeleme disiplini. Neyi ölçtüğünü ve neyin karşılığında ne ödediğini bilmek, hangi özelliğin taşınmaya değdiğine karar vermeyi de kolaylaştırıyor — özellik kesme kararlarında elimizdeki en sağlam veri genelde profiler kayıtları oluyor.
Yorumlar
İlk yorumu siz yapın.
