اگر ساعتهای زیادی را صرف حذف دستی خطوط کد یا دادهها میکنید تا علت دقیق یک کرش را بیابید، در واقع زمان خود را برای مشکلی تلف میکنید که Shrink Ray میتواند در چند دقیقه حل کند. در ۹ ژوئن ۲۰۲۶، یک بررسی فنی مفصل در tratt.net فاش کرد که کاهشدهندههای مورد آزمون چگونه میتوانند ورودیهای مشکلساز را بین ۹۵ تا ۹۹ درصد کوچک کنند و کوهی از نویز را به یک خط کد قابل عیبیابی تبدیل کنند.
بسیاری از توسعهدهندگان به زنجیرهای خطی از ابزارها تکیه میکنند: آنها با دستورات ساده چاپ (print) شروع میکنند، به دیباگرهای رسمی نقل مکان میکنند و در نهایت به سراغ ابزارهای سنگین مانند sanitizers یا Valgrind میروند. اما مؤثرترین راه برای حل یک باگ، اغلب این است که ورودی تحریککننده آن را تا حد ممکن کوچک کنند. کاهش دستی یک وظیفه «سیزیفوار» است؛ زیرا حذف یک بخش از ورودی ممکن است کرش را متوقف کند، در حالی که حذف دو بخش مجزا ممکن است دوباره آن را فعال کند. این وضعیت فضای جستوجویی ایجاد میکند که بسیار بزرگتر از توان مدیریت انسانی است.
سازوکار کاهش (The Mechanism of Reduction)
یک کاهشدهنده مورد آزمون بر سه رکن اصلی استوار است: یک برنامه، یک ورودی و یک «تست جذابیت» (Interestingness Test). کاهشدهنده بهطور تکرارپذیر نسخههای کوتاهتری از ورودی را امتحان میکند. تست جذابیت — اسکریپتی که اگر خطا همچنان پابرجاست مقدار ۰ و در غیر این صورت مقداری غیرصفر برمیگرداند — به عنوان راهنمای ابزار عمل میکند.
برای درک سادگی این سازوکار، یک برنامه پایتون را تصور کنید که کلمات را از یک فایل میخواند و اگر کلمهای بیش از ۲۵ نویسه باشد، هشدار میدهد (if len(l) > 25: print("Word too long\n")). یک کاهشدهنده ابتدایی میتواند در چند خط کد پایتون نوشته شود که صرفاً هر بار یک خط از ورودی را حذف کرده و بررسی کند که آیا هشدار هنوز ظاهر میشود یا خیر.
این کاهشدهنده ابتدایی با بارگذاری متن در یک لیست (cur) کار میکند. سپس با حذف یک خط (del cnd[i]) یک کاندید (cnd) ایجاد کرده و آن را در یک فایل موقت مینویسد. اگر تست جذابیت مقدار ۰ را برگرداند، این کاندید به نقطه شروع جدید تبدیل میشود. در یک تست واقعی با استفاده از فایل /usr/share/dict/words در لینوکس، این روش با موفقیت توانست تنها یک کلمه طولانی یعنی 'antidisestablishmentarianism' را به عنوان تنها علت خطا ایزوله کند.
منطق کاهش و پیادهسازی
منطق داخلی این کاهشدهنده ابتدایی از یک حلقه مشخص پیروی میکند:
- متن ورودی را بارگذاری کرده و آن را به خطوط تقسیم میکند (
cur). - در ورودی پیمایش میکند و با حذف یک خط نسبت به نقطه شروع قبلی، یک کاندید (
cnd) میسازد. - کاندید را در یک
tempfile.NamedTemporaryFileمینویسد و تست جذابیت را از طریقsubprocess.runاجرا میکند. - اگر تست مقدار ۰ برگرداند، ورودی فعلی را بهروز میکند (
cur = cnd)؛ در غیر این صورت، شاخص را افزایش میدهد (i += 1).
نکته کلیدی این است که کاهشدهنده هیچ درکی از برنامه زیربنایی ندارد؛ او صرفاً از سیگنال باینری تست جذابیت برای هرس کردن ورودی پیروی میکند. این «ناآگاهی» در واقع راز موفقیت آن است و اجازه میدهد روی هر فایل متنی، فارغ از پیچیدگی برنامه، کار کند.
مقیاسبندی با ابزارهای حرفهای
در حالی که اسکریپتهای دستی برای موارد ساده جواب میدهند، ابزارهای حرفهای مانند Shrink Ray از قوانین کاهش پیچیده و اجرای موازی استفاده میکنند. نویسنده اشاره میکند که سازندگان کامپایلرها بیشترین بهره را از این ابزارها بردهاند، هرچند برای هر برنامهنویسی مفید هستند.
در یک تست روی برنامه C با ۷۸ خط کد که با پرچمهای مختلف کامپایلر (FAST=0 در مقابل FAST=1) تحریک میشد، یک کاهشدهنده ساده توانست در ۱۰ ثانیه حجم کد را ۳۰٪ کم کند. برای بهبود این نتیجه، نویسنده یک حلقه ساده افزود: هر بار که یک ورودی جذاب پیدا میشد، کاهشدهنده حذف خطوط را دوباره از ابتدا (i=0) شروع میکرد. این تغییر ساده باعث شد زمان اجرا ۱۰ برابر شود، اما توانست ۳ خط دیگر از کد را حذف کند.
وقتی Shrink Ray با پرچم --no-clang-delta (برای جلوگیری از استفاده از دانش خاص زبان C) روی همین برنامه اجرا شد، در ۱۵ دقیقه بیش از ۶۰٪ از حجم بایتها را کاهش داد. این ابزار فقط خطوط را حذف نمیکند؛ بلکه سینتکسهای رایج کامنت را میشناسد و حتی میتواند اعداد بزرگ را به مقادیر کوچکتر تبدیل کند تا فرآیند دیباگ سادهتر شود.

تسلط بر تست جذابیت
طبق گزارش tratt.net، بزرگترین مانع در کاهش، نوشتن یک تست جذابیت دقیق است. یک تست غیربهینه میتواند منجر به «کاهش بیش از حد» (Over-reduction) شود؛ جایی که کاهشدهنده دقیقاً همان باگی را که سعی در یافتنش دارید، حذف میکند. Shrink Ray برای جلوگیری از این خطای رایج، صراحتاً بررسی میکند که آیا تست، ورودی خالی را میپذیرد یا خیر.
برای جلوگیری از این مشکل، نویسنده چندین استراتژی حیاتی را پیشنهاد میکند:
- اعتبارسنجی سختگیرانه: بهجای اینکه فقط بررسی کنید آیا دو خروجی متفاوت هستند (مثلاً
test "$slow_out" != "$fast_out")، تست باید تأیید کند که نسخه «صحیح» برنامه هنوز خروجی مورد انتظار را تولید میکند (مثلاًtest "$slow_out" = "0d754a56"). این کار تضمین میکند که کاهش باعث شکست منطق اصلی یا ایجاد تفاوتهای گمراهکننده نشده است. - بهینهسازی عملکرد: چون کاهشدهندهها تستها را صدها بار در ثانیه اجرا میکنند، تست باید سریع باشد. این شامل بهینهسازی اسکریپتهای شل و حتی غیرفعال کردن خودکار فایلهای core dump برای افزایش سرعت تا ۳ برابر است.
- مدیریت مهلت زمانی (Timeout): برای جلوگیری از برنامههایی که پایان نمییابند (که وقتی کاهشدهنده خطی مثل
i-=1را حذف میکند رخ میدهد)، نویسنده ازtimeout 1sبرای دستورات فرعی استفاده میکند. قانون کلی این است که زمان اجرای اولیه را اندازه بگیرید و مهلت را تقریباً ۱.۵ تا ۲ برابر آن زمان قرار دهید. - آگاهی از موازیسازی: کاهشدهندههایی مانند Shrink Ray تستها را بهصورت موازی اجرا میکنند. در حالی که Shrink Ray از دایرکتوریهای موقت برای جداسازی اجراها استفاده میکند، توسعهدهندگان باید مراقب منابع مشترکی باشند که میتوانند باعث ایجاد تداخل (Race Condition) در تست جذابیت شوند.
حل عدم قطعیت (Non-Determinism)
یکی از قدرتمندترین کاربردهای این ابزارها، مقابله با باگهای غیرقطعی است؛ خطاهایی که فقط در بخشی از اجراها ظاهر میشوند. برای مثال، باگی که فقط اگر random.random() < 0.33 باشد رخ میدهد، تنها در یکسوم موارد باعث خطای تقسیم بر صفر میشود.
با نوشتن تست جذابیت که ورودی را چندین بار (مثلاً ۳ تکرار) اجرا کرده و اگر خطا حداقل یک بار رخ داد آن را بپذیرد، کاهشدهنده اغلب میتواند نسخهای از ورودی را بیابد که فراخوانی تصادفی را کاملاً حذف کند و یک خطای غیرقطعی را به خطای قطعی تبدیل کند.
اگر باگ همچنان گریزان باشد، نویسنده یک رویکرد دو مرحلهای را توصیه میکند، زیرا تستهای سختگیرانه (که نیاز دارند خطا $n$ بار پشت سر هم رخ دهد) اغلب برای شروع بسیار دشوار هستند — ورودی اولیه ممکن است تنها ۳.۶٪ شانس عبور از چنین تستی را داشته باشد:
۱. فاز سهلگیرانه: با تستی شروع کنید که اگر خطا حداقل یک بار در $n$ اجرا رخ داد، ورودی را بپذیرد. این به کاهشدهنده اجازه میدهد مسیری برای پیشروی بیابد.
۲. فاز سختگیرانه: پس از یافتن یک کاهش، تست را بهصورت دستی بازنویسی کنید تا فقط در صورتی ورودی را بپذیرد که خطا $n$ بار پشت سر هم رخ دهد. این کار قطعیت را تثبیت میکند.
این انتقال «شانسمحور»، کاهشدهنده را مجبور میکند تا باگ را پایدار کند و اصلاح آن را بهطور قابل توجهی سادهتر میسازد. شهود نویسنده این است که زیرمسیرهای کاهش اغلب همگرا میشوند، به این معنی که یک کاهش «بد» احتمالاً بعداً شناسایی میشود، حتی اگر تست سختگیرانه گاهی دور زده شود.
هدایت فراتر از طول ورودی
کاهشدهندههای استاندارد از طول ورودی به عنوان معیاری برای «بهتر بودن» استفاده میکنند. با این حال، در برخی موارد، اندازه اثر اجرای حاصل (Execution Trace) مهمتر از اندازه فایل ورودی است. این موضوع بهویژه در دیباگ ابزارهایی مانند yk صادق است، جایی که تعداد دستورات اجرا شده (طول اثر) دشواری دیباگ را تعیین میکند.
از آنجایی که Shrink Ray روش اصولی برای بیان طول اثر ندارد، نویسنده از یک «هک ناامن» از طریق یک شمارنده سراسری استفاده میکند. تست جذابیت کوتاهترین طول اثر یافت شده تا آن لحظه را در یک فایل موقت (/tmp/global_best) ذخیره میکند. مکانیزم به این صورت است:
- تست برنامه را اجرا کرده و خروجی را میگیرد (مثلاً
YKD_LOG="$t:jit-asm"). - بررسی میکند که آیا برنامه segfault کرده است (کد خروج ۱۳۹) از طریق
[ "$?" -ne 139 ]. - تعداد خطوط اثر حاصل را با
wc -lمیشمارد و فاصلهها را باtr -d " "حذف میکند. - اگر اثر جدید طولانیتر از مقدار موجود در
/tmp/global_bestباشد، ورودی به عنوان مورد غیرجذاب رد میشود. - اگر اثر کوتاهتر یا برابر باشد، طول جدید در
/tmp/global_bestنوشته شده و ورودی پذیرفته میشود.
در یک مورد، این تکنیک یک اثر segfault را از ۴۰,۰۰۰ خط به ۱۰,۱۰۰ خط کاهش داد. اگرچه فایل ورودی نهایی کمی بزرگتر از آنچه یک کاهشدهنده استاندارد تولید میکرد بود، اما اثر آنقدر کوچک شد که توسعهدهنده در ۳۰ دقیقه باگ را شناسایی کرد.
این تغییر تمرکز — از کاهش «ورودی» به کاهش «اثر» — به توسعهدهندگان اجازه میدهد تا زمان واقعی (wall-clock time)، پیچیدگی اجرا یا سطح عدم قطعیت را، بدون توجه به اندازه فایل اصلی، پایین بیاورند.
این رویکرد فرض بنیادین را که «کوچکترین فایل همیشه بهترین است» تغییر میدهد. با تغییر معیارهای «جذابیت»، توسعهدهندگان میتوانند جستوجوی تپهنوردی (hill-climbing) کاهشدهنده را مجبور کنند تا برای معیاری بهینه شود که باگ را برای انسان قابل خواندن میکند.
اگر در حال حاضر با یک تست ناپایدار (flaky) یا یک لاگ کرش حجیم میجنگید، اولین قدم شما باید تعریف یک اسکریپت باینری «تست جذابیت» باشد و اجازه دهید یک کاهشدهنده کارهای سخت را برای شما انجام دهد.




گفتگو