چگونه ردیابی‌های پشته‌ای Chrome DevTools را 10 برابر افزایش دادیم

بندیکت مورر
Benedikt Meurer

توسعه دهندگان وب در هنگام اشکال زدایی کد خود انتظار کمی دارند یا هیچ تاثیری بر عملکرد ندارند. با این حال، این انتظار به هیچ وجه جهانی نیست. یک توسعه‌دهنده ++C هرگز انتظار ندارد که یک ساختار اشکال‌زدایی برنامه‌شان به عملکرد تولید برسد، و در سال‌های اولیه کروم، باز کردن DevTools به‌طور قابل‌توجهی بر عملکرد صفحه تأثیر می‌گذاشت.

این واقعیت که این کاهش عملکرد دیگر احساس نمی شود نتیجه سال ها سرمایه گذاری در قابلیت های اشکال زدایی DevTools و V8 است. با این وجود، هرگز نمی‌توانیم سربار عملکرد DevTools را به صفر برسانیم . تنظیم نقاط شکست، گذر از کد، جمع‌آوری ردپای پشته، گرفتن ردیابی عملکرد و غیره، همگی به درجات متفاوتی بر سرعت اجرا تأثیر می‌گذارند. از این گذشته، مشاهده چیزی آن را تغییر می دهد .

اما مسلماً سربار DevTools - مانند هر دیباگر دیگری - باید معقول باشد. اخیراً شاهد افزایش قابل توجهی در تعداد گزارش‌هایی بوده‌ایم که در موارد خاص، DevTools برنامه را تا حدی کند می‌کند که دیگر قابل استفاده نیست. در زیر می‌توانید مقایسه کنار هم از گزارش chromium:1069425 را مشاهده کنید، که سربار عملکرد به معنای واقعی کلمه باز بودن DevTools را نشان می‌دهد.

همانطور که از ویدیو می بینید، کاهش سرعت به ترتیب 5-10 برابر است، که به وضوح قابل قبول نیست. اولین قدم این بود که بفهمیم همه زمان ها به کجا می روند و چه چیزی باعث این کندی عظیم در زمانی که DevTools باز بود، می شود. استفاده از Linux perf در فرآیند Chrome Renderer، توزیع زیر را از زمان کلی اجرای رندر نشان داد:

زمان اجرای Chrome Renderer

در حالی که ما تا حدودی انتظار داشتیم چیزی مرتبط با جمع‌آوری ردپای پشته ببینیم، انتظار نداشتیم که حدود 90 درصد از زمان اجرای کلی به نمادسازی فریم‌های پشته اختصاص یابد. نمادسازی در اینجا به عمل حل نام توابع و موقعیت های منبع مشخص - اعداد خط و ستون در اسکریپت ها - از فریم های پشته خام اشاره دارد.

استنتاج نام روش

چیزی که حتی شگفت‌انگیزتر بود این واقعیت بود که تقریباً تمام وقت به تابع JSStackFrame::GetMethodName() در V8 می‌رود - اگرچه از تحقیقات قبلی می‌دانستیم که JSStackFrame::GetMethodName() در سرزمین مشکلات عملکرد غریبه نیست. این تابع سعی می‌کند نام متد را برای فریم‌هایی که فراخوانی‌های متد در نظر گرفته می‌شوند محاسبه کند (فریم‌هایی که فراخوانی‌های تابع از فرم obj.func() را به جای func() ) نشان می‌دهند. یک نگاه سریع به کد نشان داد که با انجام یک پیمایش کامل از شی و زنجیره اولیه آن و جستجوی

  1. ویژگی های داده ای که value آنها بسته شدن func یا
  2. ویژگی های accessor که در آن get یا set برابر با بسته شدن func است.

در حال حاضر، در حالی که این به خودی خود چندان ارزان به نظر نمی رسد، همچنین به نظر نمی رسد که این کاهش سرعت وحشتناک را توضیح دهد. بنابراین ما شروع به بررسی نمونه گزارش شده در chromium:1069425 کردیم و متوجه شدیم که ردپای پشته برای کارهای ناهمگام و همچنین برای پیام‌های گزارشی که از classes.js نشات می‌گیرند - یک فایل جاوا اسکریپت 10 MiB جمع‌آوری شده است. با نگاهی دقیق تر مشخص شد که این اساساً یک کد برنامه زمان اجرا جاوا به همراه کد برنامه کامپایل شده در جاوا اسکریپت است. ردیابی پشته حاوی چندین فریم با روش‌هایی بود که بر روی یک شی A فراخوانی می‌شدند، بنابراین فکر کردیم ارزش درک اینکه با چه نوع شی‌ای سروکار داریم را داشته باشد.

روی هم ردی از یک شی

ظاهراً کامپایلر جاوا به جاوا اسکریپت یک شیء منفرد با 82203 توابع روی آن تولید کرد - این به وضوح شروع به جالب شدن کرد. سپس به JSStackFrame::GetMethodName() V8 بازگشتیم تا بفهمیم آیا میوه‌ای کم آویزان وجود دارد که بتوانیم آنجا بچینیم.

  1. این کار بدین صورت است که ابتدا "name" تابع را به عنوان یک ویژگی روی شی جستجو می کند و اگر پیدا شد، بررسی می کند که مقدار ویژگی با تابع مطابقت داشته باشد.
  2. اگر تابع نامی نداشته باشد یا شیء دارای خاصیت منطبق نباشد، با عبور از تمام خصوصیات شی و نمونه های اولیه آن، به جستجوی معکوس برمی گردد.

در مثال ما، همه توابع ناشناس هستند و دارای خصوصیات "name" خالی هستند.

A.SDV = function() {
   // ...
};

اولین یافته این بود که جستجوی معکوس به دو مرحله تقسیم شد (برای خود شی و هر شی در زنجیره نمونه اولیه آن انجام شد):

  1. اسامی تمام خصوصیات قابل شمارش را استخراج کنید و
  2. برای هر نام یک جستجوی خصوصیات عمومی انجام دهید، و آزمایش کنید که آیا مقدار ویژگی به دست آمده با بسته شدن مورد نظر مطابقت دارد یا خیر.

این میوه مانند میوه‌ای کم آویزان به نظر می‌رسید، زیرا استخراج نام‌ها نیاز به قدم زدن در تمام خواص از قبل دارد. به جای انجام دو پاس - O(N) برای استخراج نام و O(N log(N)) برای تست ها - می‌توانیم همه چیز را در یک پاس انجام دهیم و مستقیماً مقادیر ویژگی را بررسی کنیم. که کل عملکرد را حدود 2 تا 10 برابر سریعتر کرد.

یافته دوم حتی جالب تر بود. در حالی که عملکردها از نظر فنی عملکردهای ناشناس بودند، موتور V8 چیزی را که ما نام استنباط شده برای آنها می نامیم ضبط کرده بود. برای حروف توابعی که در سمت راست تکالیف به شکل obj.foo = function() {...} تجزیه کننده V8 "obj.foo" را به عنوان نام استنتاج شده برای تابع literal به خاطر می سپارد. بنابراین در مورد ما این بدان معناست که، در حالی که ما نام مناسبی را نداشتیم که بتوانیم آن را جستجو کنیم، اما چیزی به اندازه کافی نزدیک داشتیم: برای مثال A.SDV = function() {...} در بالا، ما "A.SDV" به عنوان نام استنباط شده، و ما می‌توانیم با جستجوی آخرین نقطه، نام ویژگی را از نام استنباط شده استخراج کنیم و سپس به دنبال ویژگی "SDV" روی شی باشیم. این ترفند تقریباً در همه موارد انجام شد و یک پیمایش کامل گران قیمت را با یک جستجوی ملک جایگزین کرد. این دو بهبود به عنوان بخشی از این CL قرار گرفتند و به طور قابل توجهی کاهش سرعت را برای مثال گزارش شده در chromium:1069425 کاهش دادند.

Error.stack

می توانستیم آن را یک روز اینجا بنامیم. اما چیز عجیبی در جریان بود، زیرا DevTools هرگز از نام روش برای قاب‌های پشته استفاده نمی‌کرد. در واقع کلاس v8::StackFrame در C++ API حتی راهی برای رسیدن به نام متد نشان نمی دهد. بنابراین اشتباه به نظر می رسید که در وهله اول JSStackFrame::GetMethodName() فراخوانی کنیم. در عوض تنها جایی که از نام روش استفاده می کنیم (و در معرض نمایش قرار می دهیم) در جاوا اسکریپت stack trace API است. برای درک این کاربرد، مثال ساده زیر error-methodname.js را در نظر بگیرید:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

در اینجا ما یک تابع foo داریم که تحت نام "bar" روی object نصب شده است. با اجرای این قطعه در Chromium خروجی زیر به دست می آید:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

در اینجا ما جستجوی نام متد را در بازی مشاهده می کنیم: بالاترین قاب پشته برای فراخوانی تابع foo در نمونه ای از Object از طریق متدی به نام bar نشان داده می شود. بنابراین ویژگی غیر استاندارد error.stack استفاده زیادی از JSStackFrame::GetMethodName() می کند و در واقع تست های عملکرد ما نیز نشان می دهد که تغییرات ما کارها را به طور قابل توجهی سریعتر کرده است.

افزایش سرعت در معیارهای میکرو StackTrace

اما به موضوع Chrome DevTools بازگردیم، این واقعیت که نام روش محاسبه می‌شود، حتی اگر از error.stack استفاده نشود، درست به نظر نمی‌رسد. تاریخچه‌ای در اینجا وجود دارد که به ما کمک می‌کند: به‌طور سنتی V8 دو مکانیسم مجزا برای جمع‌آوری و نمایش ردیابی پشته‌ای برای دو API مختلف توصیف‌شده در بالا داشت (API C++ v8::StackFrame و جاوا اسکریپت stack trace API). داشتن دو روش مختلف برای انجام (تقریبا) یکسان مستعد خطا بود و اغلب منجر به ناهماهنگی ها و اشکالات می شد، بنابراین در اواخر سال 2018 پروژه ای را آغاز کردیم تا در یک گلوگاه برای ثبت ردیابی پشته قرار بگیریم.

آن پروژه یک موفقیت بزرگ بود و تعداد مسائل مربوط به مجموعه ردیابی پشته را به شدت کاهش داد. بیشتر اطلاعات ارائه شده از طریق ویژگی غیراستاندارد error.stack نیز با تنبلی و تنها زمانی که واقعاً مورد نیاز بود محاسبه شده بود، اما به عنوان بخشی از refactoring، ما همان ترفند را برای اشیاء v8::StackFrame اعمال کردیم. تمام اطلاعات مربوط به قاب پشته در اولین باری که هر روشی روی آن فراخوانی شد محاسبه می شود.

این به طور کلی عملکرد را بهبود می بخشد، اما متأسفانه مشخص شد که تا حدودی با نحوه استفاده از این اشیاء API C++ در Chromium و DevTools مخالف است. به ویژه از آنجایی که ما یک کلاس جدید v8::internal::StackFrameInfo را معرفی کرده بودیم، که تمام اطلاعات مربوط به یک قاب پشته را که یا از طریق v8::StackFrame یا از طریق error.stack در معرض دید قرار می گرفت، در خود نگه می داشت، ما همیشه مجموعه فوق العاده را محاسبه می کردیم. از اطلاعات ارائه شده توسط هر دو API، به این معنی که برای استفاده از v8::StackFrame (و به ویژه برای DevTools) به محض درخواست هرگونه اطلاعات در مورد یک قاب پشته، نام روش را نیز محاسبه می کنیم. به نظر می رسد که DevTools همیشه فوراً اطلاعات منبع و اسکریپت را درخواست می کند.

بر اساس این درک، ما توانستیم نمایش قاب پشته را به شدت ساده کنیم و حتی تنبل تر کنیم، به طوری که استفاده از V8 و Chromium اکنون فقط هزینه محاسبه اطلاعاتی را که آنها درخواست می کنند پرداخت می کند. این امر باعث تقویت عملکرد گسترده DevTools و سایر موارد استفاده از Chromium شد، که فقط به کسری از اطلاعات مربوط به قاب‌های پشته نیاز دارند (در اصل فقط نام اسکریپت و مکان منبع به شکل خط و ستون فاصله دارد) و در را برای اطلاعات بیشتر باز کرد. بهبود عملکرد

نام توابع

با حذف مجدد بازسازي‌هاي فوق، سربار نمادسازي (زمان صرف شده در v8_inspector::V8Debugger::symbolize ) به حدود 15% از زمان اجراي كلي كاهش يافت و ما مي‌توانيم به وضوح بيشتري ببينيم كه V8 زمان را در كجا مي‌گذراند. (جمع آوری و) نماد کردن قاب های پشته ای برای مصرف در DevTools.

هزینه نمادسازی

اولین چیزی که برجسته شد، هزینه تجمعی برای محاسبه خط و شماره ستون بود. بخش گران قیمت در اینجا در واقع محاسبه افست کاراکتر در اسکریپت است (بر اساس آفست بایت کدی که از V8 دریافت می کنیم)، و معلوم شد که به دلیل بازافریدی که در بالا انجام دادیم، این کار را دو بار انجام دادیم، یک بار هنگام محاسبه شماره خط و یک بار دیگر. هنگام محاسبه شماره ستون ذخیره موقعیت منبع در موارد v8::internal::StackFrameInfo به حل سریع این مشکل کمک کرد و v8::internal::StackFrameInfo::GetColumnNumber به طور کامل از هر نمایه حذف کرد.

یافته جالب‌تر برای ما این بود که v8::StackFrame::GetFunctionName در تمام نمایه‌هایی که نگاه کردیم به طرز شگفت‌آوری بالا بود. با حفاری عمیق‌تر در اینجا متوجه شدیم که محاسبه نامی که برای تابع در قاب پشته در DevTools نشان می‌دهیم غیرضروری پرهزینه است.

  1. ابتدا به دنبال ویژگی غیر استاندارد "displayName" می‌گردیم و اگر یک ویژگی داده با مقدار رشته به دست می‌دهد، از آن استفاده می‌کنیم:
  2. در غیر این صورت به جستجوی ویژگی استاندارد "name" و دوباره بررسی اینکه آیا ویژگی داده‌ای که مقدار آن یک رشته است به دست می‌آید، بازگردیم.
  3. و در نهایت به یک نام اشکال زدایی داخلی که توسط تجزیه کننده V8 استنباط شده و در تابع تحت اللفظی ذخیره می شود، برگردیم.

ویژگی "displayName" به عنوان راه حلی برای ویژگی "name" در نمونه های Function که فقط خواندنی و غیرقابل تنظیم در جاوا اسکریپت هستند، اضافه شد، اما از زمان توسعه دهنده مرورگر هرگز استانداردسازی نشد و استفاده گسترده ای مشاهده نشد. ابزار استنتاج نام تابع را اضافه کرد که در 99.9٪ موارد کار را انجام می دهد. علاوه بر این ES2015 ویژگی "name" را در نمونه های Function قابل تنظیم کرد، و به طور کامل نیاز به ویژگی "displayName" ویژه را از بین برد. از آنجایی که جستجوی منفی برای "displayName" بسیار پرهزینه است و واقعاً ضروری نیست (ES2015 بیش از پنج سال پیش منتشر شد)، ما تصمیم گرفتیم پشتیبانی از ویژگی غیر استاندارد fn.displayName از V8 (و DevTools) حذف کنیم .

با جستجوی منفی "displayName" ، نیمی از هزینه v8::StackFrame::GetFunctionName حذف شد. نیم دیگر به جستجوی ویژگی عمومی "name" می رود. خوشبختانه ما قبلاً منطقی برای جلوگیری از جستجوهای پرهزینه ویژگی "name" در نمونه های Function (دست نخورده) داشتیم، که چندی پیش در V8 معرفی کردیم تا خود Function.prototype.bind() سریعتر شود. ما بررسی‌های لازم را منتقل کردیم که به ما امکان می‌دهد در وهله اول از جستجوی عمومی پرهزینه صرف نظر کنیم، در نتیجه v8::StackFrame::GetFunctionName دیگر در هیچ نمایه‌ای که در نظر گرفته‌ایم نشان داده نمی‌شود.

نتیجه گیری

با بهبودهای فوق، ما به طور قابل توجهی هزینه های سربار DevTools را از نظر ردیابی پشته کاهش داده ایم.

ما می دانیم که هنوز پیشرفت های مختلفی وجود دارد - به عنوان مثال هزینه های اضافی هنگام استفاده از MutationObserver s همچنان قابل توجه است، همانطور که در chromium:1077657 گزارش شده است - اما در حال حاضر، ما به نقاط درد اصلی اشاره کرده ایم و ممکن است دوباره به آن بازگردیم. آینده برای ساده سازی بیشتر عملکرد اشکال زدایی.

کانال های پیش نمایش را دانلود کنید

استفاده از Chrome Canary ، Dev یا Beta را به عنوان مرورگر توسعه پیش‌فرض خود در نظر بگیرید. این کانال‌های پیش‌نمایش به شما امکان دسترسی به جدیدترین ویژگی‌های DevTools را می‌دهند، به شما اجازه می‌دهند APIهای پلتفرم وب پیشرفته را آزمایش کنید و به شما کمک می‌کنند تا قبل از کاربران، مشکلات سایت خود را پیدا کنید!

با تیم Chrome DevTools در تماس باشید

از گزینه‌های زیر برای بحث در مورد ویژگی‌های جدید، به‌روزرسانی‌ها یا هر چیز دیگری مربوط به DevTools استفاده کنید.