Heisenbug — en bugg som försvinner när man försöker debugga den. Termen kommer från Heisenbergs osäkerhetsprincip: observation påverkar systemets beteende. Inom mobilutveckling är Heisenbug ett av de svåraste problemen eftersom standard debugmetoder (loggar, breakpoints, extra kod) ändrar programmets tillstånd och döljer buggen. Vi analyserar orsakerna och metoderna att bekämpa svårfångade fel.
Huvudpunkter
Heisenbug — en klass av fel som uppträder i produktionsmiljön eller vid normal drift, men försvinner vid försök att reproducera i debuggmiljö. Termen introducerades på 1980-talet av programmeraren Jim Gray i samband med distribuerade system, men är idag mest relevant för mobilappar på grund av deras asynkrona natur.
Huvudorsak: standard debugverktyg ändrar exekveringsmiljön. Breakpoint stoppar tråden i några millisekunder, loggning lägger till synkron I/O, extra kontroller ändrar ordningen på operationer. I en flertrådad miljö kan även en mikrosekunds fördröjning ändra exekveringsordningen för trådar och dölja en datarace.
Enligt Microsoft Research (2022) klassificeras cirka 15-25% av alla buggar i flertrådade mobilappar som Heisenbug. Tiden att hitta och åtgärda en Heisenbug är i genomsnitt 5-10 gånger längre än för en vanlig bugg, på grund av omöjligheten att direkt reproducera.
Appen kraschar i produktion vid snabb svepning i listan, men när debugger ansluts eller loggar läggs till fungerar den perfekt. Orsak: datarace mellan UI-tråden (uppdatering av RecyclerView) och bakgrundstråden (uppdatering av adapterdata). Loggar lägger till en fördröjning som slumpmässigt synkroniserar trådarna.
Bohrbug — en förutsägbar, stabilt reproducerbar bugg. Uppkallad efter Bohrs atommodell: som en atom beter sig buggen likadant vid varje observation. Exempel: NullPointerException vid klick på en knapp innan data har laddats. Behandlas med standard enhetstestning.
Mandelbug — en bugg med komplex, kaotisk orsak-verkan-relation (uppkallad efter Mandelbrotmängden). Visar sig endast vid en specifik kombination av förhållanden: OS-version, enhetsmodell, nätverksstatus, månfas. Skiljer sig från Heisenbug genom att den inte försvinner vid debuggen — problemet ligger i svårigheten att reproducera, inte i beteendeförändring orsakad av verktyg.
Heisenbug — en bugg som försvinner just på grund av debugverktyg. Om du lägger till en logg — försvinner buggen. Om du sätter en breakpoint — visas inte buggen. Om du tar bort allt — kommer buggen tillbaka. Huvudorsak: ändrad timing vid debuggen.
| Typ | Reproducerbarhet | Reaktion på debuggen | Exempel |
|---|---|---|---|
| Bohrbug | 100% | Förändras inte | NPE vid tom lista |
| Mandelbug | Kaotisk | Förändras inte | Krasch på Android 12, Samsung, lågt batteri |
| Heisenbug | Endast utan debuggen | Försvinner | Race condition som försvinner med loggar |
| Schrödinbug | Visas inte i koden | Visas vid anblick | Bugg synlig i koden men utlöses aldrig |
Race condition — nummer ett bland orsakerna till Heisenbug. Två trådar kommer åt gemensamma data utan synkronisering. Debugger inför en fördröjning som gör att trådarna hinner synkroniseras naturligt. Utan debugger är exekveringsordningen oförutsägbar.
Timing-beroende fel — buggar som endast visar sig vid en viss exekveringshastighet. Till exempel en animation som måste slutföras innan nästa operation påbörjas. I debugger går animationen långsammare och operationen hinner starta efter animationens slut. I produktion — tvärtom.
// Exempel på race condition — typisk Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Inte trådsäker
}
}
fun getItems(): List<String> = items.toList()
// Race condition: getItems läsning kan överlappa med loadFromNetwork skrivning
}
Kompilatoroptimering — kompilatorn (JIT, ART, Kotlin/Native) kan omordna instruktioner för optimering. I debug build är optimeringar avaktiverade och koden körs “som den är skriven”. I release build ändrar kompilatorn ordningen på operationer, vilket kan avslöja dolda antaganden i koden.
ThreadSanitizer (TSan) — Googles verktyg för att upptäcka dataraces i C/C++ och Kotlin/Native. Bäddas in i builden och upptäcker varje åtkomst till delat minne utan synkronisering. Till skillnad från loggar påverkar TSan inte timingen, eftersom det fungerar genom instrumented code, inte via I/O.
Deterministiska tester — ersätt verklig asynkronicitet med kontrollerad. Använd TestDispatcher (Kotlin), RxJava Plugins eller GCD test queues (iOS) för full kontroll över exekveringsordningen. Ställ in specifika scenarier: tråd A körs, sedan B, sedan A igen.
Cyklick loggning — loggning till en cyklisk buffert i minnet (inte på disk). När buggen inträffar sparas bufferten till en fil. Eftersom skrivning till minnet tar nanosekunder (istället för millisekunder för disk I/O) påverkar en sådan logg inte timingen och maskerar inte 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) } }
}
}
Loggning i produktion — om buggen inte reproduceras lokalt, samla data i produktion. Använd Firebase Crashlytics logs, Sentry Breadcrumbs eller en anpassad cyklisk loggare. Viktigt: loggning bör vara asynkron och ha minimal inverkan på prestanda.
Isolering av tillstånd — minimera delat muterbart tillstånd. Varje komponent bör ha sitt eget isolerade tillstånd, otillgängligt för direkt skrivning från andra komponenter. Använd Unidirectional Data Flow (UDF) — tillstånd flödar i en riktning: Event → Reducer → State → UI.
Funktionell ansats — rena funktioner utan sidoeffekter är lättare att testa och debugga. Isolera sidoeffekter (nätverk, databas, filer) i strängt definierade lager (repository, data source). Trådrelaterade fel i funktionell kod är praktiskt taget omöjliga.
Strict mode — aktivera Android StrictMode i debug build. Den upptäcker överträdelser av trådningspolicy (nätverk på huvudtråden, disk I/O på huvudtråden) och kastar ett undantag. Detta förvandlar en potentiell Heisenbug till en deterministisk Bohrbug, omedelbart synlig.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Code review med fokus på asynkronicitet — obligatorisk del av processen. Varje pull request bör kontrolleras för delat muterbart tillstånd, icke-trådsäkra samlingar, brist på synkronisering. Använd lint-regler för automatiskt förbud mot vissa mönster (t.ex. åtkomst till MutableList utan synchronized).
Vanliga frågor
För att standardmetoder — breakpoints, loggar, print — förändrar exekveringsmiljön så mycket att buggen slutar visa sig. Debugger stoppar alla trådar i tiotals millisekunder. Under den tiden löser sig dataracen som orsakade buggen naturligt. Verktyg som inte påverkar execution timing behövs.
Mandelbug är svår att reproducera på grund av komplexa förhållanden, men debugverktyg påverkar inte dess uppträdande. Heisenbug försvinner just på grund av debugverktyg. Exempel på Mandelbug: krasch endast på enheter med Android 11, 3 GB RAM och batterinivå under 15%. Exempel på Heisenbug: datarace som försvinner vid tillägg av Log.d().
Kör flaky test detection — tester som ibland misslyckas, ibland lyckas. I Android använd Android Test Orchestrator för testisolering. Lägg till StrictMode i debug-tester. Instrumentera builden med ThreadSanitizer. Om ett test är flaky i >5% av körningarna — betrakta det som en potentiell Heisenbug och undersök före merge.
Delvis. Flow och structured concurrency i Kotlin minskar mängden delat muterbart tillstånd och förenklar trådhantering. Men coroutines garanterar inte trådsäkerhet: om två coroutines har delat tillstånd är datarace fortfarande möjlig. Använd Mutex för att skydda gemensamt tillstånd eller Channel för dataöverföring mellan coroutines.
Använd en cyklisk loggbuffert i minnet med automatisk dumpning vid fel. Lägg till detaljerad övervakning via Crashlytics eller Sentry med anpassade breadcrumbs. För Android, aktivera ANR detection och granska traces. Om buggen är en datarace kan ThreadSanitizer i debug build med produktionsnära belastning avslöja problemet.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också