Heisenbug — hata ayıklamaya çalıştığınızda kaybolan bir hata. Terim, Heisenberg'in belirsizlik ilkesinden gelir: gözlem, sistemin davranışını etkiler. Mobil geliştirmede Heisenbug en zor sorunlardan biridir çünkü standart hata ayıklama yöntemleri (loglar, kesme noktaları, ek kod) program durumunu değiştirir ve hatayı gizler. Nedenlerini ve yakalanması zor hatalarla başa çıkma yöntemlerini inceleyelim.
Anahtar Noktalar
Heisenbug — üretimde veya normal çalışma sırasında ortaya çıkan, ancak hata ayıklama ortamında tekrarlanmaya çalışıldığında kaybolan bir hata sınıfı. Terim, 1980'lerde programcı Jim Gray tarafından dağıtık sistemler bağlamında ortaya atılmıştır, ancak bugün asenkron yapıları nedeniyle mobil uygulamalar için en alakalı olanıdır.
Ana neden: standart hata ayıklama araçları yürütme ortamını değiştirir. Bir kesme noktası iş parçacığını birkaç milisaniye duraklatır, günlükleme senkron G/Ç ekler, ek kontroller işlemlerin sırasını değiştirir. Çok iş parçacıklı bir ortamda, mikrosaniyelik bir gecikme bile iş parçacığı yürütme sırasını değiştirebilir ve bir veri yarışını gizleyebilir.
Microsoft Research'e (2022) göre, çok iş parçacıklı mobil uygulamalardaki tüm hataların yaklaşık %15-25'i Heisenbug olarak sınıflandırılır. Aynı zamanda, bir Heisenbug'u bulma ve düzeltme süresi, doğrudan tekrarlanamaması nedeniyle normal bir hatadan ortalama 5-10 kat daha uzundur.
Bir uygulama, listede hızlıca kaydırma yapıldığında üretimde çöker, ancak hata ayıklayıcıya bağlandığında veya log eklendiğinde — mükemmel çalışır. Nedeni: UI iş parçacığı (RecyclerView güncellemesi) ile arka plan iş parçacığı (adaptör veri güncellemesi) arasında veri yarışı. Loglar, iş parçacıklarını rastgele senkronize eden bir gecikme ekler.
Bohrbug — öngörülebilir, istikrarlı bir şekilde tekrarlanabilir hata. Bohr'un atom modeline benzetilerek adlandırılmıştır: bir atom gibi, hata her gözlemlendiğinde aynı şekilde davranır. Örnek: veriler yüklenmeden önce bir düğmeye tıklandığında NullPointerException. Standart birim testleri ile tedavi edilir.
Mandelbug — karmaşık, kaotik neden-sonuç ilişkisine sahip hata (Mandelbrot kümesine benzetilerek adlandırılmıştır). Yalnızca belirli koşulların bir kombinasyonu altında ortaya çıkar: OS sürümü, cihaz modeli, ağ durumu. Hata ayıklama sırasında kaybolmamasıyla Heisenbug'dan ayrılır — sorun tekrarlama zorluğudur, araçların davranışı değiştirmesi değil.
Heisenbug — tam olarak hata ayıklama araçları nedeniyle kaybolan hatadır. Bir log eklerseniz — hata kaybolur. Bir kesme noktası koyarsanız — hata ortaya çıkmaz. Her şeyi kaldırırsanız — hata geri döner. Ana neden: hata ayıklama sırasında değişen zamanlama.
| Tür | Tekrarlanabilirlik | Hata ayıklamaya tepki | Örnek |
|---|---|---|---|
| Bohrbug | %100 | Değişmez | Boş listede NPE |
| Mandelbug | Kaotik | Değişmez | Android 12, Samsung, düşük pilde çökme |
| Heisenbug | Sadece ayıklama olmadan | Kaybolur | Loglarla kaybolan veri yarışı |
| Schrödinbug | Kodda ortaya çıkmaz | Bakınca belirir | Kodda görünür ama asla tetiklenmez |
Race condition — Heisenbug'un bir numaralı nedeni. İki iş parçacığı senkronizasyon olmadan paylaşılan verilere erişir. Hata ayıklayıcı bir gecikme getirir ve iş parçacıklarının doğal olarak senkronize olmasına neden olur. Hata ayıklayıcı olmadan, yürütme sırası tahmin edilemez.
Zamanlamaya bağlı hatalar — yalnızca belirli bir yürütme hızında ortaya çıkan hatalar. Örneğin, bir sonraki işlem başlamadan önce tamamlanması gereken bir animasyon. Hata ayıklayıcıda animasyon daha yavaş çalışır ve işlem animasyon bittikten sonra başlamak için zaman bulur. Üretimde — tam tersi.
// Example race condition — typical Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Not thread-safe
}
}
fun getItems(): List<String> = items.toList()
// Race condition: getItems read may overlap with loadFromNetwork write
}
Derleyici optimizasyonu — derleyici (JIT, ART, Kotlin/Native) optimizasyon için talimatları yeniden sıralayabilir. Hata ayıklama yapısında (debug build) optimizasyonlar devre dışıdır ve kod «yazıldığı gibi» yürütülür. Sürüm yapısında (release build) derleyici işlem sırasını değiştirir, bu da kodda gizli varsayımları ortaya çıkarabilir.
ThreadSanitizer (TSan) — C/C++ ve Kotlin/Native'de veri yarışlarını tespit etmek için bir Google aracı. Yapıya gömülür ve senkronizasyon olmadan herhangi bir paylaşılan bellek erişimini tespit eder. Logların aksine TSan, G/Ç üzerinden değil, enstrümante kod aracılığıyla çalıştığı için zamanlamayı etkilemez.
Deterministik testler — gerçek eşzamansızlığı kontrollü eşzamansızlıkla değiştirin. Yürütme sırası üzerinde tam kontrol için TestDispatcher (Kotlin), RxJava Plugins veya GCD test kuyrukları (iOS) kullanın. Belirli senaryolar belirleyin: iş parçacığı A çalışır, sonra B, sonra tekrar A.
Döngüsel günlükleme — bellekteki halka tampona günlükleme (disk değil). Hata oluştuğunda, tampon bir dosyaya kaydedilir. Belleğe yazma nanosaniye sürdüğünden (disk G/Ç için milisaniye yerine), bu tür günlükleme zamanlamayı etkilemez ve Heisenbug'u maskelemez.
class CyclicBuffer(val capacity: Int = 1000) {
private val buffer = ArrayDeque<String>(capacity)
private val lock = Any()
fun log(message: String) {
synchronized(lock) {
if (buffer.size >= capacity) buffer.removeFirst()
buffer.addLast(message)
}
}
fun flush() {
synchronized(lock) { buffer.forEach { fileWriter.write(it) } }
}
}
Üretimde günlükleme — hata yerel olarak tekrarlanamıyorsa, üretimde veri toplayın. Firebase Crashlytics logları, Sentry Breadcrumbs veya özel bir döngüsel kaydedici kullanın. Önemli: günlükleme eşzamansız olmalı ve performans üzerinde minimum etkiye sahip olmalıdır.
Durum izolasyonu — paylaşılan değişebilir durumu en aza indirin. Her bileşen, diğer bileşenlerden doğrudan yazılamayan kendi izole durumuna sahip olmalıdır. Unidirectional Data Flow (UDF) kullanın — durum tek yönde akar: Olay → Azaltıcı → Durum → UI.
Fonksiyonel yaklaşım — yan etkileri olmayan saf işlevlerin test edilmesi ve hata ayıklaması daha kolaydır. Yan etkileri (ağ, DB, dosyalar) kesin olarak tanımlanmış katmanlarda (depo, veri kaynağı) izole edin. Fonksiyonel kodda iş parçacığı hataları neredeyse imkansızdır.
Sıkı mod — hata ayıklama yapısında Android StrictMode'u etkinleştirin. İş parçacığı politikası ihlallerini (ana iş parçacığında ağ, ana iş parçacığında disk G/Ç) tespit eder ve bir istisna fırlatır. Bu, potansiyel bir Heisenbug'u hemen görülebilen deterministik bir Bohrbug'a dönüştürür.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Eşzamansızlığa odaklanan kod incelemesi — sürecin zorunlu bir parçasıdır. Her çekme isteği, paylaşılan değişebilir durum, iş parçacığı güvenli olmayan koleksiyonlar ve senkronizasyon eksikliği açısından kontrol edilmelidir. Belirli kalıpları (örneğin, synchronized olmadan MutableList'e erişmek) otomatik olarak yasaklamak için lint kuralları kullanın.
Sıkça Sorulan Sorular
Çünkü standart yöntemler — kesme noktaları, loglar, print — yürütme ortamını o kadar değiştirir ki hata ortaya çıkmaz olur. Hata ayıklayıcı tüm iş parçacıklarını onlarca milisaniye duraklatır. Bu süre boyunca, hataya neden olan veri yarışı doğal olarak çözülür. Yürütme zamanlamasını etkilemeyen araçlar gerekir.
Mandelbug koşulların karmaşıklığı nedeniyle tekrarlanması zordur, ancak hata ayıklama araçları onun ortaya çıkışını etkilemez. Heisenbug ise tam olarak hata ayıklama araçları nedeniyle kaybolur. Mandelbug örneği: yalnızca Android 11, 3 GB RAM ve %15'in altında pil seviyesine sahip cihazlarda çökme. Heisenbug örneği: Log.d() eklendiğinde kaybolan veri yarışı.
Flaky test tespiti kullanın — bazen başarısız olan, bazen geçen testler. Android'de test izolasyonu için Android Test Orchestrator kullanın. Hata ayıklama testlerine StrictMode ekleyin. Yapıyı ThreadSanitizer ile enstrümante edin. Bir test çalıştırmaların >%5'inde flaky ise — onu potansiyel bir Heisenbug olarak kabul edin ve birleştirmeden önce araştırın.
Kısmen. Kotlin'de Flow ve yapılandırılmış eşzamanlılık, paylaşılan değişebilir durum miktarını azaltır ve iş parçacığı yönetimini basitleştirir. Ancak coroutine'ler iş parçacığı güvenliğini garanti etmez: iki coroutine durumu paylaşıyorsa, veri yarışı hala mümkündür. Paylaşılan durumu korumak için Mutex veya coroutine'ler arasında veri geçirmek için Channel kullanın.
Hata durumunda otomatik olarak boşaltılan bellekte döngüsel log tamponu kullanın. Özel breadcrumbs ile Crashlytics veya Sentry aracılığıyla ayrıntılı izleme ekleyin. Android için ANR tespitini etkinleştirin ve izleri kontrol edin. Hata bir veri yarışıysa, üretim benzeri yük ile hata ayıklama yapısında ThreadSanitizer sorunu ortaya çıkarabilir.
Özet
Anahtar teslim bir mobil uygulama geliştireceğiz
IT Sectr, 2017'den beri girişimler ve işletmeler için iOS ve Android uygulamaları oluşturmaktadır. Size danışmanlık yapacak ve en iyi çözümü önereceğiz.
Ayrıca okuyun