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
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.
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 — 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í.
| Typ | Reprodukovatelnost | Reakce na ladění | Příklad |
|---|---|---|---|
| Bohrbug | 100% | Nemění se | NPE při prázdném seznamu |
| Mandelbug | Chaotická | Nemění se | Pád na Android 12, Samsung, při nízké baterii |
| Heisenbug | Pouze bez ladění | Zmizí | Race condition mizející s logy |
| Schrödinbug | Neprojevuje se v kódu | Objeví se při pohledu | Chyba viditelná v kódu, ale nikdy se nespustí |
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.
// 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.
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.
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.
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ě.
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
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í.
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().
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.
Čá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.
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í
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í.
Přečtěte si také