Heisenbug — باگی که هنگام تلاش برای دیباگ ناپدید میشود. این اصطلاح از اصل عدم قطعیت هایزنبرگ گرفته شده است: مشاهده بر رفتار سیستم تأثیر میگذارد. در توسعه موبایل، Heisenbug یکی از دشوارترین مشکلات است زیرا روشهای استاندارد دیباگ (لاگها، نقاط شکست، کد اضافی) وضعیت برنامه را تغییر میدهند و باگ را پنهان میکنند. علل بروز و روشهای مقابله با خطاهای گریزان را بررسی میکنیم.
نکات اصلی
Heisenbug — دستهای از خطاها که در محیط تولید یا هنگام کار عادی ظاهر میشوند، اما هنگام تلاش برای تکرار در محیط دیباگ ناپدید میشوند. این اصطلاح در دهه ۱۹۸۰ توسط برنامهنویس Jim Gray در زمینه سیستمهای توزیعشده معرفی شد، اما امروز به دلیل ماهیت ناهمزمان برنامههای موبایل بسیار مرتبط است.
علت اصلی: ابزارهای استاندارد دیباگ محیط اجرا را تغییر میدهند. نقطه شکست (breakpoint) نخ را برای چند میلیثانیه متوقف میکند، لاگگیری I/O همزمان اضافه میکند، بررسیهای اضافی ترتیب عملیات را تغییر میدهند. در محیط چندنخی، حتی تأخیر میکروثانیهای میتواند ترتیب اجرای نخها را تغییر دهد و رقابت داده را پنهان کند.
بر اساس دادههای Microsoft Research (۲۰۲۲)، حدود ۱۵-۲۵٪ از همه باگها در برنامههای موبایل چندنخی به عنوان Heisenbug طبقهبندی میشوند. زمان یافتن و رفع یک Heisenbug به طور متوسط ۵-۱۰ برابر بیشتر از یک باگ معمولی است، زیرا تکرار مستقیم امکانپذیر نیست.
برنامه در محیط تولید هنگام کشیدن سریع لیست کرش میکند، اما پس از اتصال دیباگر یا اضافه کردن لاگها عالی کار میکند. علت: رقابت داده بین نخ UI (بهروزرسانی RecyclerView) و نخ پسزمینه (بهروزرسانی دادههای آداپتر). لاگها تأخیری اضافه میکنند که نخها را به طور تصادفی همگامسازی میکند.
Bohrbug — باگ قابل پیشبینی و پایدار قابل تکرار. این نام به قیاس با مدل اتمی بور گرفته شده است: مانند اتم، باگ در هر مشاهده یکسان رفتار میکند. مثال: NullPointerException هنگام کلیک روی دکمه قبل از بارگذاری داده. با تست واحد استاندارد درمان میشود.
Mandelbug — باگ با رابطه علت و معلولی پیچیده و آشوبگونه (به قیاس با مجموعه ماندلبرو نامگذاری شده است). فقط در ترکیب خاصی از شرایط ظاهر میشود: نسخه سیستم عامل، مدل دستگاه، وضعیت شبکه، فاز ماه. تفاوت آن با Heisenbug این است که در هنگام دیباگ ناپدید نمیشود — مشکل در دشواری تکرار است، نه تغییر رفتار توسط ابزارها.
Heisenbug — باگی که دقیقاً به دلیل ابزارهای دیباگ ناپدید میشود. اگر لاگ اضافه کنید — باگ ناپدید میشود. اگر نقطه شکست بگذارید — باگ ظاهر نمیشود. اگر همه چیز را بردارید — باگ برمیگردد. علت اصلی: زمانبندی تغییر یافته در هنگام دیباگ.
| نوع | تکرارپذیری | واکنش به دیباگ | مثال |
|---|---|---|---|
| Bohrbug | ۱۰۰٪ | تغییر نمیکند | NPE در لیست خالی |
| Mandelbug | آشوبگونه | تغییر نمیکند | کرش در Android 12، سامسونگ، با باتری کم |
| Heisenbug | فقط بدون دیباگ | ناپدید میشود | Race condition ناپدیدشونده با لاگها |
| Schrödinbug | در کد ظاهر نمیشود | با نگاه ظاهر میشود | باگ قابل مشاهده در کد که هرگز رخ نمیدهد |
Race condition — شماره یک در میان علل Heisenbug. دو نخ بدون همگامسازی به دادههای مشترک دسترسی پیدا میکنند. دیباگر تأخیری ایجاد میکند که باعث میشود نخها به طور طبیعی همگام شوند. بدون دیباگر ترتیب اجرا غیرقابل پیشبینی است.
خطاهای وابسته به زمانبندی — خطاهایی که فقط در سرعت خاصی از اجرا ظاهر میشوند. مثلاً انیمیشنی که باید قبل از شروع عملیات بعدی کامل شود. در دیباگر انیمیشن کندتر اجرا میشود و عملیات بعد از اتمام انیمیشن شروع میشود. در تولید — برعکس.
// مثال race condition — Heisenbug معمولی
class ListViewModel : ViewModel() {
private var items = mutableListOf<String>()
fun loadFromNetwork() {
viewModelScope.launch(Dispatchers.IO) {
val result = api.fetchItems()
items.addAll(result) // ❌ ایمن نخی نیست
}
}
fun getItems(): List<String> = items.toList()
// Race condition: خواندن getItems ممکن است با نوشتن loadFromNetwork همپوشانی داشته باشد
}
بهینهسازی کامپایلر — کامپایلر (JIT، ART، Kotlin/Native) ممکن است برای بهینهسازی دستورالعملها را مرتب کند. در نسخه دیباگ (debug build) بهینهسازیها غیرفعال هستند و کد «آنطور که نوشته شده» اجرا میشود. در release build کامپایلر ترتیب عملیات را تغییر میدهد که میتواند فرضیات پنهان در کد را آشکار کند.
ThreadSanitizer (TSan) — ابزار Google برای تشخیص رقابت داده در C/C++ و Kotlin/Native. در build جاسازی میشود و هر دسترسی به حافظه مشترک بدون همگامسازی را تشخیص میدهد. بر خلاف لاگها، TSan بر زمانبندی تأثیر نمیگذارد زیرا از طریق instrumented code کار میکند، نه از طریق I/O.
تستهای قطعی — ناهمزمانی واقعی را با کنترلشده جایگزین کنید. برای کنترل کامل روی ترتیب اجرا از TestDispatcher (Kotlin)، RxJava Plugins یا GCD test queues (iOS) استفاده کنید. سناریوهای خاص تعیین کنید: نخ A اجرا میشود، سپس B، سپس A دوباره.
لاگگیری چرخهای — لاگگیری در بافر چرخهای در حافظه (نه روی دیسک). وقتی باگ رخ میدهد، بافر در فایل ذخیره میشود. از آنجایی که نوشتن در حافظه نانوثانیه طول میکشد (به جای میلیثانیه برای I/O دیسک)، چنین لاگی بر زمانبندی تأثیر نمیگذارد و 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) } }
}
}
لاگگیری در تولید — اگر باگ به صورت محلی تکرار نمیشود، در تولید داده جمعآوری کنید. از Firebase Crashlytics logs، Sentry Breadcrumbs یا لاگر چرخهای سفارشی استفاده کنید. مهم: لاگگیری باید ناهمزمان باشد و حداقل تأثیر را بر عملکرد داشته باشد.
ایزوله کردن وضعیت — state اشتراکی قابل تغییر را به حداقل برسانید. هر مؤلفه باید وضعیت ایزوله خود را داشته باشد که برای نوشتن مستقیم از سایر مؤلفهها قابل دسترسی نباشد. از Unidirectional Data Flow (UDF) استفاده کنید — وضعیت در یک جهت جریان دارد: Event → Reducer → State → UI.
رویکرد تابعی — توابع خالص بدون عوارض جانبی تست و دیباگ آسانتری دارند. عوارض جانبی (شبکه، پایگاه داده، فایلها) را در لایههای کاملاً مشخص (repository، data source) ایزوله کنید. خطاهای نخی در کد تابعی عملاً غیرممکن هستند.
حالت سختگیرانه — Android StrictMode را در debug build فعال کنید. نقض سیاست نخبندی (شبکه در نخ اصلی، I/O دیسک در نخ اصلی) را تشخیص میدهد و استثنا پرتاب میکند. این کار Heisenbug بالقوه را به Bohrbug قطعی و بلافاصله قابل مشاهده تبدیل میکند.
class DebugApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
}
}
}
بازبینی کد با تمرکز بر ناهمزمانی — بخش اجباری فرآیند. هر pull request باید از نظر state اشتراکی قابل تغییر، مجموعههای ناایمن نخی، عدم همگامسازی بررسی شود. برای ممنوعیت خودکار الگوهای خاص از قوانین lint استفاده کنید (مثلاً دسترسی به MutableList بدون synchronized).
سوالات متداول
زیرا روشهای استاندارد — نقاط شکست، لاگها، print — محیط اجرا را آنقدر تغییر میدهند که باگ دیگر ظاهر نمیشود. دیباگر همه نخها را برای دهها میلیثانیه متوقف میکند. در این مدت race condition که باعث باگ میشد به طور طبیعی حل میشود. به ابزارهایی نیاز است که بر زمانبندی اجرا تأثیر نگذارند.
Mandelbug به دلیل پیچیدگی شرایط به سختی تکرار میشود، اما ابزارهای دیباگ بر ظاهر آن تأثیر نمیگذارند. Heisenbug دقیقاً از ابزارهای دیباگ ناپدید میشود. مثال Mandelbug: کرش فقط در دستگاههای Android 11 با ۳ گیگابایت رم و سطح باتری زیر ۱۵٪. مثال Heisenbug: race condition که با اضافه کردن Log.d() ناپدید میشود.
Flaky test detection را اجرا کنید — تستهایی که گاهی رد میشوند، گاهی قبول. در Android از Android Test Orchestrator برای ایزوله کردن تستها استفاده کنید. StrictMode را به تستهای debug اضافه کنید. build را با ThreadSanitizer ابزارگذاری کنید. اگر تست در بیش از ۵٪ اجراها flaky است — آن را Heisenbug بالقوه در نظر بگیرید و قبل از merge بررسی کنید.
تا حدی. Flow و structured concurrency در Kotlin مقدار state اشتراکی قابل تغییر را کاهش میدهند و مدیریت نخها را ساده میکنند. اما Coroutines ایمنی نخی را تضمین نمیکنند: اگر دو coroutine state اشتراکی داشته باشند، race condition همچنان ممکن است. برای محافظت از state مشترک از Mutex یا برای انتقال داده بین coroutineها از Channel استفاده کنید.
از بافر چرخهای لاگ در حافظه با تخلیه خودکار هنگام خطا استفاده کنید. نظارت دقیق از طریق Crashlytics یا Sentry با breadcrumbs سفارشی اضافه کنید. برای Android ANR detection را فعال کنید و traceها را بررسی کنید. اگر باگ race condition است، ThreadSanitizer در debug build با بار نزدیک به تولید میتواند مشکل را آشکار کند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.