لوحة أداء أسرع بنسبة 400% من خلال الأداء

Andrés Olivares
Andrés Olivares
Nancy Li
Nancy Li

وبغض النظر عن نوع التطبيق الذي تعمل على تطويره، من الضروري تحسين أداء التطبيق وضمان سرعة تحميله وتوفير تفاعلات سلسة لتجربة المستخدم ونجاح التطبيق. وتتمثل إحدى طرق القيام بذلك في فحص نشاط أحد التطبيقات باستخدام أدوات التحليل لمعرفة ما يجري خفية أثناء تشغيله خلال فترة زمنية. تُعد لوحة الأداء في "أدوات مطوري البرامج" أداة رائعة لإنشاء ملفات تعريفية لتحليل أداء تطبيقات الويب وتحسينه. إذا كان تطبيقك يعمل في Chrome، سيقدّم لك نظرة عامة مرئية تفصيلية حول ما يفعله المتصفّح أثناء تنفيذ تطبيقك. يمكن أن يساعدك فهم هذا النشاط في تحديد الأنماط والعوائق والنقاط الفعالة للأداء التي يمكنك اتخاذ إجراء بشأنها لتحسين الأداء.

يمكنك الاطّلاع في المثال التالي على إرشادات حول استخدام لوحة الأداء.

إعداد وإعادة صياغة سيناريو إنشاء الملفات الشخصية

وضعنا هدفًا مؤخرًا لتحسين أداء لوحة الأداء. وعلى وجه الخصوص، أردنا تحميل أحجام كبيرة من بيانات الأداء بسرعة أكبر. ويحدث ذلك مثلاً عند إنشاء ملفات شخصية لعمليات طويلة الأمد أو معقدة أو تسجيل بيانات عالية الدقة. ولتحقيق ذلك، كانت أول حاجة إلى فهم كيفية أداء التطبيق وسبب أدائه بهذه الطريقة، وهو ما تم تحقيقه باستخدام أداة تحليل البيانات.

كما تعلم، إنّ "أدوات مطوري البرامج" هي تطبيق ويب. وبالتالي، يمكن تحليلها باستخدام لوحة الأداء. لإنشاء ملف شخصي لهذه اللوحة، يمكنك فتح "أدوات مطوري البرامج" ثم فتح نسخة افتراضية أخرى من "أدوات مطوري البرامج" مرتبطة بها. يُعرف هذا الإعداد في Google باسم DevTools-on-DevTools.

عندما يصبح الإعداد جاهزًا، يجب إعادة إنشاء السيناريو المطلوب تحليله وتسجيله. ولتجنُّب حدوث أي التباس، ستتم الإشارة إلى نافذة "أدوات مطوري البرامج" الأصلية باسم "مثيل أدوات مطوري البرامج الأول"، وستتم الإشارة إلى النافذة التي تفحص المثيل الأول باسم "المثيل الثاني من أدوات مطوّري البرامج".

لقطة شاشة لمثيل في "أدوات مطوري البرامج" يفحص العناصر في "أدوات مطوري البرامج" نفسه
أدوات مطوّري البرامج: فحص "أدوات مطوّري البرامج" باستخدام "أدوات مطوري البرامج"

في المثال الثاني على "أدوات مطوري البرامج"، تلاحظ لوحة الأداء (التي سيُطلق عليها لوحة الأداء من الآن فصاعدًا) المثيل الأول من "أدوات مطوري البرامج" لإعادة إنشاء السيناريو، الذي يحمِّل ملفًا شخصيًا.

في الحالة الثانية من "أدوات مطوري البرامج"، يبدأ التسجيل المباشر، بينما في الحالة الأولى، يتم تحميل ملف شخصي من ملف على القرص. يتم تحميل ملف كبير الحجم بهدف تحليل أداء معالجة الإدخالات الكبيرة بدقة. عند انتهاء التحميل في كلتا المثيلات، تظهر بيانات تحليل الأداء، المعروفة عادةً باسم تتبُّع الأداء، في المثيل الثاني من "أدوات مطوري البرامج" للوحة الأداء التي تُحمِّل ملفًا شخصيًا.

الحالة الأولية: تحديد فرص التحسين

بعد انتهاء التحميل، تم رصد ما يلي في لقطة الشاشة التالية على مثيل لوحة الأداء الثاني. ركِّز على نشاط سلسلة المحادثات الرئيسية التي تظهر ضمن المسار الرئيسي. يمكن ملاحظة أن هناك خمس مجموعات كبيرة من الأنشطة في مخطط اللهب. وهي تتألف من المهام التي يستغرق فيها التحميل معظم الوقت. كان الوقت الإجمالي لهذه المهام حوالي 10 ثوانٍ. في لقطة الشاشة التالية، يتم استخدام لوحة الأداء للتركيز على كل مجموعة من مجموعات الأنشطة هذه لمعرفة ما يمكن العثور عليه.

لقطة شاشة للوحة الأداء في "أدوات مطوري البرامج" وهي تفحص تحميل بيانات تتبُّع الأداء في لوحة الأداء لمثيل آخر من "أدوات مطوري البرامج" يستغرق تحميل الملف الشخصي حوالي 10 ثوانٍ. يتم تقسيم هذا الوقت في الغالب على خمس مجموعات رئيسية من الأنشطة.

مجموعة النشاط الأول: عمل غير ضروري

أصبح واضحًا أن المجموعة الأولى من الأنشطة كانت رمزًا برمجيًا قديمًا لا يزال قيد التشغيل، ولكنه لم يكن ضروريًا حقًا. في الأساس، كل شيء تحت الكتلة الخضراء المسماة processThreadEvents تم إهدار الجهد. كان ذلك فوزًا سريعًا. وفّرت إزالة استدعاء الدالة هذا حوالي 1.5 ثانية من الوقت. رائع!

مجموعة النشاط الثانية

في مجموعة النشاط الثانية، لم يكن الحل بسيطًا مثل الحل الأول. استغرقت عملية buildProfileCalls حوالى 0.5 ثانية، ولم يكن من الممكن تجنُّب هذه المَهمّة.

لقطة شاشة للوحة الأداء في "أدوات مطوري البرامج" تفحص مثيلاً آخر من لوحة الأداء تستغرق إحدى المهام المرتبطة بوظيفة BuildProfileCalls حوالي 0.5 ثانية.

بدافع الفضول، تم تفعيل خيار الذاكرة في لوحة الأداء لإجراء مزيد من التحقيق، ووجدنا أنّ نشاط buildProfileCalls كان يستخدم أيضًا مساحة كبيرة من الذاكرة. هنا، يمكنك رؤية كيفية تحرك الرسم البياني الخطي الأزرق بشكل مفاجئ في وقت تشغيل buildProfileCalls، ما يشير إلى تسرُّب محتمل للذاكرة.

لقطة شاشة لأداة تحليل الذاكرة في "أدوات مطوري البرامج" تقيّم استهلاك الذاكرة في لوحة الأداء. يشير الفحص إلى أن وظيفة BuildProfileCalls هي المسؤولة عن تسرُّب الذاكرة.

لمتابعة هذا الشك، استخدمنا لوحة الذاكرة (Memory) (لوحة أخرى في "أدوات مطوري البرامج" تختلف عن درج "الذاكرة" في لوحة الأداء)) للتحقق من الأمر. في لوحة الذاكرة، يعرض قسم "عينة التخصيص" تم اختيار نوع تحليل البيانات، الذي سجّل لقطة لأجزاء من لوحة الأداء تُحمِّل ملف وحدة المعالجة المركزية (CPU).

لقطة شاشة للحالة الأولية لأداة تحليل الذاكرة. "عينة التخصيص" بمربع أحمر، ويشير إلى أن هذا الخيار هو الأفضل لتحليل ذاكرة 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 مرات! وفي الواقع، يجب أن يظل ناتج الرمز محدّثًا في حال عدم تحميل أي ملف شخصي آخر. من الناحية النظرية، يجب تشغيل التعليمة البرمجية مرة واحدة فقط.

عند التحقيق في الأمر، تبيَّن أنّه تم طلب الرمز ذي الصلة نتيجة لعدة أجزاء في مسار التحميل بشكل مباشر أو غير مباشر استدعاء الدالة التي تحتسب الخريطة المصغّرة. وذلك لأن تعقيد الرسم البياني للاتصالات قد تطور بمرور الوقت، وإضافة المزيد من التبعيات إلى هذه التعليمة البرمجية دون علم. ليس هناك حل سريع لهذه المشكلة. تعتمد طريقة حله على بنية قاعدة الأكواد المعنية. في حالتنا، كان علينا تقليل مدى تعقيد التسلسل الهرمي للاستدعاءات قليلاً وإضافة فحص لمنع تنفيذ الرمز في حال ظلّت البيانات المُدخلة بدون تغيير. بعد تنفيذ ذلك، حصلنا على هذه المظهر للجدول الزمني:

لقطة شاشة للوحة الأداء تعرض استدعاءات الدوال الستة المنفصلة لإنشاء الخريطة المصغّرة نفسها للتتبُّع إلى مرتين فقط.

يُرجى العِلم أنّ تنفيذ عرض الخريطة المصغّرة يحدث مرّتين، وليس مرّة واحدة. وذلك لأن هناك خريطتين مصغّرتين يتم رسمهما لكل ملف شخصي: إحداهما للنظرة العامة أعلى اللوحة، والأخرى للقائمة المنسدلة التي تحدد الملف الشخصي المرئي حاليًا من السجل (يحتوي كل عنصر في هذه القائمة على نظرة عامة على الملف الشخصي الذي يحدده). على الرغم من ذلك، يتضمّن أحدهما المحتوى نفسه، لذا يجب إعادة استخدام أحدهما للآخر.

بما أنّ هاتين الخريطتَين المصغَّرتَين هما صورتَين مرسومتَين على لوحة رسم، كان الأمر يعتمد على استخدام drawImage أداة لوحة الرسم، وبالتالي تشغيل الرمز مرة واحدة فقط لتوفير بعض الوقت الإضافي. ونتيجةً لهذه الجهود، تم تقليل مدة المجموعة من 2.4 ثانية إلى 140 مللي ثانية.

الخاتمة

بعد تطبيق جميع هذه الإصلاحات (وبعض الحلول الأخرى الأصغر هنا وهناك)، بدا تغيير المخطط الزمني لتحميل الملف الشخصي على النحو التالي:

قبل:

لقطة شاشة للوحة الأداء تعرض عملية التحميل قبل إجراء التحسينات. استغرقت العملية حوالي عشر ثوانٍ.

بعد:

لقطة شاشة للوحة الأداء تعرض عملية تحميل التتبُّع بعد إجراء التحسينات وتستغرق هذه العملية الآن حوالي ثانيتَين.

كان وقت التحميل بعد إجراء التحسينات ثانيتَين، ما يعني أنّ تحسُّنًا بنسبة%80 تقريبًا تم تحقيقه بجهد منخفض نسبيًا، لأنّ معظم ما تم إنجازه كان يتضمّن إصلاحات سريعة. بالطبع، كان تحديد الإجراءات المطلوب تنفيذها في البداية على النحو الصحيح، وكانت لوحة الأداء الأداة المناسبة لذلك.

من المهم أيضًا تسليط الضوء على أن هذه الأرقام خاصة بالملف الشخصي المستخدم كموضوع للدراسة. كان الملف الشخصي مثيرًا للاهتمام بالنسبة إلينا لأنه كان كبيرًا بشكل خاص. ومع ذلك، نظرًا لأن مسار المعالجة هو نفسه لكل ملف شخصي، ينطبق التحسين الملحوظ الذي تم تحقيقه على كل ملف شخصي يتم تحميله في لوحة الأداء.

الخلاصات

وفي ما يلي بعض الدروس المستخلَصة من هذه النتائج في ما يتعلق بتحسين أداء تطبيقك:

1. الاستفادة من أدوات التحليل لتحديد أنماط الأداء في بيئة التشغيل

تعد أدوات إنشاء الملفات التعريفية مفيدة للغاية لفهم ما يحدث في تطبيقك أثناء تشغيله، خاصةً لتحديد الفرص المتاحة لتحسين الأداء. تُعد لوحة الأداء ضمن "أدوات مطوري البرامج في Chrome" خيارًا رائعًا لتطبيقات الويب، نظرًا لكونها أداة تحليل بيانات الويب الأصلية في المتصفح، ويتم الحفاظ على تحديثها باستمرار باستخدام أحدث ميزات النظام الأساسي للويب. بالإضافة إلى ذلك، أصبحت الآن أسرع بكثير! 😉

استخدم النماذج التي يمكن استخدامها كأعباء عمل تمثيلية واكتشف ما يمكنك العثور عليه!

2. تجنُّب التسلسلات الهرمية للاستدعاءات المعقّدة

عند الإمكان، تجنَّب جعل الرسم البياني للاتصال معقدًا للغاية. مع التسلسلات الهرمية المعقدة للاستدعاءات، من السهل إدخال حالات تراجع في الأداء ويصعب فهم سبب تشغيل الرمز الخاص بك كما هي، مما يجعل من الصعب إجراء التحسينات.

3- تحديد العمل غير الضروري

من الشائع أن تحتوي قواعد الرموز القديمة على رموز لم تعد لازمة. في حالتنا، كانت الرموز القديمة وغير الضرورية تأخذ جزءًا كبيرًا من إجمالي مدّة التحميل. وكانت إزالتها من الفاكهة الأقل تعليقًا.

4. استخدام هياكل البيانات بشكل مناسب

استخدم هياكل البيانات لتحسين الأداء، ولكن عليك أيضًا فهم التكاليف والمقايضات التي يجلبها كل نوع من أنواع هياكل البيانات عند تحديد الهياكل التي يجب استخدامها. وهذا ليس فقط تعقيد المساحة لهيكل البيانات نفسه، ولكنه أيضًا التعقيد الزمني للعمليات السارية.

5- تخزين النتائج في ذاكرة التخزين المؤقت لتجنُّب العمل المكرّر للعمليات المعقّدة أو المتكرّرة

إذا كان تنفيذ العملية مكلفًا، من المنطقي تخزين نتائجها لوقت الحاجة إليها في المرة التالية. من المنطقي أيضًا القيام بذلك إذا تم إجراء العملية عدة مرات - حتى إذا لم تكن كل مرة على حدة مكلفة بشكل خاص.

6- تأجيل العمل غير المهم

إذا لم تكن نتيجة المهمة مطلوبة على الفور وكان تنفيذ المهمة يؤدّي إلى تمديد المسار الحرج، يمكنك تأجيلها من خلال تسميتها الكسولة إذا كانت هناك حاجة إلى مخرجاتها فعليًا.

7. استخدام خوارزميات فعالة على المدخلات الكبيرة

بالنسبة إلى المدخلات الكبيرة، تصبح خوارزميات التعقيد الزمني الأمثل أمرًا بالغ الأهمية. لم ننظر في هذه الفئة في هذا المثال، ولكن لا يمكن المبالغة في أهميتها.

8. ميزة إضافية: تحديد مسار التعلّم

للتأكّد من أنّ الرمز الذي يتم تطويره باستمرار، من الضروري مراقبة السلوك ومقارنته بالمعايير. وبهذه الطريقة، يمكنك تحديد الانحدار بشكل استباقي وتحسين الموثوقية بشكل عام، مما يؤدي إلى نجاحك على المدى الطويل.