Heisenbug — баг који нестаје при покушају отклањања. Термин потиче од Хајзенберговог принципа неодређености: посматрање утиче на понашање система. У мобилном развоју, Heisenbug је један од најтежих проблема јер стандардне методе отклањања (логови, брејкпоинти, додатни код) мењају стање програма и скривају баг. Размотрићемо узроке настанка и методе борбе са неухватљивим грешкама.
Главно
Heisenbug — класа грешака које се појављују у продукцији или при нормалном раду, али нестају при покушају репродукције у окружењу за отклањање. Термин је увео 1980-их програмер Џим Греј у контексту дистрибуираних система, али је данас најактуелнији за мобилне апликације због њихове асинхроне природе.
Главни узрок: стандардни алати за отклањање мењају окружење извршавања. Брејкпоинт зауставља нит на неколико милисекунди, логовање додаје синхрони I/O, додатне провере мењају редослед операција. У вишенитном окружењу чак и микросекундно кашњење може променити редослед извршавања нити и сакрити трку података.
Према подацима Microsoft Research-а (2022), око 15-25% свих багова у вишенитним мобилним апликацијама класификује се као Heisenbug. Време проналажења и исправљања једног Heisenbug-а је у просеку 5-10 пута веће него код обичног бага, због немогућности директне репродукције.
Апликација се руши у продукцији при брзом превлачењу листе, али када се повеже debugger или додају логови — ради савршено. Узрок: трка података између UI нити (ажурирање RecyclerView-а) и позадинске нити (ажурирање података адаптера). Логови додају кашњење које случајно синхронизује нити.
Bohrbug — предвидљиви, стабилно репродуцибилни баг. Назван по аналогији са Боровим атомским моделом: као атом, баг се понаша исто при сваком посматрању. Пример: NullPointerException при клику на дугме пре учитавања података. Лечи се стандардним јединичним тестирањем.
Mandelbug — баг са сложеном, хаотичном узрочно-последичном везом (назван по аналогији са Манделбротовим скупом). Појављује се само при одређеној комбинацији услова: верзија ОС-а, модел уређаја, стање мреже, фаза месеца. Разликује се од Heisenbug-а по томе што не нестаје при отклањању — проблем је у сложености репродукције, а не у промени понашања услед алата.
Heisenbug — баг који нестаје управо због алата за отклањање. Ако додате лог — баг нестаје. Ако ставите брејкпоинт — баг се не појављује. Ако склоните све — баг се враћа. Главни узрок: измењени timing при отклањању.
| Тип | Репродуцибилност | Реакција на отклањање | Пример |
|---|---|---|---|
| Bohrbug | 100% | Не мења се | NPE при празној листи |
| Mandelbug | Хаотична | Не мења се | Краш на Android 12, Samsung, при ниској батерији |
| Heisenbug | Само без отклањања | Нестаје | Race condition која нестаје са логовима |
| Schrödinbug | Не појављује се у коду | Појављује се при погледу | Баг видљив у коду, али се никад не активира |
Race condition — број један међу узроцима Heisenbug-а. Две нити приступају заједничким подацима без синхронизације. Debugger уноси кашњење, захваљујући којем нити успевају да се природно синхронизују. Без debugger-а редослед извршавања је непредвидив.
Грешке зависне од timing-а — багови који се појављују само при одређеној брзини извршавања. На пример, анимација која треба да се заврши пре почетка следеће операције. У debugger-у анимација иде спорије и операција стиже да почне након завршетка анимације. У продукцији — обрнуто.
// Пример race condition — типични Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Није thread-safe
}
}
fun getItems(): List<String> = items.toList()
// Race condition: читање getItems може да се преклапа са уписом loadFromNetwork
}
Оптимизација компајлера — компајлер (JIT, ART, Kotlin/Native) може преуредити инструкције ради оптимизације. У debug build-у оптимизације су искључене и код се извршава „како је написан“. У release build-у компајлер мења редослед операција, што може открити скривене претпоставке у коду.
ThreadSanitizer (TSan) — Google-ов алат за откривање трка података у C/C++ и Kotlin/Native. Уграђује се у build и детектује сваки приступ заједничкој меморији без синхронизације. За разлику од логова, TSan не утиче на timing, јер ради кроз instrumented code, а не кроз I/O.
Детерминистички тестови — замените стварну асинхроност контролисаном. Користите TestDispatcher (Kotlin), RxJava Plugins или GCD test queues (iOS) за потпуну контролу над редоследом извршавања. Поставите конкретне сценарије: нит А се извршава, затим Б, па А поново.
Циклично логовање — логовање у циклични бафер у меморији (не на диск). Када се баг догоди, бафер се чува у датотеку. Пошто упис у меморију траје наносекунде (уместо милисекунди за диск I/O), такав лог не утиче на timing и не маскира Heisenbug.
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) } }
}
}
Логовање у продукцији — ако се баг не репродукује локално, прикупљајте податке у продукцији. Користите Firebase Crashlytics logs, Sentry Breadcrumbs или прилагођени циклични логер. Важно: логовање треба да буде асинхроно и минимално утиче на перформансе.
Изолација стања — минимизирајте заједничко променљиво стање. Свака компонента треба да има своје изоловано стање, недоступно за директни упис из других компоненти. Користите Unidirectional Data Flow (UDF) — стање тече у једном смеру: Event → Reducer → State → UI.
Функционални приступ — чисте функције без споредних ефеката лакше се тестирају и отклањају. Споредне ефекте (мрежа, база, датотеке) изолујте у строго дефинисаним слојевима (repository, data source). Грешке везане за нити у функционалном коду су практично немогуће.
Strict mode — укључите Android StrictMode у debug build-у. Детектује кршење threading политике (мрежа на главној нити, диск I/O на главној нити) и баца изузетак. То претвара потенцијални Heisenbug у детерминистички Bohrbug, видљив одмах.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Code review са фокусом на асинхроност — обавезни део процеса. Сваки pull request треба проверити на заједничко променљиво стање, нитнобезбедне колекције, недостатак синхронизације. Користите lint правила за аутоматску забрану одређених образаца (нпр. приступ MutableList-у без synchronized).
Често постављана питања
Зато што стандардне методе — брејкпоинти, логови, print — толико мењају окружење извршавања да баг престаје да се манифестује. Debugger зауставља све нити на десетине милисекунди. За то време, race condition која је изазивала баг се природно разрешава. Потребни су алати који не утичу на execution timing.
Mandelbug је тешко репродуковати због сложености услова, али алати за отклањање не утичу на његово манифестовање. Heisenbug нестаје управо од алата за отклањање. Пример Mandelbug-а: краш само на уређајима са Android 11, 3 GB RAM и при нивоу батерије испод 15%. Пример Heisenbug-а: race condition која нестаје при додавању Log.d().
Покрените flaky test detection — тестове који понекад падају, понекад пролазе. У Android-у користите Android Test Orchestrator за изолацију тестова. Додајте StrictMode у debug тестове. Инструментирајте build са ThreadSanitizer-ом. Ако је тест flaky у >5% покретања — сматрајте га потенцијалним Heisenbug-ом и истражите пре merge-а.
Делимично. Flow и structured concurrency у Kotlin-у смањују количину заједничког променљивог стања и поједностављују управљање нитима. Али корутине не гарантују нитну безбедност: ако две корутине деле стање, race condition је и даље могућа. Користите Mutex за заштиту заједничког стања или Channel за пренос података између корутина.
Користите циклични бафер логова у меморији са аутоматским исписом при грешци. Додајте детаљни мониторинг путем Crashlytics-а или Sentry-а са прилагођеним breadcrumbs-има. За Android укључите ANR detection и прегледајте trace-ове. Ако је баг race condition, ThreadSanitizer у debug build-у са оптерећењем блиским продукцијском може открити проблем.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође