یک پانل عملکرد 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. پاداش: خطوط لوله خود را معیار قرار دهید

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

،

آندرس اولیوارس
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. جایزه: خطوط لوله خود را معیار کنید

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

،

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

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

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

تنظیم و بازآفرینی سناریوی پروفایل ما

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

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

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

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

در نمونه دوم DevTools ، پانل عملکرد - که از اینجا به عنوان پانل به نام Permon استفاده می شود - اولین نمونه DevTools را برای بازآفرینی سناریو ، که یک پروفایل را بار می کند ، مشاهده می کند.

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

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

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

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

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

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

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

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

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

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

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

برای پیگیری این سوء ظن ، از پانل حافظه (پانل دیگری در DevTools ، متفاوت از کشو حافظه در پانل Perf) برای بررسی استفاده کردیم. در پانل حافظه ، نوع پروفایل "نمونه گیری تخصیص" انتخاب شد ، که عکس فوری پشته را برای پانل Perfer بارگیری پروفایل CPU ثبت کرد.

تصویری از وضعیت اولیه Memory Profiler. گزینه "نمونه گیری تخصیص" با یک جعبه قرمز برجسته شده است و نشان می دهد که این گزینه برای پروفایل حافظه JavaScript بهترین است.

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

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

از این عکس فوری ، مشاهده شد که کلاس 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 ساخته شده است. کل زمان اجرای عملکرد 1،372.51 میلی ثانیه بود.

بعد از:

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

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

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

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

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

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

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

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

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

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

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

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

تصویری از پانل عملکرد که شش عملکرد جداگانه را نشان می دهد برای تولید همان ردیابی حداقل به دو برابر کاهش می یابد.

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

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

نتیجه گیری

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

قبل از:

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

بعد از:

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

زمان بار پس از پیشرفت 2 ثانیه بود ، به این معنی که بهبود حدود 80 ٪ با تلاش نسبتاً کم حاصل شد ، زیرا بیشتر آنچه انجام شد شامل رفع سریع بود. البته ، شناسایی صحیح آنچه در ابتدا باید انجام شود مهم بود ، و پانل Perfore ابزار مناسبی برای این کار بود.

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

غذای آماده

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8. جایزه: خطوط لوله خود را معیار کنید

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

،

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

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

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

تنظیم و بازآفرینی سناریوی پروفایل ما

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

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

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

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

در نمونه دوم DevTools ، پانل عملکرد - که از اینجا به عنوان پانل به نام Permon استفاده می شود - اولین نمونه DevTools را برای بازآفرینی سناریو ، که یک پروفایل را بار می کند ، مشاهده می کند.

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

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

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

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

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

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

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

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

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

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

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

برای پیگیری این سوء ظن ، از پانل حافظه (پانل دیگری در DevTools ، متفاوت از کشو حافظه در پانل Perf) برای بررسی استفاده کردیم. در پانل حافظه ، نوع پروفایل "نمونه گیری تخصیص" انتخاب شد ، که عکس فوری پشته را برای پانل Perfer بارگیری پروفایل CPU ثبت کرد.

تصویری از وضعیت اولیه Memory Profiler. گزینه "نمونه گیری تخصیص" با یک جعبه قرمز برجسته شده است و نشان می دهد که این گزینه برای پروفایل حافظه JavaScript بهترین است.

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

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

از این عکس فوری ، مشاهده شد که کلاس 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 ساخته شده است. کل زمان اجرای عملکرد 1،372.51 میلی ثانیه بود.

بعد از:

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

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

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

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

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

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

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

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

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

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

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

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

تصویری از پانل عملکرد که شش عملکرد جداگانه را نشان می دهد برای تولید همان ردیابی حداقل به دو برابر کاهش می یابد.

توجه داشته باشید که اعدام ارائه دهنده Minimap دو بار اتفاق می افتد ، نه یک بار. This is because there are two minimaps being drawn for every profile: one for the overview on top of the panel, and another for the drop-down menu that selects the currently visible profile from the history (every item in this menu contains an overview of the profile it selects). Nonetheless, these two have the exact same content, so one should be able to reused for the other.

Since these minimaps are both images drawn on a canvas, it was a matter of using the drawImage canvas utility , and subsequently running the code only once to save some extra time. As a result of this effort, the group's duration was reduced from 2.4 seconds to 140 milliseconds.

نتیجه گیری

After having applied all these fixes (and a couple of other smaller ones here and there), the change of the profile loading timeline looked as follows:

قبل از:

A screenshot of the performance panel showing trace loading before optimizations. The process took approximately ten seconds.

بعد از:

A screenshot of the performance panel showing trace loading after optimizations. The process now takes approximately two seconds.

The load time after the improvements was 2 seconds, meaning that an improvement of about 80% was achieved with relatively low effort, since most of what was done consisted of quick fixes. Of course, properly identifying what to do initially was key, and the perf panel was the right tool for this.

It's also important to highlight that these numbers are particular to a profile being used as a subject of study. The profile was interesting to us because it was particularly large. Nonetheless, since the processing pipeline is the same for every profile, the significant improvement achieved applies to every profile loaded in the perf panel.

غذای آماده

There are some lessons to take away from these results in terms of performance optimization of your application:

1. Make use of profiling tools to identify runtime performance patterns

Profiling tools are incredibly useful to understand what's happening in your application while it's running, especially to identify opportunities to improve performance. The Performance panel in Chrome DevTools is a great option for web applications since it's the native web profiling tool in the browser, and it's actively maintained to be up-to-date with the latest web platform features. Also, it's now significantly faster! 😉

Use samples that can be used as representative workloads and see what you can find!

2. Avoid complex call hierarchies

When possible, avoid making your call graph too complicated. With complex call hierarchies, it's easy to introduce performance regressions and hard to understand why your code is running the way it is, making it hard to land improvements.

3. Identify unnecessary work

It's common for aging codebases to contain code that's no longer needed. In our case, legacy and unnecessary code was taking a significant portion of the total loading time. Removing it was the lowest-hanging fruit.

4. Use data structures appropriately

Use data structures to optimize performance, but also understand the costs and trade-offs each type of data structure brings when deciding which ones to use. This isn't only the space complexity of the data structure itself, but also time complexity of the applicable operations.

5. Cache results to avoid duplicate work for complex or repetitive operations

If the operation is costly to execute, it makes sense to store its results for the next time it's needed. It also makes sense to do this if the operation is done many times—even if each individual time isn't particularly costly.

6. Defer non-critical work

If the output of a task isn't needed immediately and the task execution is extending the critical path, consider deferring it by lazily calling it when its output is actually needed.

7. Use efficient algorithms on large inputs

For large inputs, optimal time complexity algorithms become crucial. We didn't look into this category in this example, but their importance can hardly be overstated.

8. Bonus: benchmark your pipelines

To make sure your evolving code remains fast, it's wise to monitor the behavior and compare it against standards. This way, you proactively identify regressions and improve the overall reliability, setting you up for long-term success.