مقدمة
في السنوات الأخيرة، تم تسريع تطبيقات الويب بشكل كبير. تعمل العديد من التطبيقات الآن بسرعة كافية لدرجة أنّني سمعت بعض المطوّرين يتساءلون بصوت عالٍ "هل الويب سريع بما يكفي؟". قد يكون ذلك صحيحًا لبعض التطبيقات، ولكن نعلم أنّه ليس سريعًا بما يكفي للمطوّرين الذين يعملون على تطبيقات عالية الأداء. على الرغم من التطورات المذهلة في تكنولوجيا الأجهزة الافتراضية لبرنامج JavaScript، أظهرت دراسة حديثة أنّ تطبيقات Google تقضي ما بين% 50 و% 70 من وقتها داخل V8. يملك تطبيقك وقتًا محدودًا، وبالتالي فإنّ تقليل دورات المعالجة في نظام معيّن يعني أنّ نظامًا آخر يمكنه إنجاز المزيد من المهام. تذكَّر أنّ التطبيقات التي تعمل بمعدّل 60 لقطة في الثانية تملك 16 ملي ثانية فقط لكل لقطة، وإلا سيحدث تقطُّع. اطّلِع على المزيد من المعلومات عن تحسين JavaScript وإنشاء ملفات تعريف لتطبيقات JavaScript، وذلك من خلال قصة حقيقية عن خبراء الأداء في فريق V8 الذين كانوا يتتبّعون مشكلة أداء غامضة في Find Your Way to Oz.
جلسة Google I/O 2013
وقدّمتُ هذه المادة في مؤتمر Google I/O لعام 2013. يمكنك الاطّلاع على الفيديو أدناه:
ما أهمية الأداء؟
دورات وحدة المعالجة المركزية هي لعبة صفرية. إنّ جعل جزء من نظامك يستخدم موارد أقل يتيح لك استخدام المزيد في جزء آخر أو تشغيله بشكل أكثر سلاسة بشكل عام. غالبًا ما يكون الأداء الأسرع وتنفيذ المزيد من المهام هدفَين متنافسَين. يطلب المستخدمون ميزات جديدة ويتوقعون أيضًا أن يعمل تطبيقك بسلاسة أكبر. تستمرّ الآلات الافتراضية لـ JavaScript في السرعة، ولكنّ هذا ليس سببًا لتجاهل مشاكل الأداء التي يمكنك حلّها اليوم، كما يعلم العديد من المطوّرين الذين يتعاملون مع مشاكل الأداء في تطبيقات الويب الخاصة بهم. في الوقت الفعلي، يكون عدد اللقطات في الثانية مرتفعًا، لذا من المهم جدًا أن تكون التطبيقات خالية من الانقطاعات. أجرت شركة Insomniac Games دراسة أظهرت أنّ معدّل عرض اللقطات الثابت والمستمر مهم لنجاح اللعبة: "إنّ معدّل عرض اللقطات الثابت لا يزال علامة على المنتج الاحترافي والمُعدّ بشكل جيد". يُرجى العِلم لمطوّري الويب.
حلّ مشاكل الأداء
يشبه حلّ مشكلة الأداء حلّ جريمة. عليك فحص الأدلة بعناية والتحقّق من الأسباب المُشتَبه بها وتجربة حلول مختلفة. يجب توثيق قياساتك طوال الوقت للتأكّد من أنّك أصلحت المشكلة فعلاً. لا يختلف هذا الأسلوب كثيرًا عن الطريقة التي يتّبعها المحققون الجنائيون لحلّ القضايا. يفحص المحققون الأدلة ويستجوبون المشتبه بهم ويُجريون التجارب على أمل العثور على الدليل الحاسم.
V8 CSI: Oz
تواصل فريق V8 مع فريق Find Your Way to Oz بشأن مشكلة في الأداء لم يتمكّنوا من حلّها بأنفسهم. في بعض الأحيان، كان تطبيق Oz يتوقّف عن العمل، ما يؤدي إلى حدوث تقطُّع في الأداء. أجرى مطوّرو Oz بعض التحقيقات الأولية باستخدام لوحة المخطط الزمني في أدوات مطوّري البرامج في Chrome. عند الاطّلاع على استخدام الذاكرة، صادفوا الرسم البياني المخيف سن المنشار. كان برنامج جمع المهملات يجمع 10 ميغابايت من المهملات مرة واحدة في الثانية، وكانت فترات التوقف في جمع المهملات تتزامن مع الارتباك. على غرار لقطة الشاشة التالية من "المخطط الزمني" في "أدوات مطوّري البرامج في Chrome":
تولّى المحققان في V8، "جاكوب" و"يانغ" التحقيق في هذه المشكلة. لقد حدثت مراسلات طويلة بين "جاكوب" و"يانغ" من فريق V8 وفريق Oz. لقد اختصرنا هذه المحادثة إلى الأحداث المهمة التي ساعدت في تتبُّع هذه المشكلة.
الدليل
الخطوة الأولى هي جمع الدليل الأوّلي ودراسة تفاصيله.
ما هو نوع الطلب الذي ننظر إليه؟
العرض التجريبي لتطبيق Oz هو تطبيق ثلاثي الأبعاد تفاعلي. ولهذا السبب، يكون حساسًا جدًا للتوقفات التي تحدث بسبب عمليات جمع المهملات. تذكَّر أنّ التطبيق التفاعلي الذي يعمل بمعدّل 60 لقطة في الثانية لديه 16 ملي ثانية لتنفيذ جميع مهام JavaScript، ويجب ترك بعض هذا الوقت لمعالجة Chrome لطلبات الرسومات ورسم الشاشة.
يُجري Oz الكثير من العمليات الحسابية على القيم المزدوجة ويُجري عمليات استدعاء متكررة لواجهة برمجة التطبيقات WebAudio وWebGL.
ما هو نوع مشكلة الأداء التي نرصدها؟
نلاحظ حدوث فواصل في البث، أي انخفاض في عدد اللقطات في الثانية، أو حدوث تقطُّع في البث. وترتبط هذه الفواصل الزمنية بعمليات جمع المهملات.
هل يلتزم المطوّرون بأفضل الممارسات؟
نعم، إنّ مطوّري Oz على دراية جيدة بأداء JavaScript VM وأساليب تحسينه. تجدر الإشارة إلى أنّ مطوّري Oz كانوا يستخدمون CoffeeScript كلغة مصدر وينشئون رمز JavaScript من خلال مجمّع CoffeeScript. وقد جعل ذلك بعض جوانب التحقيق أكثر تعقيدًا بسبب عدم التوافق بين الرمز البرمجي الذي يكتبه مطوّرو Oz والرمز البرمجي الذي يستخدِمه V8. تتيح "أدوات مطوّري البرامج في Chrome" الآن خرائط المصادر التي كانت ستسهّل هذا الإجراء.
لماذا يتم تشغيل أداة جمع المهملات؟
تدير الآلة الافتراضية الذاكرة في JavaScript تلقائيًا للمطوّر. يستخدم V8 نظامًا شائعًا لجمع المهملات يتم فيه تقسيم الذاكرة إلى جيلَين (أو أكثر). يحتفظ الجيل الجديد بالكائنات التي تم تخصيصها مؤخرًا. إذا ظلّ عنصر متوفّرًا لفترة طويلة بما يكفي، يتم نقله إلى الجيل القديم.
يتم جمع بيانات الجيل الجديد بمعدّل تكرار أعلى بكثير من معدّل جمع بيانات الجيل القديم. وهذا مقصود، لأنّ جمع البيانات من الجيل الأصغر سنًا أرخص بكثير. غالبًا ما يكون من الآمن افتراض أنّ الفواصل الزمنية المتكرّرة لجمع "مخطّط إدارة الذاكرة" (GC) ناتجة عن جمع الجيل الجديد.
في الإصدار 8، تنقسم مساحة الذاكرة الجديدة إلى كتلتين متتاليتين متساويتين من الذاكرة. لا يتم استخدام سوى جزء واحد من هذين الجزءَين من الذاكرة في أي وقت معيّن، ويُعرف باسم "مساحة التخزين المؤقت". عندما تتوفّر ذاكرة متبقية في المساحة "إلى"، يكون تخصيص عنصر جديد رخيصًا. يتم نقل المؤشر في مساحة "الوجهة" إلى الأمام بعدد وحدات البايت المطلوبة للكائن الجديد. ويستمر ذلك إلى أن يتم استخدام المساحة المتوفّرة. في هذه المرحلة، يتم إيقاف البرنامج ويبدأ جمع البيانات.
في هذه المرحلة، يتم تبديل المساحة "من" بالمساحة "إلى". يتم فحص المساحة التي كانت "المساحة المقصودة" والتي أصبحت الآن "المساحة المصدر" من البداية إلى النهاية، ويتم نسخ أي عناصر لا تزال نشطة إلى المساحة المقصودة أو ترقيتها إلى حزمة الجيل القديم. إذا أردت معرفة التفاصيل، نقترح عليك الاطّلاع على خوارزمية "تشيني".
من البديهي أنّك يجب أن تدرك أنّه في كل مرة يتم فيها تخصيص عنصر بشكل ضمني أو صريح (من خلال طلب new أو [] أو {})، يقترب تطبيقك أكثر فأكثر من عملية جمع المهملات ووقت التوقف المؤقت المخيف للتطبيق.
هل من المتوقّع أن يستهلك هذا التطبيق 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، ويتمّ تحديد القيمة الخامسة في نهاية المطاف لنقطة.س.
يُجري Oz آلاف عمليات المعالجة هذه لكل لقطة. إذا حدث أيّ من هذه العمليات الحسابية في دوالّ لم يتم تحسينها مطلقًا، قد تكون هي سبب ظهور البيانات غير الصالحة. لأنّ العمليات الحسابية في النموذج غير المحسَّن تُخصّص الذاكرة حتى للنتائج المؤقتة.
المشتبه به رقم 4
تخزين رقم بدقة مزدوجة في خاصيّة يجب إنشاء عنصر HeapNumber لتخزين الرقم وتغيير السمة للإشارة إلى هذا العنصر الجديد. لن يؤدي تغيير السمة للإشارة إلى HeapNumber إلى إنشاء بيانات غير صالحة. ومع ذلك، من المحتمل أن يكون هناك العديد من الأرقام ذات الدقة المزدوجة التي يتم تخزينها كسمات للعناصر. يحتوي الرمز البرمجي على عبارات مثل ما يلي:
sprite.position.x += 0.5 * (dt);
في الرمز المحسَّن، في كل مرة يتم فيها تعيين قيمة تم احتسابها حديثًا إلى x، وهي عبارة تبدو غير ضارة، يتم تخصيص عنصر HeapNumber جديد بشكل ضمني، ما يقربنا من فترة توقف جمع المهملات.
يُرجى العِلم أنّه باستخدام صفيف من النوع (أو صفيف عادي يحتوي على أعداد مزدوجة فقط)، يمكنك تجنُّب هذه المشكلة المحدّدة تمامًا لأنّ مساحة التخزين لعدد الدقة المزدوجة لا يتم تخصيصها إلا مرة واحدة، ولا يتطلّب تغيير القيمة بشكل متكرّر تخصيص مساحة تخزين جديدة.
المشتبه به رقم 4 هو احتمال.
الطب الشرعي
في هذه المرحلة، يتوقّع المحققون سببَين محتمَلَين: تخزين أرقام الحِزم كخصائص للعناصر وإجراء العمليات الحسابية داخل الدوالّ غير المحسَّنة. لقد حان الوقت للتوجه إلى المختبر وتحديد المتهم الذي ارتكب الجريمة بشكل نهائي. ملاحظة: في هذا القسم، سأستخدم نسخة من المشكلة التي تم العثور عليها في رمز مصدر Oz الفعلي. إنّ عملية إعادة الإنشاء هذه أصغر بكثير من الرمز الأصلي، وبالتالي من الأسهل فهمها.
التجربة 1
التحقّق من المُشتبه به رقم 3 (الحساب الحسابي داخل الدوالّ غير المحسَّنة) يحتوي محرك V8 JavaScript على نظام تسجيل مضمّن يمكنه تقديم إحصاءات رائعة عن تفاصيل العملية التي تنطوي عليها عروض أسعارك.
بدءًا من عدم تشغيل Chrome على الإطلاق، يمكنك تشغيل Chrome باستخدام العلامات:
--no-sandbox --js-flags="--prof --noprof-lazy --log-timer-events"
سيؤدي إغلاق Chrome بالكامل بعد ذلك إلى إنشاء ملف v8.log في الدليل الحالي.
لتفسير محتوى ملف v8.log، عليك تنزيل الإصدار نفسه من v8 الذي يستخدمه Chrome (اطّلِع على about:version)، وإنشاءه.
بعد إنشاء الإصدار 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 يتضمّن ثلاث دوال: opt وunopt وmain. تظهر علامة نجمية (*) بجانب أسماء الدوال المحسّنة. يُرجى ملاحظة أنّ الدالة opt محسّنة وأنّ الدالة unopt غير محسّنة.
ومن الأدوات المهمة الأخرى في مجموعة أدوات "محقّق V8" هي plot-timer-event. ويمكن تنفيذها على النحو التالي:
$ tools/plot-timer-event /path/to/v8.log
بعد التشغيل، يظهر ملف png باسم timer-events.png في الدليل الحالي. عند فتحه، من المفترض أن يظهر لك ما يلي:
بالإضافة إلى الرسم البياني في أسفل الصفحة، يتم عرض البيانات في صفوف. المحور السيني هو الوقت (بالمللي ثانية). يتضمّن الجانب الأيمن تصنيفات لكل صف:
يحتوي صف V8.Execute على خط عمودي أسود مرسوم عليه عند كل علامة ملف تعريف كان V8 ينفذ فيها رمز JavaScript. يتضمّن V8.GCScavenger خطًا عموديًا أزرقًا يتم رسمه عند كل علامة ملف شخصي كان V8 يُجري فيها عملية جمع جيل جديد. وينطبق الأمر نفسه على بقية حالات V8.
أحد أهم الصفوف هو "نوع الرمز البرمجي الذي يتم تنفيذه". سيظهر باللون الأخضر عند تنفيذ الرمز البرمجي المحسَّن، وسيظهر باللون الأحمر والأزرق عند تنفيذ الرمز البرمجي غير المحسَّن. تعرض لقطة الشاشة التالية عملية الانتقال من الرمز المحسّن إلى الرمز غير المحسّن ثم العودة إلى الرمز المحسّن:
من المفترض أن يظهر هذا الخط باللون الأخضر بالكامل، ولكن ليس على الفور. وهذا يعني أنّ برنامجك قد انتقل إلى حالة ثابتة محسّنة. سيعمل الرمز غير المحسَّن دائمًا بشكل أبطأ من الرمز المحسَّن.
إذا اتّبعت هذه الخطوات، يُرجى العِلم أنّه يمكنك العمل بشكل أسرع من خلال إعادة صياغة تطبيقك كي يتمكّن من التشغيل في shell لتصحيح أخطاء v8: d8. يمنحك استخدام d8 أوقات تكرار أسرع باستخدام أداتَي tick-processor وplot-timer-event. من الآثار الجانبية الأخرى لاستخدام 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-i-in بواسطة V8. بعبارة أخرى، إذا كانت الدالة تحتوي على بنية حلقة for-i-in، قد لا يتم تحسينها. هذا مثال خاصّ اليوم، ومن المرجّح أن يتغيّر في المستقبل، أي أنّه قد يُحسِّن V8 بنية حلقة التكرار هذه في يوم ما. بما أنّنا لسنا خبراء في V8 ولا نعرف V8 جيدًا، كيف يمكننا تحديد سبب عدم تحسين updateSprites؟
التجربة 2
تشغيل Chrome باستخدام هذه العلامة:
--js-flags="--trace-deopt --trace-opt-verbose"
تعرِض هذه السمة سجلّاً مفصّلاً لبيانات التحسين وإزالة التحسين. عند البحث في البيانات عن updateSprites، نجد ما يلي:
[disabled optimization for updateSprites, reason: ForInStatement is not fast case]
وكما افترض المحققون، كان السبب هو بنية حلقة for-i-in.
الطلب مغلق
بعد اكتشاف سبب عدم تحسين 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 بشكل كبير، ما يؤدي إلى تقليل فترات التوقف في جمع القمامة. من المفترض أن يكون من السهل عليك تأكيد ذلك من خلال إجراء التجارب نفسها باستخدام الرمز الجديد. سيلاحظ القارئ الدقيق أنّه لا يزال يتم تخزين الأرقام المزدوجة كخصائص. إذا أظهرت ميزة "التحليل الإحصائي" أنّ ذلك يستحقّ العناء، سيؤدي تغيير الموضع إلى أن يكون مصفوفة من القيم المزدوجة أو مصفوفة بيانات مُحدَّدة إلى تقليل عدد العناصر التي يتم إنشاؤها.
الخاتمة
لم يتوقف مطوّرو Oz عند هذا الحد. باستخدام الأدوات والأساليب التي شاركها معهم خبراء V8، تمكّنوا من العثور على بعض الدوالّ الأخرى التي كانت عالقة في جحيم إزالة التحسين، وقسموا رمز الحساب إلى دوالّ أساسية تم تحسينها، ما أدّى إلى تحسين الأداء بشكلٍ أكبر.
ننصحك بالخروج وبدء حلّ بعض مشاكل الأداء.