Heisenbug — isang bug na nawawala kapag sinubukan mong i-debug ito. Ang termino ay nagmula sa prinsipyo ng kawalan ng katiyakan ni Heisenberg: ang pagmamasid ay nakakaapekto sa pag-uugali ng sistema. Sa mobile development, ang Heisenbug ay isa sa mga pinakamahirap na problema dahil ang mga standard na paraan ng debug (logs, breakpoints, karagdagang code) ay nagbabago sa estado ng programa at nagtatago ng bug. Talakayin natin ang mga sanhi at paraan ng paglaban sa mga mailap na error.
Mga pangunahing punto
Heisenbug — isang klase ng mga error na lumilitaw sa production o sa normal na operasyon, ngunit nawawala kapag sinubukang i-reproduce sa debug environment. Ang termino ay ipinakilala noong 1980s ng programmer na Jim Gray sa konteksto ng mga distributed system, ngunit pinaka-kaugnay ngayon para sa mga mobile app dahil sa kanilang asynchronous na kalikasan.
Pangunahing sanhi: ang mga standard na debug tools ay nagbabago sa execution environment. Ang Breakpoint ay humihinto sa thread sa loob ng ilang millisecond, ang pagla-log ay nagdadagdag ng synchronous I/O, ang karagdagang pagsusuri ay nagbabago sa pagkakasunud-sunod ng mga operasyon. Sa multi-threaded na kapaligiran, kahit isang microsecond na pagkaantala ay maaaring magbago ng pagkakasunud-sunod ng pagpapatupad ng mga thread at magtago ng data race.
Ayon sa datos ng Microsoft Research (2022), humigit-kumulang 15-25% ng lahat ng bug sa multi-threaded mobile app ay nauuri bilang Heisenbug. Ang oras upang mahanap at ayusin ang isang Heisenbug ay nasa average na 5-10 beses na mas mahaba kaysa sa isang ordinaryong bug, dahil sa imposibilidad ng direktang pag-reproduce.
Ang app ay nag-crash sa production kapag mabilis na nag-swipe sa listahan, ngunit kapag nakakonekta ang debugger o idinagdag ang mga log — gumagana nang perpekto. Sanhi: data race sa pagitan ng UI thread (pag-update ng RecyclerView) at background thread (pag-update ng data ng adapter). Ang mga log ay nagdaragdag ng pagkaantala na random na nag-si-synchronize ng mga thread.
Bohrbug — isang predictable, stable na reproducible na bug. Pinangalanan sa pamamagitan ng pagkakatulad sa atomic model ni Bohr: tulad ng isang atom, ang bug ay kumikilos nang pareho sa bawat obserbasyon. Halimbawa: NullPointerException kapag nag-click sa isang button bago mag-load ng data. Ginagamot sa pamamagitan ng standard unit testing.
Mandelbug — isang bug na may komplikado, magulong ugnayang sanhi-at-bunga (pinangalanan sa pamamagitan ng pagkakatulad sa Mandelbrot set). Lumilitaw lamang sa isang partikular na kombinasyon ng mga kondisyon: bersyon ng OS, modelo ng device, estado ng network, yugto ng buwan. Naiiba sa Heisenbug dahil hindi ito nawawala kapag nagde-debug — ang problema ay nasa kahirapan ng pag-reproduce, hindi sa pagbabago ng pag-uugali dulot ng mga tool.
Heisenbug — isang bug na nawawala mismo dahil sa mga debug tool. Kung magdagdag ka ng log — nawawala ang bug. Kung maglagay ka ng breakpoint — hindi lumalabas ang bug. Kung tanggalin mo lahat — babalik ang bug. Pangunahing sanhi: binagong timing habang nagde-debug.
| Uri | Reproducibility | Reaksyon sa debug | Halimbawa |
|---|---|---|---|
| Bohrbug | 100% | Hindi nagbabago | NPE sa walang laman na listahan |
| Mandelbug | Magulo | Hindi nagbabago | Crash sa Android 12, Samsung, mababang baterya |
| Heisenbug | Walang debug lang | Nawawala | Race condition na nawawala sa mga log |
| Schrödinbug | Hindi lumilitaw sa code | Lumilitaw kapag tiningnan | Bug nakikita sa code ngunit hindi kailanman nangyayari |
Race condition — numero uno sa mga sanhi ng Heisenbug. Dalawang thread ang uma-access sa shared data nang walang synchronization. Ang debugger ay nagpapakilala ng pagkaantala, na nagpapahintulot sa mga thread na natural na mag-synchronize. Kung walang debugger, ang pagkakasunud-sunod ng pagpapatupad ay hindi mahuhulaan.
Mga error na nakadepende sa timing — mga bug na lumilitaw lamang sa isang partikular na bilis ng pagpapatupad. Halimbawa, isang animation na dapat matapos bago magsimula ang susunod na operasyon. Sa debugger, mas mabagal ang animation at ang operasyon ay nagsisimula pagkatapos ng animation. Sa production — kabaligtaran.
// Halimbawa ng race condition — tipikal na Heisenbug
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ Hindi thread-safe
}
}
fun getItems(): List<String> = items.toList()
// Race condition: ang pagbasa ng getItems ay maaaring mag-overlap sa pagsulat ng loadFromNetwork
}
Optimization ng compiler — ang compiler (JIT, ART, Kotlin/Native) ay maaaring mag-reorder ng mga instruction para sa optimization. Sa debug build, ang mga optimization ay naka-disable at ang code ay isinasagawa “gaya ng pagkakasulat”. Sa release build, binabago ng compiler ang pagkakasunud-sunod ng mga operasyon, na maaaring magbunyag ng mga nakatagong assumption sa code.
ThreadSanitizer (TSan) — tool ng Google para sa pag-detect ng data race sa C/C++ at Kotlin/Native. Naka-embed sa build at nagde-detect ng bawat access sa shared memory nang walang synchronization. Hindi tulad ng mga log, hindi naaapektuhan ng TSan ang timing, dahil gumagana ito sa pamamagitan ng instrumented code, hindi sa pamamagitan ng I/O.
Deterministic na mga test — palitan ang tunay na asynchrony ng kontrolado. Gamitin ang TestDispatcher (Kotlin), RxJava Plugins o GCD test queues (iOS) para sa buong kontrol sa pagkakasunud-sunod ng pagpapatupad. Magtakda ng mga partikular na senaryo: ang thread A ay isinasagawa, pagkatapos B, pagkatapos A muli.
Cyclic logging — pagla-log sa cyclic buffer sa memory (hindi sa disk). Kapag nangyari ang bug, ang buffer ay nai-save sa isang file. Dahil ang pagsulat sa memory ay tumatagal ng nanosecond (sa halip na millisecond para sa disk I/O), ang naturang log ay hindi nakakaapekto sa timing at hindi nagtatago ng 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) } }
}
}
Pagla-log sa production — kung ang bug ay hindi ma-reproduce nang lokal, mangolekta ng data sa production. Gamitin ang Firebase Crashlytics logs, Sentry Breadcrumbs o custom na cyclic logger. Mahalaga: ang pagla-log ay dapat asynchronous at may minimal na epekto sa performance.
Pag-iisolate ng estado — bawasan ang shared mutable state. Ang bawat component ay dapat may sariling isolated na estado, hindi ma-access para sa direktang pagsulat mula sa ibang component. Gamitin ang Unidirectional Data Flow (UDF) — ang estado ay dumadaloy sa isang direksyon: Event → Reducer → State → UI.
Functional na approach — ang mga pure function na walang side effect ay mas madaling i-test at i-debug. I-isolate ang mga side effect (network, database, file) sa mahigpit na tinukoy na mga layer (repository, data source). Ang mga error na nauugnay sa thread sa functional code ay praktikal na imposible.
Strict mode — i-activate ang Android StrictMode sa debug build. Nakikita nito ang mga paglabag sa threading policy (network sa main thread, disk I/O sa main thread) at nagtatapon ng exception. Ito ay nagpapalit ng potensyal na Heisenbug sa deterministic na Bohrbug na agad nakikita.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
Code review na may focus sa asynchrony — mandatoryong bahagi ng proseso. Ang bawat pull request ay dapat suriin para sa shared mutable state, hindi thread-safe na koleksyon, kawalan ng synchronization. Gamitin ang lint rules para sa awtomatikong pagbabawal ng ilang pattern (hal., pag-access sa MutableList nang walang synchronized).
Mga madalas itanong
Dahil ang mga standard na paraan — breakpoints, log, print — ay nagbabago sa execution environment nang labis na ang bug ay tumitigil sa pagpapakita. Ang debugger ay humihinto sa lahat ng thread sa loob ng sampu-sampung millisecond. Sa oras na ito, ang race condition na nagdulot ng bug ay natural na nareresolba. Kailangan ang mga tool na hindi nakakaapekto sa execution timing.
Mandelbug ay mahirap i-reproduce dahil sa pagiging kumplikado ng mga kondisyon, ngunit ang mga debug tool ay hindi nakakaapekto sa pagpapakita nito. Heisenbug ay nawawala mismo dahil sa debug tool. Halimbawa ng Mandelbug: crash lamang sa mga device na may Android 11, 3 GB RAM at antas ng baterya sa ibaba 15%. Halimbawa ng Heisenbug: race condition na nawawala kapag nagdagdag ng Log.d().
Patakbuhin ang flaky test detection — mga test na minsan pumasa, minsan nabibigo. Sa Android, gamitin ang Android Test Orchestrator para sa pag-iisolate ng test. Magdagdag ng StrictMode sa debug tests. I-instrument ang build gamit ang ThreadSanitizer. Kung ang test ay flaky sa >5% ng mga run — ituring ito bilang potensyal na Heisenbug at imbestigahan bago ang merge.
Bahagya. Ang Flow at structured concurrency sa Kotlin ay nagbabawas ng dami ng shared mutable state at nagpapasimple ng pamamahala ng thread. Ngunit ang coroutines ay hindi garantiya ng thread safety: kung ang dalawang coroutine ay may shared state, ang race condition ay posible pa rin. Gamitin ang Mutex para protektahan ang shared state o Channel para sa paglipat ng data sa pagitan ng coroutines.
Gumamit ng cyclic log buffer sa memory na may awtomatikong pag-dump kapag may error. Magdagdag ng detalyadong monitoring sa pamamagitan ng Crashlytics o Sentry na may custom na breadcrumbs. Para sa Android, i-activate ang ANR detection at suriin ang mga trace. Kung ang bug ay race condition, ang ThreadSanitizer sa debug build na may load na malapit sa production ay maaaring magbunyag ng problema.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din