یک پانل عملکرد 400٪ سریعتر از طریق درک تصویر

آندرس اولیوارس
Andrés Olivares
نانسی لی
Nancy Li

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

مثال زیر شما را با استفاده از پنل عملکرد راهنمایی می کند.

راه اندازی و بازآفرینی سناریوی نمایه سازی ما

اخیراً هدف ما این است که پنل Performance را با عملکرد بهتری انجام دهیم. به ویژه، ما می‌خواستیم حجم زیادی از داده‌های عملکرد را سریع‌تر بارگیری کند. این مورد، برای مثال، هنگام پروفیل فرآیندهای طولانی مدت یا پیچیده یا گرفتن داده های با دانه بندی بالا است. برای رسیدن به این هدف، ابتدا به درک چگونگی عملکرد برنامه و چرایی عملکرد آن نیاز بود که با استفاده از ابزار پروفایل به دست آمد.

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

با آماده شدن تنظیمات، سناریویی که باید نمایه شود باید دوباره ایجاد و ضبط شود. برای جلوگیری از سردرگمی، به پنجره DevTools اصلی به عنوان "نمونه اول DevTools" و پنجره ای که اولین نمونه را بررسی می کند، "نمونه دوم DevTools" نامیده می شود.

تصویری از یک نمونه DevTools که عناصر موجود در خود DevTools را بررسی می کند.
DevTools-on-DevTools: بازرسی DevTools با DevTools.

در نمونه دوم DevTools، پانل Performance - که از اینجا به بعد پانل perf نامیده می شود - اولین نمونه DevTools را برای ایجاد مجدد سناریو مشاهده می کند که یک نمایه را بارگیری می کند.

در نمونه دوم DevTools یک ضبط زنده شروع می شود، در حالی که در اولین نمونه، یک نمایه از یک فایل روی دیسک بارگذاری می شود. یک فایل بزرگ به منظور نمایش دقیق عملکرد پردازش ورودی های بزرگ بارگذاری می شود. هنگامی که هر دو نمونه بارگیری می‌شوند، داده‌های نمایه عملکرد - که معمولاً ردیابی نامیده می‌شود - در دومین نمونه DevTools از پانل perf که یک نمایه را بارگیری می‌کند، دیده می‌شود.

حالت اولیه: شناسایی فرصت های بهبود

پس از اتمام بارگذاری، موارد زیر در نمونه پانل perf دوم ما در اسکرین شات بعدی مشاهده شد. روی فعالیت رشته اصلی تمرکز کنید، که در زیر مسیر با عنوان Main قابل مشاهده است. مشاهده می شود که پنج گروه بزرگ از فعالیت در نمودار شعله وجود دارد. اینها شامل وظایفی است که در آن بارگذاری بیشترین زمان را می گیرد. زمان کل این کارها تقریباً 10 ثانیه بود. در تصویر زیر، از پنل عملکرد برای تمرکز روی هر یک از این گروه‌های فعالیت استفاده می‌شود تا ببینید چه چیزی می‌تواند پیدا شود.

تصویری از پانل عملکرد در DevTools در حال بررسی بارگیری یک ردیابی عملکرد در پانل عملکرد یک نمونه دیگر از DevTools. نمایه حدود 10 ثانیه طول می کشد تا بارگذاری شود. این زمان بیشتر در پنج گروه اصلی فعالیت تقسیم می شود.

گروه فعالیت اول: کارهای غیر ضروری

مشخص شد که اولین گروه از فعالیت‌ها، کدهای قدیمی بود که هنوز اجرا می‌شد، اما واقعاً مورد نیاز نبود. اساساً، هر چیزی که تحت بلوک سبز با برچسب processThreadEvents قرار داشت، تلاش‌ها را هدر داد. آن یکی یک برد سریع بود. با حذف آن فراخوانی عملکرد حدود 1.5 ثانیه در زمان صرفه جویی می شود. سرد!

گروه فعالیت دوم

در گروه فعالیت دوم، راه حل به سادگی مورد اول نبود. buildProfileCalls حدود 0.5 ثانیه طول کشید و این کار چیزی نبود که بتوان از آن اجتناب کرد.

تصویری از پانل عملکرد در DevTools در حال بازرسی نمونه دیگری از پنل عملکرد. یک کار مرتبط با تابع buildProfileCalls حدود 0.5 ثانیه طول می کشد.

از روی کنجکاوی، گزینه Memory را در پنل perf برای بررسی بیشتر فعال کردیم و دیدیم که فعالیت buildProfileCalls نیز از حافظه زیادی استفاده می کند. در اینجا، می‌توانید ببینید که چگونه نمودار خط آبی ناگهان در زمان اجرای buildProfileCalls پرش می‌کند، که نشان‌دهنده نشت احتمالی حافظه است.

تصویری از نمایه ساز حافظه در DevTools که مصرف حافظه پنل عملکرد را ارزیابی می کند. بازرس پیشنهاد می کند که تابع buildProfileCalls مسئول نشت حافظه است.

برای پیگیری این شبهه، از پنل حافظه (پنل دیگری در DevTools، متفاوت از کشوی حافظه در پنل perf) برای بررسی استفاده کردیم. در پانل حافظه، نوع پروفایل «نمونه‌گیری تخصیص» انتخاب شد، که عکس فوری پشته‌ای را برای پانل پرف که نمایه CPU را بارگیری می‌کند، ثبت کرد.

تصویری از وضعیت اولیه نمایه ساز حافظه. گزینه «نمونه‌سازی تخصیص» با یک کادر قرمز برجسته شده است و نشان می‌دهد که این گزینه برای پروفایل حافظه جاوا اسکریپت بهترین است.

تصویر زیر عکس فوری پشته ای را نشان می دهد که جمع آوری شده است.

تصویری از نمایه‌ساز حافظه، با یک عملیات مبتنی بر مجموعه با حافظه فشرده انتخاب شده است.

از این عکس فوری heap، مشاهده شد که کلاس Set حافظه زیادی مصرف می کند. با بررسی نقاط فراخوانی، مشخص شد که ما به طور غیرضروری خصوصیات نوع Set را به اشیایی که در حجم زیاد ایجاد شده اند اختصاص می دهیم. این هزینه در حال افزایش بود و حافظه زیادی مصرف می‌شد، به حدی که معمولاً برنامه در ورودی‌های بزرگ خراب می‌شد.

مجموعه‌ها برای ذخیره اقلام منحصربه‌فرد مفید هستند و عملیات‌هایی را ارائه می‌دهند که از منحصربه‌فرد بودن محتوای آن‌ها استفاده می‌کنند، مانند حذف کردن مجموعه داده‌ها و ارائه جستجوهای کارآمدتر. با این حال، این ویژگی‌ها ضروری نبودند زیرا داده‌های ذخیره‌شده تضمین شده بود که از منبع منحصربه‌فرد هستند. به این ترتیب، ست ها در وهله اول ضروری نبودند. برای بهبود تخصیص حافظه، نوع ویژگی از یک Set به یک آرایه ساده تغییر یافت. پس از اعمال این تغییر، یک عکس فوری پشته دیگر گرفته شد و کاهش تخصیص حافظه مشاهده شد. علیرغم عدم دستیابی به بهبودهای قابل توجه سرعت با این تغییر، مزیت ثانویه این بود که برنامه با دفعات کمتری از کار می افتد.

تصویری از نمایه ساز حافظه. عملیات مبتنی بر مجموعه ای که قبلاً حافظه فشرده بود، برای استفاده از یک آرایه ساده تغییر یافت که هزینه حافظه را به میزان قابل توجهی کاهش داد.

گروه فعالیت سوم: وزن کردن مبادلات ساختار داده

بخش سوم عجیب است: در نمودار شعله می توانید ببینید که از ستون های باریک اما بلند تشکیل شده است که نشان دهنده فراخوانی های عملکرد عمیق و بازگشت های عمیق در این مورد است. در مجموع این بخش حدود 1.4 ثانیه به طول انجامید. با نگاه کردن به پایین این بخش، مشخص شد که عرض این ستون ها با مدت زمان یک تابع تعیین می شود: appendEventAtLevel ، که نشان می دهد می تواند یک گلوگاه باشد.

در اجرای تابع appendEventAtLevel ، یک چیز برجسته بود. برای هر ورودی داده در ورودی (که در کد به عنوان "رویداد" شناخته می شود)، یک مورد به نقشه اضافه شد که موقعیت عمودی ورودی های جدول زمانی را ردیابی می کرد. این مشکل ساز بود، زیرا مقدار اقلامی که ذخیره می شد بسیار زیاد بود. نقشه ها برای جستجوهای مبتنی بر کلید سریع هستند، اما این مزیت به صورت رایگان ارائه نمی شود. همانطور که نقشه بزرگتر می شود، افزودن داده به آن می تواند به عنوان مثال به دلیل هش کردن مجدد گران شود. این هزینه زمانی قابل توجه می شود که مقادیر زیادی آیتم به طور متوالی به نقشه اضافه شود.

/**
 * Adds an event to the flame chart data at a defined vertical level.
 */
function appendEventAtLevel (event, level) {
  // ...

  const index = data.length;
  data.push(event);
  this.indexForEventMap.set(event, index);

  // ...
}

ما با روش دیگری آزمایش کردیم که نیازی به اضافه کردن یک مورد در نقشه برای هر ورودی در نمودار شعله نداشت. این بهبود قابل توجه بود و تأیید می کرد که گلوگاه در واقع به سربار متحمل شده با اضافه کردن تمام داده ها به نقشه مربوط می شود. زمانی که گروه فعالیت گرفت از حدود 1.4 ثانیه به حدود 200 میلی ثانیه کاهش یافت.

قبل از:

تصویری از پنل عملکرد قبل از بهینه سازی در تابع appendEventAtLevel. کل زمان اجرای تابع 1372.51 میلی ثانیه بود.

بعد از:

تصویری از پانل عملکرد پس از بهینه سازی در تابع appendEventAtLevel. کل زمان اجرای این تابع 207.2 میلی ثانیه بود.

گروه فعالیت چهارم: به تعویق انداختن کارهای غیر بحرانی و داده های کش برای جلوگیری از کارهای تکراری

با بزرگنمایی این پنجره، می توان دید که دو بلوک تقریباً یکسان از فراخوانی تابع وجود دارد. با نگاه کردن به نام توابع فراخوانی شده، می توانید استنباط کنید که این بلوک ها شامل کدهایی هستند که درختان را می سازند (به عنوان مثال، با نام هایی مانند refreshTree یا buildChildren ). در واقع کد مربوطه کدی است که نمای درختی را در کشوی پایین پنل ایجاد می کند. جالب اینجاست که این نماهای درختی بلافاصله پس از بارگذاری نشان داده نمی شوند. درعوض، کاربر باید یک نمای درختی (برگه‌های «پایین به بالا»، «درخت فراخوان» و «گزارش رویداد» در کشو) را برای نمایش درخت‌ها انتخاب کند. علاوه بر این، همانطور که از اسکرین شات می توانید متوجه شوید، فرآیند درخت سازی دو بار اجرا شد.

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

دو مشکل وجود دارد که ما با این تصویر شناسایی کردیم:

  1. یک کار غیر بحرانی مانع از عملکرد زمان بارگذاری بود. کاربران همیشه به خروجی آن نیاز ندارند. به این ترتیب، این کار برای بارگذاری نمایه حیاتی نیست.
  2. نتیجه این کارها در حافظه پنهان ذخیره نشد. به همین دلیل است که درختان با وجود تغییر نکردن داده ها، دو بار محاسبه شدند.

ما با به تعویق انداختن محاسبه درخت به زمانی که کاربر به صورت دستی نمای درختی را باز کرد شروع کردیم. تنها در این صورت است که ارزش پرداخت بهای ایجاد این درختان را دارد. کل زمان اجرای این دوبار حدود 3.4 ثانیه بود، بنابراین به تعویق انداختن آن تفاوت قابل توجهی در زمان بارگذاری ایجاد کرد. ما همچنان به دنبال ذخیره این نوع وظایف نیز هستیم.

گروه فعالیت پنجم: در صورت امکان از سلسله مراتب فراخوانی پیچیده اجتناب کنید

با نگاهی دقیق به این گروه، مشخص شد که یک زنجیره تماس خاص به طور مکرر فراخوانی شده است. همین الگو 6 بار در نقاط مختلف نمودار شعله ظاهر شد و مدت زمان کل این پنجره حدود 2.4 ثانیه بود!

تصویری از پنل عملکرد که شش فراخوانی عملکرد مجزا را برای ایجاد مینیمپ ردیابی یکسان نشان می‌دهد، که هر کدام دارای پشته‌های تماس عمیق هستند.

کد مربوطه که چندین بار فراخوانی می شود، بخشی است که داده هایی را که قرار است در "مینیمپ" رندر شوند (نمای کلی فعالیت خط زمانی در بالای پانل) پردازش می کند. معلوم نبود چرا چندین بار اتفاق می‌افتد، اما مطمئناً نباید ۶ بار اتفاق می‌افتد! در واقع، اگر پروفایل دیگری بارگذاری نشود، خروجی کد باید جاری باقی بماند. در تئوری، کد باید فقط یک بار اجرا شود.

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

تصویری از پنل عملکرد که شش تابع مجزا را نشان می‌دهد برای ایجاد یک مینیمپ یک ردیابی تنها به دو برابر کاهش یافته است.

توجه داشته باشید که اجرای رندر minimap دو بار اتفاق می افتد نه یک بار. این به این دلیل است که برای هر نمایه دو نقشه کوچک ترسیم می شود: یکی برای نمای کلی در بالای پانل، و دیگری برای منوی کشویی که نمایه قابل مشاهده فعلی را از تاریخ انتخاب می کند (هر آیتم در این منو شامل نمای کلی از نمایه ای که انتخاب می کند). با این وجود، این دو دقیقاً محتوای مشابهی دارند، بنابراین باید بتوان از یکی برای دیگری استفاده مجدد کرد.

از آنجایی که این مینی مپ ها هر دو تصاویری هستند که روی بوم کشیده شده اند، باید از ابزار drawImage canvas استفاده کرد و سپس کد را فقط یک بار اجرا کرد تا در زمان اضافی صرفه جویی شود. در نتیجه این تلاش، مدت زمان گروه از 2.4 ثانیه به 140 میلی ثانیه کاهش یافت.

نتیجه

پس از اعمال همه این اصلاحات (و چند مورد کوچکتر دیگر اینجا و آنجا)، تغییر جدول زمانی بارگیری نمایه به صورت زیر به نظر می رسد:

قبل از:

تصویری از پانل عملکرد که بارگذاری ردیابی را قبل از بهینه سازی نشان می دهد. این فرآیند تقریباً ده ثانیه طول کشید.

بعد از:

تصویری از پانل عملکرد که بارگذاری ردیابی را پس از بهینه سازی نشان می دهد. این فرآیند اکنون تقریباً دو ثانیه طول می کشد.

زمان بارگذاری پس از بهبودها 2 ثانیه بود، به این معنی که با تلاش نسبتاً کم ، بهبودی حدود 80 درصد حاصل شد، زیرا بیشتر کارهای انجام شده شامل رفع سریع بود. البته، تشخیص درست کارهایی که در ابتدا باید انجام شود، کلید اصلی بود، و پنل perf ابزار مناسبی برای این کار بود.

همچنین مهم است که مشخص شود این اعداد مختص پروفایلی هستند که به عنوان موضوع مطالعه استفاده می شود. نمایه برای ما جالب بود زیرا به خصوص بزرگ بود. با این وجود، از آنجایی که خط لوله پردازش برای هر پروفایل یکسان است، بهبود قابل توجهی که به دست آمده برای هر پروفایل بارگذاری شده در پانل perf اعمال می شود.

غذای آماده

در مورد بهینه سازی عملکرد برنامه شما درس هایی وجود دارد که می توان از این نتایج گرفت:

1. از ابزارهای پروفایل برای شناسایی الگوهای عملکرد زمان اجرا استفاده کنید

ابزارهای نمایه سازی برای درک آنچه در برنامه شما در حال اجراست، به ویژه برای شناسایی فرصت هایی برای بهبود عملکرد، بسیار مفید هستند. پانل عملکرد در Chrome DevTools یک گزینه عالی برای برنامه‌های کاربردی وب است زیرا ابزار نمایه‌سازی وب بومی در مرورگر است و فعالانه برای به‌روز بودن با آخرین ویژگی‌های پلتفرم وب حفظ می‌شود. همچنین، در حال حاضر به طور قابل توجهی سریعتر است! 😉

از نمونه هایی استفاده کنید که می توانند به عنوان بار کاری نماینده استفاده شوند و ببینید چه چیزی می توانید پیدا کنید!

2. از سلسله مراتب فراخوانی پیچیده اجتناب کنید

در صورت امکان، از پیچیده کردن بیش از حد نمودار تماس خودداری کنید. با سلسله مراتب فراخوانی پیچیده، معرفی رگرسیون‌های عملکرد آسان است و درک اینکه چرا کد شما به روشی که هست اجرا می‌شود، دشوار است، و این امر باعث می‌شود تا بهبودها را سخت کند.

3. کارهای غیر ضروری را شناسایی کنید

معمولاً پایگاه‌های کد قدیمی حاوی کدهایی هستند که دیگر مورد نیاز نیستند. در مورد ما، کدهای قدیمی و غیر ضروری بخش قابل توجهی از کل زمان بارگذاری را می گرفتند. برداشتن آن کم آویزترین میوه بود.

4. از ساختارهای داده به طور مناسب استفاده کنید

از ساختارهای داده برای بهینه‌سازی عملکرد استفاده کنید، اما همچنین هزینه‌ها و معاوضه‌هایی را که هر نوع ساختار داده در هنگام تصمیم‌گیری برای استفاده به همراه دارد را درک کنید. این فقط پیچیدگی فضایی خود ساختار داده نیست، بلکه پیچیدگی زمانی عملیات قابل اجرا نیز هست.

5. نتایج را در حافظه پنهان نگه دارید تا از کارهای تکراری برای عملیات پیچیده یا تکراری جلوگیری کنید

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

6. کار غیر انتقادی را به تعویق بیندازید

اگر خروجی یک کار فوراً مورد نیاز نیست و اجرای کار در حال گسترش مسیر بحرانی است، با فراخوانی تنبلی آن را در زمانی که خروجی آن واقعاً مورد نیاز است، به تعویق بیندازید.

7. از الگوریتم های کارآمد در ورودی های بزرگ استفاده کنید

برای ورودی‌های بزرگ، الگوریتم‌های پیچیدگی زمانی بهینه بسیار مهم هستند. ما در این مثال به این مقوله توجه نکردیم، اما اهمیت آنها به سختی قابل اغراق است.

8. امتیاز: خطوط لوله خود را معیار قرار دهید

برای اطمینان از اینکه کد در حال تکامل شما سریع باقی می ماند، عاقلانه است که رفتار را کنترل کرده و آن را با استانداردها مقایسه کنید. به این ترتیب، شما به طور فعال رگرسیون ها را شناسایی می کنید و قابلیت اطمینان کلی را بهبود می بخشید، و شما را برای موفقیت بلندمدت آماده می کنید.