Heisenbug — debug etməyə çalışdıqda yox olan səhv. Termin Heisenberg qeyri-müəyyənlik prinsipindən gəlir: müşahidə sistemin davranışına təsir edir. Mobil inkişafda Heisenbug ən çətin problemlərdən biridir, çünki standart debug metodları (loglar, breakpointlər, əlavə kod) proqramın vəziyyətini dəyişir və səhvi gizlədir. Yaranma səbəblərini və tutulmaz səhvlərlə mübarizə metodlarını nəzərdən keçirək.
Əsas məqamlar
Heisenbug — istehsal mühitində və ya normal iş zamanı özünü göstərən, lakin debug mühitində təkrarlanmağa çalışdıqda yox olan səhvlər sinfi. Termin 1980-ci illərdə proqramçı Jim Gray tərəfindən paylanmış sistemlər kontekstində təqdim edilmişdir, lakin bu gün asinxron təbiətinə görə mobil tətbiqlər üçün ən aktualdır.
Əsas səbəb: standart debug alətləri icra mühitini dəyişir. Breakpoint axını bir neçə millisaniyə dayandırır, loglama sinxron I/O əlavə edir, əlavə yoxlamalar əməliyyatların sırasını dəyişir. Çoxaxınlı mühitdə hətta mikrosaniyəlik gecikmə belə axınların icra sırasını dəyişə və məlumat yarışını gizlədə bilər.
Microsoft Research (2022) məlumatlarına görə, çoxaxınlı mobil tətbiqlərdə bütün səhvlərin təxminən 15-25%-i Heisenbug kimi təsnif edilir. Bir Heisenbug-u tapmaq və düzəltmək üçün vaxt adi səhvdən orta hesabla 5-10 dəfə çoxdur, çünki birbaşa təkrarlamaq mümkün deyil.
Tətbiq istehsal mühitində siyahını sürətli çəkərkən çökür, lakin debugger qoşulduqda və ya loglar əlavə edildikdə mükəmməl işləyir. Səbəb: UI axını (RecyclerView yeniləməsi) və fon axını (adapter məlumatlarının yeniləməsi) arasında məlumat yarışı. Loglar gecikmə əlavə edir, bu da axınları təsadüfi şəkildə sinxronlaşdırır.
Bohrbug — proqnozlaşdırıla bilən, sabit təkrarlanan səhv. Borun atom modelinə bənzətmə ilə adlandırılmışdır: atom kimi, səhv hər müşahidədə eyni davranır. Nümunə: məlumat yüklənməmişdən əvvəl düyməni basdıqda NullPointerException. Standart vahid testi ilə müalicə olunur.
Mandelbug — mürəkkəb, xaotik səbəb-nəticə əlaqəsi olan səhv (Mandelbrot çoxluğuna bənzətmə ilə adlandırılmışdır). Yalnız müəyyən şərtlər kombinasiyasında özünü göstərir: OS versiyası, cihaz modeli, şəbəkə vəziyyəti, ay fazası. Heisenbug-dan fərqi ondadır ki, debug zamanı yox olmur — problem təkrarlamanın mürəkkəbliyindədir, alətlərin davranışı dəyişdirməsində deyil.
Heisenbug — məhz debug alətləri səbəbindən yox olan səhv. Log əlavə etsəniz — səhv yox olur. Breakpoint qoysanız — səhv özünü göstərmir. Hər şeyi silsəniz — səhv qayıdır. Əsas səbəb: debug zamanı dəyişdirilmiş timing.
| Tip | Təkrarlanma | Debug reaksiyası | Nümunə |
|---|---|---|---|
| Bohrbug | 100% | Dəyişmir | Boş siyahıda NPE |
| Mandelbug | Xaotik | Dəyişmir | Android 12, Samsung, aşağı batareyada çökmə |
| Heisenbug | Yalnız debug olmadan | Yox olur | Loglarla yox olan race condition |
| Schrödinbug | Kodda özünü göstərmir | Baxdıqda görünür | Kodda görünən, lakin heç vaxt işləməyən səhv |
Race condition — Heisenbug səbəbləri arasında bir nömrə. İki axın sinxronizasiya olmadan ümumi məlumatlara müraciət edir. Debugger gecikmə tətbiq edir, bunun nəticəsində axınlar təbii şəkildə sinxronlaşmağa vaxt tapır. Debugger olmadan icra sırası proqnozlaşdırıla bilməz.
Timing-dən asılı səhvlər — yalnız müəyyən icra sürətində özünü göstərən səhvlər. Məsələn, növbəti əməliyyat başlamazdan əvvəl bitməli olan animasiya. Debuggerdə animasiya daha yavaş gedir və əməliyyat animasiya bitdikdən sonra başlamağa vaxt tapır. İstehsal mühitində isə əksinə.
// Race condition nümunəsi — tipik Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Thread-safe deyil
}
}
fun getItems(): List<String> = items.toList()
// Race condition: getItems oxuması loadFromNetwork yazması ilə üst-üstə düşə bilər
}
Kompilyator optimallaşdırması — kompilyator (JIT, ART, Kotlin/Native) optimallaşdırma üçün təlimatları yenidən sıralaya bilər. Debug build-də optimallaşdırmalar söndürülür və kod “yazıldığı kimi” icra olunur. Release build-də kompilyator əməliyyatların sırasını dəyişir, bu da kodda gizli fərziyyələri üzə çıxara bilər.
ThreadSanitizer (TSan) — C/C++ və Kotlin/Native-də məlumat yarışlarını aşkarlamaq üçün Google aləti. Build-ə daxil edilir və sinxronizasiya olmadan paylaşılan yaddaşa hər girişi aşkarlayır. Loglardan fərqli olaraq, TSan timing-ə təsir etmir, çünki I/O vasitəsilə deyil, instrumented code vasitəsilə işləyir.
Deterministik testlər — real asinxronluğu idarə olunan ilə əvəz edin. İcra sırasına tam nəzarət üçün TestDispatcher (Kotlin), RxJava Plugins və ya GCD test queues (iOS) istifadə edin. Konkret ssenarilər təyin edin: axın A icra olunur, sonra B, sonra A yenidən.
Tsiklik loglama — diskə deyil, yaddaşda tsiklik buferə loglama. Səhv baş verdikdə, bufer fayla saxlanılır. Yaddaşa yazma nanosaniyələr çəkdiyindən (disk I/O üçün millisaniyələr əvəzinə), belə log timing-ə təsir etmir və Heisenbug-u maskalamır.
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) } }
}
}
İstehsal mühitində loglama — səhv lokal təkrarlanmırsa, istehsalda məlumat toplayın. Firebase Crashlytics logs, Sentry Breadcrumbs və ya xüsusi tsiklik logger istifadə edin. Vacib: loglama asinxron olmalı və performansa minimal təsir göstərməlidir.
Vəziyyətin izolyasiyası — paylaşılan mutable state-i minimallaşdırın. Hər komponent digər komponentlərdən birbaşa yazmaq üçün əlçatmaz olan öz izolyasiya edilmiş vəziyyətinə malik olmalıdır. Unidirectional Data Flow (UDF) istifadə edin — vəziyyət bir istiqamətdə axır: Event → Reducer → State → UI.
Funksional yanaşma — yan təsirləri olmayan təmiz funksiyaları test etmək və debug etmək daha asandır. Yan təsirləri (şəbəkə, verilənlər bazası, fayllar) ciddi müəyyən edilmiş təbəqələrdə (repository, data source) izolyasiya edin. Funksional koddaxıl səhvlər praktiki olaraq mümkün deyil.
Strict mode — debug build-də Android StrictMode-u aktivləşdirin. Threading policy pozuntularını (main thread-də şəbəkə, main thread-də disk I/O) aşkarlayır və istisna atır. Bu, potensial Heisenbug-u dərhal görünən deterministik Bohrbug-a çevirir.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Asinxronluğa diqqət yetirən code review — prosesin məcburi hissəsi. Hər pull request paylaşılan mutable state, thread-safe olmayan kolleksiyalar, sinxronizasiya olmaması üçün yoxlanmalıdır. Müəyyən nümunələri avtomatik qadağan etmək üçün lint qaydaları istifadə edin (məsələn, synchronized olmadan MutableList-ə giriş).
Tez-tez verilən suallar
Çünki standart metodlar — breakpointlər, loglar, print — icra mühitini o qədər dəyişir ki, səhv özünü göstərməyi dayandırır. Debugger bütün axınları onlarla millisaniyə dayandırır. Bu müddət ərzində səhvə səbəb olan race condition təbii şəkildə həll olunur. İcra timinginə təsir etməyən alətlər lazımdır.
Mandelbug şərtlərin mürəkkəbliyi səbəbindən çətin təkrarlanır, lakin debug alətləri onun özünü göstərməsinə təsir etmir. Heisenbug isə məhz debug alətlərindən yox olur. Mandelbug nümunəsi: yalnız Android 11, 3 GB RAM və batareya səviyyəsi 15%-dən aşağı olan cihazlarda çökmə. Heisenbug nümunəsi: Log.d() əlavə etdikdə yox olan race condition.
Flaky test detection işə salın — bəzən uğursuz, bəzən uğurlu olan testlər. Android-də test izolyasiyası üçün Android Test Orchestrator istifadə edin. Debug testlərinə StrictMode əlavə edin. Build-i ThreadSanitizer ilə instrument edin. Test >5% hallarda flaky-dirsə, onu potensial Heisenbug hesab edin və merge-dən əvvəl araşdırın.
Qismən. Kotlin-də Flow və structured concurrency paylaşılan mutable state miqdarını azaldır və axın idarəçiliyini sadələşdirir. Lakin korutinler thread təhlükəsizliyinə zəmanət vermir: iki korutin paylaşılan state-ə malikdirsə, race condition hələ də mümkündür. Ümumi state-i qorumaq üçün Mutex və ya korutinler arasında məlumat ötürmək üçün Channel istifadə edin.
Səhv zamanı avtomatik boşaltma ilə yaddaşda tsiklik log buferi istifadə edin. Xüsusi breadcrumbs ilə Crashlytics və ya Sentry vasitəsilə ətraflı monitorinq əlavə edin. Android üçün ANR detection aktivləşdirin və trace-lərə baxın. Səhv race condition-dırsa, istehsal yükünə yaxın debug build-də ThreadSanitizer problemi aşkarlaya bilər.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun