İçeriğe geç
Blog

Android Cihaz Parçalanması: Cihaza Özel Hataları Avlamak

Yalnızca belirli Android cihazlarda görünen hataların izini sürme yöntemi: cihaz parmak izi, uzaktan log toplama, çökme raporu kategorileri ve cihaz elinizde yokken yeniden üretme teknikleri.

IIlhan Seyhan6 dk okuma1 görüntülenme
Android Cihaz Parçalanması: Cihaza Özel Hataları Avlamak

Bazı hatalar yalnızca tek bir cihaz modelinde, tek bir GPU ailesinde ya da 2 GB RAM'li telefonlarda görünür; elinizdeki test cihazında hiç ortaya çıkmaz. Bu yazıda böyle bir raporu nasıl sınıflandırdığımızı, uzaktan hangi veriyi topladığımızı ve o cihaz elimizde yokken hatayı nasıl yeniden üretilebilir hale getirdiğimizi anlatıyoruz.

İlk soru: hata mı, cihaz sınıfı mı?

Cihaza özel bir raporun teşhisi dört eksende başlar: cihaz modeli, GPU ve sürücü, Android sürümü, bellek sınıfı. Tek bir kullanıcıdan gelen "oyun açılmıyor" mesajı bu eksenlerin hiçbirini söylemez. Bu yüzden ilk iş, tek olaydan bir küme çıkarmaktır: aynı belirti kaç farklı modelde var, o modellerin ortak yanı ne?

Google Play Console'da çökme ve ANR oranlarını cihaz modeline göre kırabiliyorsunuz. Play'in "kötü davranış" eşikleri genel düzeyde kullanıcı tarafından algılanan çökme oranı için %1,09 ve ANR oranı için %0,47; cihaz modeli başına ise günlük aktif kullanıcıların %8'i eşiği geçerli (Play Console Yardım, Android Developers Blog). Bu iki sayı arasındaki mesafe cihaz parçalanmasının tam tanımıdır: toplam oranınız temiz görünürken tek bir modelde eşiğin katbekat üstünde olabilirsiniz. Ortalamalara bakan ekip bu hatayı hiç görmez.

Cihaz parmak izi: her oturumun ilk log satırı

Cihaza özel hataların en sık nedeni, raporun cihazı tanımlamaması. Biz her oturumun başında tek satırlık bir parmak izi yazıyoruz ve bu satırı hem yerel log tamponuna hem çökme raporunun özel anahtarlarına koyuyoruz. Unity tarafında bunun için ek bir eklenti gerekmiyor:

using System.Text;
using UnityEngine;

public static class DeviceFingerprint
{
    public static string Build()
    {
        var sb = new StringBuilder(256);
        sb.Append("app=").Append(Application.version).Append('|');
        sb.Append("model=").Append(SystemInfo.deviceModel).Append('|');
        sb.Append("os=").Append(SystemInfo.operatingSystem).Append('|');
        sb.Append("cpu=").Append(SystemInfo.processorType)
          .Append('x').Append(SystemInfo.processorCount).Append('|');
        sb.Append("ram=").Append(SystemInfo.systemMemorySize).Append("MB|");
        sb.Append("gpu=").Append(SystemInfo.graphicsDeviceName).Append('|');
        sb.Append("vendor=").Append(SystemInfo.graphicsDeviceVendor).Append('|');
        sb.Append("api=").Append(SystemInfo.graphicsDeviceType.ToString()).Append('|');
        sb.Append("driver=").Append(SystemInfo.graphicsDeviceVersion).Append('|');
        sb.Append("vram=").Append(SystemInfo.graphicsMemorySize).Append("MB|");
        sb.Append("shader=").Append(SystemInfo.graphicsShaderLevel);
        return sb.ToString();
    }
}

Bu satırın en değerli alanı driver. Aynı GPU modeli farklı sürücü sürümleriyle farklı davranır; Unity belgeleri de aynı ekran kartının platforma ve sürücüye göre farklı adlarla raporlanabileceğini not ediyor (Unity Scripting API). Bu yüzden gruplamayı model adına değil, üretici + API + sürücü sürümü üçlüsüne göre yapıyoruz. Parmak izi üreten kodu paylaşılan araç katmanında tutmanın nedenini İki Oyun Bir Kod Tabanı yazısında anlatmıştık: teşhis altyapısı, oyunlar arasında tekrar yazılmaması gereken ilk şey.

Uzaktan log toplama: az veri, çok yapı

Uzaktan log toplarken hata, "her şeyi gönder" demek. Kullanıcının veri kotasını yakar, sizi de okunmaz bir yığınla bırakır. Bizim kullandığımız düzen şu:

  • Halka tampon. Son 200 satırı bellekte tutan sabit boyutlu bir tampon; yalnızca çökme, ANR ya da kullanıcı raporu anında yukarı gidiyor.
  • Kırıntı (breadcrumb) satırları. "Sahne yüklendi", "satın alma başladı", "arka plana geçildi" gibi olaylar tek kelimelik etiketlerle. Serbest metin değil, sabit sözlük — böylece sonradan gruplanabiliyor.
  • Oturum kimliği. Kullanıcı kimliği değil oturum kimliği; aynı çökmenin öncesindeki 30 saniyeyi yeniden dizmek için yeterli.

Oranlara bakarken de bir tuzak var: Play'in Android vitals ölçümü günlük aktif kullanıcı temelli, çökme raporlama araçları ise genellikle oturum temelli. Android belgelerindeki örnekte bir kullanıcı oyunu günde üç kez oynayıp bir çökme yaşadığında vitals %100, Crashlytics %33 gösteriyor (Android Developers). İki panelin sayısı uyuşmadığında panik yapmak yerine hangi tabanı ölçtüğünü kontrol etmek gerekiyor.

Çökme raporunda dört ayrı ölüm biçimi

  1. Yönetilen istisna. C# tarafında yakalanmamış istisna. Yığın izi okunur, satır numarası vardır, genelde cihazdan bağımsızdır.
  2. Yerli çökme. SIGSEGV, SIGABRT. Sürücü, IL2CPP kodu ya da yerli eklenti kaynaklı. Sembol dosyalarını her yayınla birlikte yüklemezseniz elinizde yalnızca adresler kalır.
  3. ANR. Çökme değil; ana iş parçacığı yanıt vermiyor. Yığın izinde dosya okuma, ağ çağrısı ya da kilit beklemesi görürsünüz. Cihaz parçalanmasının en tipik alanı burasıdır, çünkü yavaş depolama ve yavaş çekirdek aynı kodu farklı süreye çıkarır.
  4. Sistemin süreci öldürmesi. Hiç rapor üretmez. Android 11 (API 30) ile gelen ApplicationExitInfo ve getHistoricalProcessExitReasons() çağrısı son çıkışların nedenini veriyor; REASON_LOW_MEMORY düşük bellek katilinin öldürdüğü süreci işaretliyor, ama belgeler her cihazın bu nedeni bildirmediğini söylüyor (Android referansı).

Dördüncü kategoriyi ayrı takip etmek işe yarıyor: "çökme oranımız düşük ama kullanıcılar oyun kapanıyor diyor" şikayetlerinin çoğu buradan çıkıyor.

Düşük bellek: rapor yok, şikayet var

Belirti tanıdıktır: oyun arka plandan dönüşte baştan başlıyor, uzun oturumlarda sahne geçişinde kapanıyor, aynı sürüm 4 GB'lık cihazda sorunsuz çalışıyor. Bakılacak yer tepe bellek kullanımı, ortalama değil. En yüksek tepe genellikle sahne geçişinde oluşur: eski sahnenin kaynakları henüz boşalmamışken yenisi yüklenir ve iki atlas aynı anda bellektedir.

Ölçüm ve baskı üretmek için kullandığımız komutlar:

adb shell dumpsys meminfo com.ornek.oyun
adb logcat -v time -s Unity:V CRASH:V DEBUG:V
adb shell am kill com.ornek.oyun

Son komut, uygulama arka plandayken sistemin öldürmesini taklit eder; "arka plandan dön" senaryosunu böylece deterministik test edilebilir hale getiriyoruz. Unity tarafında Application.lowMemory olayına bir kırıntı satırı bağlamak da, çökme öncesi son uyarıyı görmek için ucuz bir kazanç. Kare süresi ve bellek tepe noktalarını profiler'da okumanın ayrıntılarını Mobilde Kare Süresi Avı yazısında toplamıştık.

GPU ve sürücü farkları: görsel bozulmayı teşhis etmek

Görsel hatalar cihaz parçalanmasının en can sıkıcı kısmı, çünkü çökme raporu üretmezler. Sık gördüğümüz belirtiler: tek renk kaplayan siyah alanlar, gradyanlarda bantlanma, saydam nesnelerin yanlış sırayla çizilmesi, gölgenin bir cihaz ailesinde tamamen kaybolması.

Nedenler dar bir kümede toplanıyor: shader'daki hassasiyet seçimi (yarım hassasiyet bazı sürücülerde yeterken bazılarında bozuluyor), tile tabanlı GPU'larda render hedefinin yükleme/kaydetme davranışı, derinlik tamponu formatı, MSAA çözümleme adımı ve doku sıkıştırma formatının desteklenmediğinde devreye giren yedek yol. Bunların hepsi sürücüye bağlı davranışlar, motorun sürümüne değil.

Teşhis yöntemi basit: kullanıcıdan ekran görüntüsü isteyin, parmak izindeki vendor ve driver alanlarıyla eşleştirin. Üç kullanıcının üçü de aynı GPU ailesindeyse artık elinizde bir hata değil, bir hedef var. Thornguard: Tower Defense gibi ekranda çok sayıda hareketli birim ve efekt bulunan bir tür için bu tür bozulmalar okunabilirliği doğrudan etkiliyor; okunabilirlikle estetik arasındaki dengeye dair notlarımız Kart Arayüzü Tasarımı yazısında.

Cihaz elinizde yokken yeniden üretmek

Yöntemin özü şu: cihazı taklit etmeye çalışmıyoruz, cihazın uyguladığı kısıtı taklit ediyoruz. Dört kısıt ve karşılığı:

  • Bellek tavanı. Doku çözünürlüğünü ve atlas sayısını yapay olarak şişirip elimizdeki cihazı hedef cihazın bellek baskısına sokuyoruz.
  • Sürücü ve özellik seti. Grafik API sırasını değiştirip Vulkan yerine OpenGL ES 3'ü zorluyoruz; shader'da hassasiyeti aşağı çekip aynı bozulmayı masaüstünde görmeyi deniyoruz.
  • CPU ve çöp toplama baskısı. Kare başına ayırma miktarını yükseltip düşük çekirdekli cihazın ANR eşiğine yaklaşıyoruz.
  • Depolama gecikmesi. Kaynak yüklemeye yapay gecikme ekleyip ana iş parçacığındaki bekleyen çağrıları açığa çıkarıyoruz.

Bu dördünden hiçbiri işe yaramazsa sıra bulut cihazlarında. Firebase Test Lab, oyunlar için "demo modu" üzerinden çalışan Game Loop testini destekliyor ve testler fiziksel cihazlarda en fazla 45 dakika, sanal cihazlarda 60 dakika sürüyor (Firebase belgeleri). 45 dakikalık sınır, senaryoyu tek bir hataya odaklı ve kısa yazmayı zorunlu kılıyor.

Son adım tekrar edilebilirlik ölçütü. Bizim eşiğimiz kaba ama işlevsel: 10 denemede en az 8 kez üretilemiyorsa hata henüz anlaşılmamıştır, düzeltmesi de doğrulanamaz. Bunu sağlamak için rastgele sayı üretecine sabit tohum veriyoruz ve girdileri kaydedip tekrar oynatıyoruz.

Kapanışı kanıtlamak

Cihaza özel bir düzeltmenin çalıştığını tek bir test cihazı söyleyemez. Sürümü kademeli yayınla açıp, oranı yine cihaz modeli kırılımında izliyoruz; karşılaştırma için düzeltme öncesi aynı modelin oranını not almak şart, yoksa iyileşmeyle mevsimsel dalgalanma birbirine karışır.

Kısa kontrol listesi:

  • Her oturumda tek satır cihaz parmak izi, sürücü sürümü dahil.
  • Çökmeyi dört kategoriye ayırın; "raporsuz ölümler" için çıkış nedenlerini okuyun.
  • Oranları toplamda değil cihaz modeli kırılımında izleyin.
  • Cihazı değil kısıtı taklit edin: bellek, sürücü, CPU, IO.
  • Düzeltilen her cihaz hatası, test matrisine kalıcı bir satır olarak eklenir.
Paylaş:

Yorumlar

İlk yorumu siz yapın.