Heisenbug: co to je, proč vzniká a metody odchytu

Autor: IT Sectr Publikováno: 2026-07-29 Doba čtení: 10 min

Heisenbug — chyba, která zmizí při pokusu o její odladění. Termín pochází z Heisenbergova principu neurčitosti: pozorování ovlivňuje chování systému. V mobilním vývoji je Heisenbug jedním z nejobtížnějších problémů, protože standardní metody ladění (logy, breakpointy, dodatečný kód) mění stav programu a skrývají chybu. Rozebereme příčiny vzniku a metody boje s nepolapitelnými chybami.

Hlavní body

  • Race condition — hlavní příčina Heisenbug: změna načasování při ladění maskuje problém
  • Bohrbug — předvídatelná chyba, snadno reprodukovatelná na rozdíl od Heisenbug
  • Mandelbug — chyba se složitým vztahem příčiny a následku, citlivá na počáteční podmínky
  • ThreadSanitizer — nástroj pro detekci datových závodů, neovlivňující načasování
  • Deterministické testy — jediný spolehlivý způsob reprodukce Heisenbug

Co je Heisenbug v mobilním vývoji?

Heisenbug — třída chyb, které se objevují v produkčním prostředí nebo při běžném provozu, ale mizí při pokusu o reprodukci v ladícím prostředí. Termín zavedl v 80. letech programátor Jim Gray v kontextu distribuovaných systémů, ale dnes je nejaktuálnější pro mobilní aplikace kvůli jejich asynchronní povaze.

Hlavní příčina: standardní ladící nástroje mění prostředí běhu. Breakpoint zastaví vlákno na několik milisekund, logování přidává synchronní I/O, dodatečné kontroly mění pořadí operací. Ve vícevláknovém prostředí může i mikrosekundové zpoždění změnit pořadí provádění vláken a skrýt datový závod.

Podle údajů Microsoft Research (2022) je přibližně 15-25% všech chyb ve vícevláknových mobilních aplikacích klasifikováno jako Heisenbug. Čas na nalezení a opravu jednoho Heisenbug je v průměru 5-10krát delší než u běžné chyby, kvůli nemožnosti přímé reprodukce.

Příklad Heisenbug

Aplikace padá v produkci při rychlém posouvání seznamu, ale po připojení debuggeru nebo přidání logů — funguje perfektně. Příčina: datový závod mezi UI vláknem (aktualizace RecyclerView) a vláknem na pozadí (aktualizace dat adaptéru). Logy přidávají zpoždění, které náhodně synchronizuje vlákna.

Bohrbug, Mandelbug, Heisenbug: klasifikace chyb

Bohrbug — předvídatelná, stabilně reprodukovatelná chyba. Pojmenována analogicky k Bohrovu atomovému modelu: jako atom se chyba chová stejně při každém pozorování. Příklad: NullPointerException při kliknutí na tlačítko před načtením dat. Léčí se standardním unit testováním.

Mandelbug — chyba se složitým, chaotickým vztahem příčiny a následku (pojmenována analogicky k Mandelbrotově množině). Projevuje se pouze při určité kombinaci podmínek: verze OS, model zařízení, stav sítě, fáze měsíce. Liší se od Heisenbug tím, že nezmizí při ladění — problém je v obtížnosti reprodukce, nikoli ve změně chování způsobené nástroji.

Heisenbug — chyba, která mizí právě kvůli ladícím nástrojům. Pokud přidáte log — chyba zmizí. Pokud dáte breakpoint — chyba se neprojeví. Pokud vše odstraníte — chyba se vrátí. Hlavní příčina: změněné načasování při ladění.

TypReprodukovatelnostReakce na laděníPříklad
Bohrbug100%Nemění seNPE při prázdném seznamu
MandelbugChaotickáNemění sePád na Android 12, Samsung, při nízké baterii
HeisenbugPouze bez laděníZmizíRace condition mizející s logy
SchrödinbugNeprojevuje se v kóduObjeví se při pohleduChyba viditelná v kódu, ale nikdy se nespustí

Hlavní příčiny vzniku Heisenbug

Race condition — číslo jedna mezi příčinami Heisenbug. Dvě vlákna přistupují ke společným datům bez synchronizace. Debugger vnáší zpoždění, díky kterému vlákna stihnou přirozeně synchronizovat. Bez debuggeru je pořadí provádění nepředvídatelné.

Chyby závislé na načasování — chyby projevující se pouze při určité rychlosti provádění. Například animace, která by měla skončit před zahájením následující operace. V debuggeru animace běží pomaleji a operace stihne začít až po dokončení animace. V produkci — naopak.

kotlin
// Příklad race condition — typický Heisenbug
class ListViewModel : ViewModel() {
    private var items = mutableListOf<String>()

    fun loadFromNetwork() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = api.fetchItems()
            items.addAll(result) // ❌ Není thread-safe
        }
    }

    fun getItems(): List<String> = items.toList()
    // Race condition: čtení getItems se může překrývat se zápisem loadFromNetwork
}

Optimalizace kompilátoru — kompilátor (JIT, ART, Kotlin/Native) může přeskupit instrukce za účelem optimalizace. V debug sestavení jsou optimalizace vypnuty a kód se provádí „jak je napsán“. V release sestavení kompilátor mění pořadí operací, což může odhalit skryté předpoklady v kódu.

  • ThreadLocal — nesprávné použití thread-local proměnných, které nejsou viditelné pro jiná vlákna
  • Neinicializované proměnné — kód spoléhající na výchozí hodnoty polí třídy
  • Fronty GCD/dispatch — v iOS neurčité pořadí provádění bloků v concurrent frontách
  • Bufferované I/O — data se nezapisují na disk, dokud není buffer plný

Strategie odchytu nepolapitelných chyb

ThreadSanitizer (TSan) — nástroj Google pro detekci datových závodů v C/C++ a Kotlin/Native. Vestaví se do sestavení a detekuje každý přístup ke sdílené paměti bez synchronizace. Na rozdíl od logů TSan neovlivňuje načasování, protože funguje přes instrumented code, nikoli přes I/O.

Deterministické testy — nahraďte skutečnou asynchronnost řízenou. Používejte TestDispatcher (Kotlin), RxJava Plugins nebo GCD test fronty (iOS) pro úplnou kontrolu nad pořadím provádění. Nastavte konkrétní scénáře: vlákno A se provede, pak B, pak A znovu.

Cyklické logování — logování do cyklického bufferu v paměti (ne na disk). Když dojde k chybě, buffer se uloží do souboru. Protože zápis do paměti trvá nanosekundy (namísto milisekund pro diskové I/O), takový log neovlivňuje načasování a nemaskuje 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) } }
    }
}

Logování v produkci — pokud se chyba nereprodukuje lokálně, sbírejte data v produkci. Používejte Firebase Crashlytics logs, Sentry Breadcrumbs nebo vlastní cyklický logger. Důležité: logování by mělo být asynchronní a minimálně ovlivňovat výkon.

Prevence Heisenbug na úrovni architektury

Izolace stavu — minimalizujte sdílený měnitelný stav. Každá komponenta by měla mít svůj izolovaný stav, nepřístupný pro přímý zápis z jiných komponent. Používejte Unidirectional Data Flow (UDF) — stav teče jedním směrem: Event → Reducer → State → UI.

Funkcionální přístup — čisté funkce bez vedlejších účinků se snadněji testují a ladí. Vedlejší účinky (síť, databáze, soubory) izolujte v přísně definovaných vrstvách (repository, data source). Chyby související s vlákny ve funkcionálním kódu jsou prakticky nemožné.

Strict mode — zapněte Android StrictMode v debug sestavení. Detekuje porušení politiky vláken (síť na hlavním vlákně, diskové I/O na hlavním vlákně) a vyhazuje výjimku. Tím se potenciální Heisenbug mění v deterministický Bohrbug, viditelný okamžitě.

kotlin
class DebugApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
        }
    }
}

Code review se zaměřením na asynchronnost — povinná součást procesu. Každý pull request by měl být zkontrolován na sdílený měnitelný stav, nevláknově bezpečné kolekce, chybějící synchronizaci. Používejte lint pravidla pro automatický zákaz určitých vzorů (např. přístup k MutableList bez synchronized).

Často kladené otázky

Proč je Heisenbug tak těžké najít?

Protože standardní metody — breakpointy, logy, print — natolik mění prostředí běhu, že chyba přestane projevovat. Debugger zastavuje všechna vlákna na desítky milisekund. Během této doby se race condition, která způsobovala chybu, přirozeně vyřeší. Jsou potřeba nástroje, které neovlivňují načasování provádění.

Čím se Heisenbug liší od Mandelbug?

Mandelbug je obtížně reprodukovatelný kvůli složitosti podmínek, ale ladící nástroje neovlivňují jeho projev. Heisenbug mizí právě kvůli ladícím nástrojům. Příklad Mandelbug: pád pouze na zařízeních s Android 11, 3 GB RAM a při úrovni baterie pod 15%. Příklad Heisenbug: race condition mizející při přidání Log.d().

Jak testovat Heisenbug v CI/CD?

Spouštějte flaky test detection — testy, které někdy padají, někdy procházejí. V Androidu používejte Android Test Orchestrator pro izolaci testů. Přidejte StrictMode do debug testů. Instrumentujte sestavení s ThreadSanitizer. Pokud je test flaky v >5% běhů — považujte ho za potenciální Heisenbug a prošetřete před mergem.

Pomáhá Flow/Coroutines vyhnout se Heisenbug?

Částečně. Flow a structured concurrency v Kotlinu snižují množství sdíleného měnitelného stavu a zjednodušují správu vláken. Ale coroutiny nezaručují bezpečnost vláken: pokud dvě coroutiny sdílejí stav, race condition je stále možná. Používejte Mutex pro ochranu společného stavu nebo Channel pro přenos dat mezi coroutinami.

Co dělat, pokud se Heisenbug projevuje pouze v produkci?

Používejte cyklický buffer logů v paměti s automatickým výpisem při chybě. Přidejte podrobný monitoring přes Crashlytics nebo Sentry s vlastními breadcrumbs. Pro Android zapněte ANR detection a prohlížejte trace. Pokud je chyba race condition, ThreadSanitizer v debug sestavení se zátěží blízkou produkci může odhalit problém.

Shrnutí

  • Heisenbug — chyba mizející při pokusu o ladění; hlavní příčina — změna načasování nástroji vývojáře
  • Race condition — hlavní příčina Heisenbug v mobilních aplikacích, zejména v asynchronním kódu
  • Bohrbug (100% reprodukovatelný) a Mandelbug (chaotický) — jiné typy chyb, nezaměňovat s Heisenbug
  • ThreadSanitizer — nejlepší nástroj pro detekci datových závodů, neovlivňující načasování provádění
  • Cyklické logování v paměti místo disku — způsob sběru dat bez maskování Heisenbug
  • Unidirectional Data Flow a minimalizace sdíleného měnitelného stavu — architektonická prevence celé třídy chyb
  • StrictMode v debug sestavení mění potenciální Heisenbug na deterministický Bohrbug, viditelný okamžitě

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také