Heisenbug: шта је, зашто настаје и методе хватања

Аутор: IT Sectr Објављено: 2026-07-29 Време читања: 10 мин

Heisenbug — баг који нестаје при покушају отклањања. Термин потиче од Хајзенберговог принципа неодређености: посматрање утиче на понашање система. У мобилном развоју, Heisenbug је један од најтежих проблема јер стандардне методе отклањања (логови, брејкпоинти, додатни код) мењају стање програма и скривају баг. Размотрићемо узроке настанка и методе борбе са неухватљивим грешкама.

Главно

  • Race condition — главни узрок Heisenbug-а: промена timing-а при отклањању маскира проблем
  • Bohrbug — предвидљиви баг, лако се репродукује за разлику од Heisenbug-а
  • Mandelbug — баг са сложеном узрочно-последичном везом, осетљив на почетне услове
  • ThreadSanitizer — алат за откривање трка података, не утиче на timing
  • Детерминистички тестови — једини поуздан начин репродукције Heisenbug-а

Шта је Heisenbug у мобилном развоју?

Heisenbug — класа грешака које се појављују у продукцији или при нормалном раду, али нестају при покушају репродукције у окружењу за отклањање. Термин је увео 1980-их програмер Џим Греј у контексту дистрибуираних система, али је данас најактуелнији за мобилне апликације због њихове асинхроне природе.

Главни узрок: стандардни алати за отклањање мењају окружење извршавања. Брејкпоинт зауставља нит на неколико милисекунди, логовање додаје синхрони I/O, додатне провере мењају редослед операција. У вишенитном окружењу чак и микросекундно кашњење може променити редослед извршавања нити и сакрити трку података.

Према подацима Microsoft Research-а (2022), око 15-25% свих багова у вишенитним мобилним апликацијама класификује се као Heisenbug. Време проналажења и исправљања једног Heisenbug-а је у просеку 5-10 пута веће него код обичног бага, због немогућности директне репродукције.

Пример Heisenbug-а

Апликација се руши у продукцији при брзом превлачењу листе, али када се повеже debugger или додају логови — ради савршено. Узрок: трка података између UI нити (ажурирање RecyclerView-а) и позадинске нити (ажурирање података адаптера). Логови додају кашњење које случајно синхронизује нити.

Bohrbug, Mandelbug, Heisenbug: класификација багова

Bohrbug — предвидљиви, стабилно репродуцибилни баг. Назван по аналогији са Боровим атомским моделом: као атом, баг се понаша исто при сваком посматрању. Пример: NullPointerException при клику на дугме пре учитавања података. Лечи се стандардним јединичним тестирањем.

Mandelbug — баг са сложеном, хаотичном узрочно-последичном везом (назван по аналогији са Манделбротовим скупом). Појављује се само при одређеној комбинацији услова: верзија ОС-а, модел уређаја, стање мреже, фаза месеца. Разликује се од Heisenbug-а по томе што не нестаје при отклањању — проблем је у сложености репродукције, а не у промени понашања услед алата.

Heisenbug — баг који нестаје управо због алата за отклањање. Ако додате лог — баг нестаје. Ако ставите брејкпоинт — баг се не појављује. Ако склоните све — баг се враћа. Главни узрок: измењени timing при отклањању.

ТипРепродуцибилностРеакција на отклањањеПример
Bohrbug100%Не мења сеNPE при празној листи
MandelbugХаотичнаНе мења сеКраш на Android 12, Samsung, при ниској батерији
HeisenbugСамо без отклањањаНестајеRace condition која нестаје са логовима
SchrödinbugНе појављује се у кодуПојављује се при погледуБаг видљив у коду, али се никад не активира

Главни узроци настанка Heisenbug-а

Race condition — број један међу узроцима Heisenbug-а. Две нити приступају заједничким подацима без синхронизације. Debugger уноси кашњење, захваљујући којем нити успевају да се природно синхронизују. Без debugger-а редослед извршавања је непредвидив.

Грешке зависне од timing-а — багови који се појављују само при одређеној брзини извршавања. На пример, анимација која треба да се заврши пре почетка следеће операције. У debugger-у анимација иде спорије и операција стиже да почне након завршетка анимације. У продукцији — обрнуто.

kotlin
// Пример 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-у компајлер мења редослед операција, што може открити скривене претпоставке у коду.

  • ThreadLocal — неправилно коришћење thread-local променљивих које нису видљиве другим нитима
  • Неиницијализоване променљиве — код који се ослања на подразумеване вредности поља класе
  • GCD/dispatch редови — у iOS-у неодређени редослед извршавања блокова у concurrent редовима
  • Баферовани I/O — подаци се не уписују на диск док се бафер не напуни

Стратегије хватања неухватљивих багова

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.

kotlin
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 или прилагођени циклични логер. Важно: логовање треба да буде асинхроно и минимално утиче на перформансе.

Превенција Heisenbug-а на нивоу архитектуре

Изолација стања — минимизирајте заједничко променљиво стање. Свака компонента треба да има своје изоловано стање, недоступно за директни упис из других компоненти. Користите Unidirectional Data Flow (UDF) — стање тече у једном смеру: Event → Reducer → State → UI.

Функционални приступ — чисте функције без споредних ефеката лакше се тестирају и отклањају. Споредне ефекте (мрежа, база, датотеке) изолујте у строго дефинисаним слојевима (repository, data source). Грешке везане за нити у функционалном коду су практично немогуће.

Strict mode — укључите Android StrictMode у debug build-у. Детектује кршење threading политике (мрежа на главној нити, диск I/O на главној нити) и баца изузетак. То претвара потенцијални Heisenbug у детерминистички Bohrbug, видљив одмах.

kotlin
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).

Често постављана питања

Зашто је Heisenbug тако тешко пронаћи?

Зато што стандардне методе — брејкпоинти, логови, print — толико мењају окружење извршавања да баг престаје да се манифестује. Debugger зауставља све нити на десетине милисекунди. За то време, race condition која је изазивала баг се природно разрешава. Потребни су алати који не утичу на execution timing.

По чему се Heisenbug разликује од Mandelbug-а?

Mandelbug је тешко репродуковати због сложености услова, али алати за отклањање не утичу на његово манифестовање. Heisenbug нестаје управо од алата за отклањање. Пример Mandelbug-а: краш само на уређајима са Android 11, 3 GB RAM и при нивоу батерије испод 15%. Пример Heisenbug-а: race condition која нестаје при додавању Log.d().

Како тестирати Heisenbug у CI/CD?

Покрените flaky test detection — тестове који понекад падају, понекад пролазе. У Android-у користите Android Test Orchestrator за изолацију тестова. Додајте StrictMode у debug тестове. Инструментирајте build са ThreadSanitizer-ом. Ако је тест flaky у >5% покретања — сматрајте га потенцијалним Heisenbug-ом и истражите пре merge-а.

Да ли Flow/Coroutines помажу у избегавању Heisenbug-а?

Делимично. Flow и structured concurrency у Kotlin-у смањују количину заједничког променљивог стања и поједностављују управљање нитима. Али корутине не гарантују нитну безбедност: ако две корутине деле стање, race condition је и даље могућа. Користите Mutex за заштиту заједничког стања или Channel за пренос података између корутина.

Шта радити ако се Heisenbug појављује само у продукцији?

Користите циклични бафер логова у меморији са аутоматским исписом при грешци. Додајте детаљни мониторинг путем Crashlytics-а или Sentry-а са прилагођеним breadcrumbs-има. За Android укључите ANR detection и прегледајте trace-ове. Ако је баг race condition, ThreadSanitizer у debug build-у са оптерећењем блиским продукцијском може открити проблем.

Резиме

  • Heisenbug — баг који нестаје при покушају отклањања; главни узрок — промена timing-а алатима програмера
  • Race condition — главни узрок Heisenbug-а у мобилним апликацијама, посебно у асинхроном коду
  • Bohrbug (100% репродуцибилан) и Mandelbug (хаотичан) — други типови багова, не мешати са Heisenbug-ом
  • ThreadSanitizer — најбољи алат за откривање трка података, не утиче на timing извршавања
  • Циклично логовање у меморији уместо дисковног — начин прикупљања података без маскирања Heisenbug-а
  • Unidirectional Data Flow и минимизација заједничког променљивог стања — архитектурална превенција целе класе грешака
  • StrictMode у debug build-у претвара потенцијални Heisenbug у детерминистички Bohrbug, видљив одмах

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође