استخدِم الطب الشرعي والعمل الاستقصائي لحل ألغاز تحليل أداء JavaScript

مقدمة

في السنوات الأخيرة، تسارعت تطبيقات الويب بشكل كبير. يتم تشغيل العديد من التطبيقات الآن بسرعة كافية لدرجة أنني سمعت بعض المطورين يتساءلون بصوت عالٍ، "هل الويب سريع بما فيه الكفاية؟" بالنسبة إلى بعض التطبيقات، قد تكون هذه العملية مناسبة للمطوّرين الذين يعملون على تطوير تطبيقات عالية الأداء، ولكنّنا نعلم أنّ هذه السرعة ليست كافية. على الرغم من التطورات المذهلة في تكنولوجيا الأجهزة الافتراضية المستندة إلى JavaScript، أظهرت دراسة حديثة أنّ تطبيقات Google تقضي ما بين 50% و70% من وقتها في استخدام V8. مدة تطبيقك محدودة، في حين أنّ نظامًا آخر غيره يمكنه تنفيذ المزيد من المهام من خلال الاستعانة بدورات الحلاقة من أحد الأنظمة. تذكر أن التطبيقات التي يتم تشغيلها بسرعة 60 لقطة في الثانية لا تزيد مدتها عن 16 ملي ثانية لكل إطار، أو يمكن استبعاد البيانات مؤقتًا. تابع القراءة لمعرفة كيفية تحسين تطبيقات JavaScript وJavaScript للملف الشخصي، في قصة من خُطاف محققي الأداء في فريق V8 الذين يتتبّعون مشكلة غامضة في الأداء في Find Your Way to Oz.

جلسة Google I/O 2013

قدّمتُ هذه المادة في مؤتمر Google I/O لعام 2013. يمكنك مشاهدة الفيديو أدناه:

ما هي أهمية الأداء؟

فدورات وحدة المعالجة المركزية (CPU) هي لعبة تساوي الصفر. إن تقليل استخدام جزء من النظام الخاص بك يسمح لك باستخدام جزء أكبر في جزء آخر أو تشغيله بشكل أكثر سلاسة بشكل عام. غالبًا ما تتنافس زيادة الأداءات التي تزيد من سرعتك على أداء تطبيقك، وتتطلب أيضًا ميزات جديدة، مع توقّع أن يتم تشغيل تطبيقك بسلاسة أكبر. تزداد سرعة الأجهزة الافتراضية المستندة إلى JavaScript بشكل مستمر، لكن هذا ليس سببًا لتجاهل مشاكل الأداء التي يمكنك حلّها في الوقت الحالي، كما يعرف العديد من المطوّرين حاليًا مشاكل الأداء في تطبيقات الويب الخاصة بهم. في الوقت الفعلي، وعند استخدام عدد مرتفع للقطات في الثانية، يكون الضغط على الشاشة لإخلاء مساحة تخزين أمرًا بالغ الأهمية. أجرت شركة Insomniac Games دراسة أثبتت أنّ استخدام عدد لقطات ثابت ومستدام في الثانية مهم لنجاح اللعبة: "يظل معدّل عرض اللقطات القوي دليلاً على أنّ المنتج احترافي ومصنوع بشكل جيد". يدوّن مطورو الويب هذا الأمر.

حل مشاكل الأداء

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

V8 CSI: Oz

واجه فريق V8 مشكلة في الأداء لم يتمكن من حلها من تلقاء نفسه، كما تعامل معارفهم المذهلون في بناء برنامج Find Your Way to Oz مع فريق V8. من حين لآخر، يتجمّد أوز، مما يتسبب في توقف مؤقت. أجرى مطوّرو Oz بعض التحقيقات الأولية باستخدام لوحة المخطط الزمني في "أدوات مطوري البرامج في Chrome". أثناء الاطّلاع على استخدام الذاكرة، واجهوا رسمًا بيانيًا مخيفًا لـ سن المنشار. مرة كل ثانية، كان جهاز تجميع البيانات المهملة يجمع 10 ميغابايت من البيانات المهملة ويقابل الإيقاف المؤقت لتجميع البيانات المهملة النفايات. على غرار لقطة الشاشة التالية من "المخطّط الزمني" في "أدوات مطوري البرامج في Chrome":

المخطط الزمني لأدوات مطوّري البرامج

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

الدليل

الخطوة الأولى هي جمع ودراسة الأدلة الأولية.

ما نوع التطبيق الذي ننظر إليه؟

عرض Oz التجريبي هو تطبيق ثلاثي الأبعاد تفاعلي. ولهذا السبب، يعد الإيقاف المؤقت حساسًا جدًا للتوقفات التي تسببها تجميع النفايات. يُرجى تذكُّر أنّ التطبيق التفاعلي الذي يتم تشغيله بسرعة 60 لقطة في الثانية يكون لديه 16 ملي ثانية للقيام بجميع أعمال JavaScript ويجب أن يترك Chrome بعض الوقت كي يعالج Chrome استدعاءات الرسومات ورسم الشاشة.

يجري "أوز" العديد من العمليات الحسابية على القيم المزدوجة ويُجري اتصالات متكررة باستخدام Web Audio وWebOS.

ما نوع مشكلة الأداء التي نواجهها؟

نلاحظ فترات توقف مؤقتًا وتُعرف أيضًا بانخفاض عدد اللقطات في الثانية. ترتبط هذه الوقفات بإجراءات جمع البيانات المهملة.

هل يتبع المطوّرون أفضل الممارسات؟

نعم، لدى مطوّري Oz خبرة واسعة في استخدام الأجهزة الافتراضية في JavaScript وأساليب التحسين. من الجدير بالذكر أنّ مطوّرو Oz كانوا يستخدمون لغة CoffeeScript كلغة مصدرهم وينتجون رمز JavaScript عبر برنامج التجميع من CoffeeScript. وقد جعل هذا من التحقيق أكثر تعقيدًا بسبب الفصل بين الرمز البرمجي الذي يكتبه مطوّرو Oz والرمز الذي يستخدمه V8. تتوافق "أدوات مطوري البرامج في Chrome" الآن مع خرائط المصادر التي كانت ستسهّل هذه العملية.

لماذا تعمل وحدة تجميع البيانات غير المرغوب فيها؟

تتم إدارة الذاكرة في JavaScript تلقائيًا للمطوّر من خلال الجهاز الافتراضي. يستخدم V8 نظامًا شائعًا لجمع البيانات المهملة حيث يتم تقسيم الذاكرة إلى generations (أو أكثر). يمتلك الجيل الشاب عناصر تم تخصيصها مؤخرًا. إذا بقي أحد الكائنات على قيد الحياة لفترة كافية، فإنه ينتقل إلى الجيل القديم.

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

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

ذاكرة V8 الصغيرة

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

ومن البديهي أن تفهم أنّه في كل مرة يتم فيها تخصيص عنصر بشكل ضمني أو صريح (عبر طلب إلى عنصر جديد) أو [] أو {})، يقترب تطبيقك أكثر فأكثر من مجموعة البيانات غير المرغوب فيها وإيقاف التطبيق مؤقتًا.

هل من المتوقع حدوث نفايات في هذا التطبيق بمقدار 10 ميغابايت/ثانية؟

باختصار، لا. مطوِّر البرامج لا يتخذ أي إجراء لتوقُّع نقل بيانات غير سليمة تبلغ 10 ميغابايت في الثانية.

المشتبه بها

تكمن المرحلة التالية من التحقيق في تحديد المخاوف المحتملة ثم الحدّ منها.

المشتبه به الأول

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

المشتبه به الثاني

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

المشتبه به الثالث

العمليات الحسابية في رمز غير محسّن. في التعليمات البرمجية غير المُحسَّنة، ينتج عن جميع العمليات الحسابية تخصيص كائنات فعلية. على سبيل المثال، هذا المقتطف:

var a = p * d;
var b = c + 3;
var c = 3.3 * dt;
point.x = a * b * c;

يؤدي إلى إنشاء 5 عناصر HeapNumber. الثلاثة الأولى هي للمتغيرات، a وb وc. رابع القيمة المجهولة (a * b) والقيمة الخامسة تأتي من #4 * c؛ والقيمة الخامسة يتم تعيينها في النهاية إلى dot.x.

يُجري Oz آلاف هذه العمليات لكل إطار. إذا حدثت أي من هذه العمليات الحسابية في دوال لم يتم تحسينها أبدًا، فقد تكون هي سبب البيانات غير الصحيحة. لأن العمليات الحسابية في غير المحسَّنة تُخصص الذاكرة حتى للنتائج المؤقتة.

المشتبه به الرابع

تخزين رقم دقة مزدوج إلى خاصية ما. يجب إنشاء كائن HeapNumber لتخزين الرقم والخاصية التي تم تغييرها لتشير إلى هذا العنصر الجديد. جدير بالذكر أنّ تبديل الموقع بحيث يشير إلى HeapNumber لن يؤدّي إلى إنشاء بيانات غير صالحة. ومع ذلك، من الممكن أن يكون هناك العديد من أرقام الدقة المزدوجة التي يتم تخزينها كخصائص كائن. التعليمة البرمجية مليئة بعبارات مثل ما يلي:

sprite.position.x += 0.5 * (dt);

في الترميز المحسَّن، يتم ضمنيًا تخصيص عنصر HeapNumber جديد في كل مرة يتم فيها تخصيص قيمة محسوبة حديثًا x، ما يجعلنا نقترب من الإيقاف المؤقت لجمع البيانات غير المرغوب فيها.

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

المشتبه به 4 هو الاحتمالية.

الطب الشرعي

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

التجربة رقم 1

التحقق من الاشتباه رقم 3 (الحسابية داخل دوال غير محسّنة). يحتوي محرك JavaScript V8 على نظام تسجيل مدمج يمكن أن يوفر نظرة ثاقبة على ما يحدث وراء الكواليس.

بدءًا من Chrome الذي لا يعمل على الإطلاق، وإطلاق Chrome مع العلامات:

--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"

ثم إنهاء Chrome بالكامل سيؤدي إلى إنشاء ملف v8.log في الدليل الحالي.

لفهم محتوى v8.log، يجب تنزيل الإصدار v8 نفسه الذي يستخدمه Chrome (راجِع الإصدار:v8) وإنشاءه.

بعد إنشاء الإصدار 8 بنجاح، يمكنك معالجة السجلّ باستخدام أداة معالجة عمليات التدقيق:

$ tools/linux-tick-processor /path/to/v8.log

(استبدل نظام التشغيل mac أو Windows بـ linux بناءً على النظام الأساسي لديك). (يجب تشغيل هذه الأداة من دليل المصدر ذي المستوى الأعلى في الإصدار 8.)

يعرض معالج علامات التجزئة جدولاً يستند إلى النص لوظائف JavaScript التي سجّلت أكبر عدد من علامات التجزئة:

[JavaScript]:
ticks  total  nonlib   name
167   61.2%   61.2%  LazyCompile: *opt demo.js:12
 40   14.7%   14.7%  LazyCompile: unopt demo.js:20
 15    5.5%    5.5%  Stub: KeyedLoadElementStub
 13    4.8%    4.8%  Stub: BinaryOpStub_MUL_Alloc_Number+Smi
  6    2.2%    2.2%  Stub: BinaryOpStub_ADD_OverwriteRight_Number+Number
  4    1.5%    1.5%  Stub: KeyedStoreElementStub
  4    1.5%    1.5%  KeyedLoadIC:  {12}
  2    0.7%    0.7%  KeyedStoreIC:  {13}
  1    0.4%    0.4%  LazyCompile: ~main demo.js:30

يتضح لك أنّ لكل من Demo.js ثلاث وظائف، هي: الإعلام التلقائي، وعدم التفعيل، والوظيفة الرئيسية. تظهر علامة النجمة (*) بجانب الدوال المحسَّنة. ملاحظة: إنّ ميزة "تحسين الأداء" تم تحسينها وأنّ إلغاء تفعيلها غير محسَّن.

أداة أخرى مهمة في حقيبة أدوات محقق V8 هي حدث plot-timer-event. ويمكن تنفيذه على النحو التالي:

$ tools/plot-timer-event /path/to/v8.log

بعد التشغيل، يوجد ملف png يسمى current-events.png في الدليل الحالي. عند فتحه، من المفترض أن يظهر لك شيء على هذا النحو:

أحداث الموقّت

بخلاف الرسم البياني الذي يظهر في الأسفل، يتم عرض البيانات في صفوف. المحور س هو الوقت (بالمللي ثانية). يتضمن الجانب الأيمن تصنيفات لكل صف:

المحور "ص" لأحداث الموقّت

يحتوي الصف V8.Execute على خط عمودي أسود مرسوم عليه عند كل علامة ملف شخصي حيث كان V8 ينفذ رمز JavaScript. يحتوي V8.GCScavenger على خط رأسي أزرق مرسوم عليه عند كل علامة ملف شخصي حيث كان V8 ينفِّذ مجموعة الجيل الجديد. الشيء نفسه ينطبق على باقي ولايات V8.

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

نوع الرمز الذي يتم تنفيذه

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

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

عند الاطّلاع على مخطط أحداث الموقّت من رمز المصدر Oz، أظهرنا انتقالاً من الترميز المحسَّن إلى الترميز غير المحسَّن، وأثناء تنفيذ الرموز غير المحسَّنة، تم تشغيل العديد من مجموعات الجيل الجديد، على غرار لقطة الشاشة التالية (تمّت إزالة الملاحظة في المنتصف):

الرسم البياني لأحداث الموقّت

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

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

function updateSprites(dt) {
    for (var sprite in sprites) {
        sprite.position.x += 0.5 * dt;
        // 20 more lines of arithmetic computation.
    }
}

وبما أنّهم يطّلعون على استخدام V8 تمامًا، أدركوا على الفور أنّ بنية for-in- الخاصة بهم لا يتم تحسينها أحيانًا باستخدام V8. بمعنى آخر، إذا كانت الدالة تحتوي على بنية for-in-in، فقد لا يتم تحسينها. هذه حالة خاصة اليوم، ومن المحتمل أن تتغير في المستقبل، ويعني ذلك أن الإصدار V8 قد يحسّن إنشاء التكرار هذا يومًا ما. بما أننا لسنا من محققي V8 ولا نعرف V8 مثل أيدينا، كيف يمكننا تحديد سبب عدم تحسين برنامج updateSprites؟

التجربة رقم 2

تشغيل Chrome مع هذه العلامة:

--js-flags="--trace-deopt --trace-opt-verbose"

يعرض سجلاً مطوَّلًا لبيانات التحسين وإلغاء التحسين. عند البحث في البيانات عن updateSprites، نجد ما يلي:

[تم إيقاف التحسين لـ updateSprites، السبب: ForInStatement ليس حالة سريعة]

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

الطلب مغلق

بعد اكتشاف سبب عدم تحسين updateSprites، كان الحل بسيطًا، فما عليك سوى نقل العملية الحسابية إلى وظيفتها الخاصة، وهي:

function updateSprite(sprite, dt) {
    sprite.position.x += 0.5 * dt;
    // 20 more lines of arithmetic computation.
}

function updateSprites(dt) {
    for (var sprite in sprites) {
        updateSprite(sprite, dt);
    }
}

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

الخاتمة

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

انطلِق وابدأ في حل بعض الجرائم المتعلقة بالأداء!