كيف سرّعنا عمليات تتبُّع تسلسل استدعاء الدوال البرمجية في "أدوات مطوري البرامج في Chrome" بمقدار 10 مرات

بينيديكت مورير
بينيديكت ميرير

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

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

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

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

وقت تنفيذ عارض Chrome

على الرغم من أنّنا توقّعنا إلى حدٍّ ما رؤية أمر يتعلّق بجمع بيانات تتبُّع تسلسل استدعاء الدوال البرمجية، لم نكن نتوقع أن يتم توجيه حوالي 90% من إجمالي وقت التنفيذ إلى ترميز إطارات تسلسل استدعاء الدوال البرمجية. تشير الرموز هنا إلى عملية تحليل أسماء الدوال ومواضع المصدر الملموسة - أرقام الأسطر والأعمدة في النصوص البرمجية - من إطارات المكدس الأولية.

استنتاج اسم الطريقة

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

  1. خصائص البيانات التي تمثل value إغلاق func، أو
  2. خصائص الموصّل حيث يساوي get أو set إغلاق func.

الآن بينما لا يبدو هذا في حد ذاته رخيصًا للغاية، إلا أنه لا يبدو أيضًا كما لو كان يفسر هذا التباطؤ الفظيع. لذلك بدأنا البحث في المثال الذي تم الإبلاغ عنه في chromium:1069425، ووجدنا أنّه تم جمع عمليات تتبُّع تسلسل استدعاء الدوال البرمجية لمهام غير متزامنة، بالإضافة إلى رسائل السجلّ الناشئة من classes.js، وهو ملف JavaScript بحجم 10 ميبيبايت. وكشفت نظرة فاحصة أن هذا كان أساسًا وقت تشغيل Java بالإضافة إلى رمز تطبيق تم تجميعه إلى JavaScript. تضمّنت عمليات تتبُّع تسلسل استدعاء الدوال البرمجية عدة إطارات تم استدعاؤها في الكائن A، لذا رأينا أنّ الأمر قد يكون مفيدًا للتعرّف على نوع الكائن الذي نتعامل معه.

عمليات تتبع تسلسل استدعاء الدوال البرمجية لكائن

من الواضح أنّ المحول البرمجي من Java إلى JavaScript قد أنشأ كائنًا واحدًا يحتوي على 82,203 دوال، وكان من الواضح أنّ ذلك بدأ مثيرًا للاهتمام. بعد ذلك، عُدنا إلى JSStackFrame::GetMethodName() في V8 لكي نفهم ما إذا كانت هناك بعض الفاكهة التي يمكن أن نقع فيها على الأرض أم لا.

  1. وتعمل هذه الميزة من خلال البحث أولاً عن "name" للدالة باعتبارها سمة في الكائن. وإذا تم العثور عليها، تتحقق من تطابق قيمة السمة مع الدالة.
  2. إذا لم يكن للدالة اسم أو كان للكائن خاصية مطابقة، فإنها تعود إلى البحث العكسي عن طريق اجتياز جميع خصائص الكائن ونماذجه الأولية.

في المثال السابق، تكون جميع الدوال مجهولة الهوية ولها سمات "name" فارغة.

A.SDV = function() {
   // ...
};

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

  1. استخراج أسماء جميع الخصائص القابلة للتعداد،
  2. يمكنك إجراء بحث عام عن خاصية لكل اسم، لاختبار ما إذا كانت قيمة الخاصية الناتجة تتطابق مع حالة الإغلاق التي كنا نبحث عنها.

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

وكانت النتيجة الثانية أكثر إثارة للاهتمام. على الرغم من أنّ الدوالّ كانت مجهولة المصدر من الناحية التقنية، فإنّ محرّك V8 قد سجّل ما نطلق عليه الاسم المستنتَج لتلك الدوال. بالنسبة إلى القيم الحرفية للدالة التي تظهر على الجانب الأيمن من المهام بالصيغة obj.foo = function() {...}، يحفظ المحلل اللغوي V8 القيمة "obj.foo" باعتباره اسمًا مستنتجًا للدالة الحرفية. وبالتالي، في حالتنا، يعني ذلك أنّه لم يكن لدينا الاسم الصحيح الذي يمكننا البحث عنه، إلا أنّ لدينا معلومات قريبة جدًا من الاسم: في المثال A.SDV = function() {...} أعلاه، كان لدينا الاسم "A.SDV" كما تم استنتاجه، ويمكننا استنتاج اسم الخاصية من خلال البحث عن النقطة الأخيرة، ثم البحث عن الموقع "SDV" في الكائن. وقد أدى ذلك إلى حل المشكلة في جميع الحالات تقريبًا، حيث استبدلت الاجتياز الكامل المكلف بالبحث عن موقع واحد. لقد أدخل هذان التحسينان كجزء من قيمة CL هذه، ما أدّى إلى انخفاض كبير في معدل التباطؤ في المثال الذي تم الإبلاغ عنه في chromium:1069425.

Error.stack

كان من الممكن أن نسميها هنا يومًا. ولكن كان هناك شيء مريب، نظرًا لأن DevTools لا تستخدم اسم الطريقة لإطارات التكدس. في الواقع، لا تتيح فئة v8::StackFrame في واجهة برمجة التطبيقات C++ إمكانية الوصول إلى اسم الطريقة. لذا كان من الخطأ أن نتصل بـ JSStackFrame::GetMethodName() في الأساس. بدلاً من ذلك، المكان الوحيد الذي نستخدم (ونعرض فيه) اسم الطريقة هو في واجهة برمجة تطبيقات تتبُّع تسلسل استدعاء الدوال البرمجية في JavaScript. لفهم هذا الاستخدام، يمكنك وضع المثال البسيط التالي error-methodname.js في الاعتبار:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

هنا لدينا دالة foo التي تم تثبيتها باسم "bar" في object. يؤدّي تشغيل هذا المقتطف في Chromium إلى النتائج التالية:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

نرى هنا اسم الطريقة على play: يظهر إطار المكدس الأعلى لاستدعاء الدالة foo على مثيل Object عبر الطريقة المسماة bar. لذلك، تستخدم سمة error.stack غير العادية السمة JSStackFrame::GetMethodName() بشكل كبير. وفي الواقع، تشير اختبارات الأداء أيضًا إلى أنّ التغييرات التي أجريناها أدّت إلى زيادة سرعة الموقع الإلكتروني بشكل كبير.

تسريع إجراءات المعايير الصغرى في StackTrace

بالنسبة إلى موضوع "أدوات مطوري البرامج في Chrome"، لا يبدو أنّ اسم الطريقة محسوبًا على الرغم من عدم استخدام error.stack هو أمر لا يبدو صحيحًا. يمكنك الاطّلاع على بعض المعلومات المفيدة: في العادة، كان للإصدار V8 آليتين مختلفتين لجمع بيانات تتبُّع تسلسل استدعاء الدوال البرمجية في واجهتَي برمجة التطبيقات المختلفتَين المذكورتَين أعلاه (وواجهة برمجة التطبيقات C++ v8::StackFrame وواجهة برمجة التطبيقات (API) لتتبُّع تسلسل استدعاء الدوال البرمجية في JavaScript). كان هناك طريقتان مختلفتان لتنفيذ الأمر (تقريبًا) كان عرضة للخطأ وغالبًا ما أدى إلى حدوث تناقضات وأخطاء، لذلك في أواخر عام 2018 بدأنا مشروعًا للاستقرار على معقد واحد لتسجيل تتبع تسلسل استدعاء الدوال البرمجية.

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

يؤدي هذا إلى تحسين الأداء بشكل عام، ولكن للأسف اتضح أن يكون مخالفًا إلى حد ما لكيفية استخدام كائنات واجهة برمجة التطبيقات C++ هذه في Chromium وأدوات مطوري البرامج. على وجه التحديد، قدّمنا فئة v8::internal::StackFrameInfo جديدة تتضمّن جميع المعلومات حول إطار التكديس الذي تم عرضه من خلال v8::StackFrame أو عبر error.stack، فدائمًا ما نحتسب المجموعة الفائقة من المعلومات المتوفّرة من خلال كلتا الواجهتَين، ما يعني أنّه بالنسبة إلى استخدامات v8::StackFrame (وخاصةً أدوات مطوّري البرامج)، سنحتسب أيضًا اسم الطريقة فور طلب أي معلومات عن إطار التكديس. اتضح أنّ أدوات مطوّري البرامج تطلب دائمًا معلومات المصدر والنص البرمجي على الفور.

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

أسماء الدوال

بعد تنفيذ عمليات إعادة الهيكلة المذكورة أعلاه، تم خفض النفقات العامة للرموز (الوقت المقضي في v8_inspector::V8Debugger::symbolize) إلى حوالي 15% من إجمالي وقت التنفيذ، وكان بإمكاننا أن نرى بشكل أوضح أين كان V8 يقضي وقتًا عند (جمع و) ترميز الإطارات للاستهلاك في أدوات مطوري البرامج.

تكلفة الترميز

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

الأمر الأكثر إثارة للاهتمام هو أنّ v8::StackFrame::GetFunctionName كان له ترتيب مدهش في جميع الملفات الشخصية التي نظرنا إليها. وبتعمق أكثر هنا أدركنا أن حساب الاسم الذي سنعرضه للدالة في إطار الحزمة في DevTools ليس مبررًا بدون داعٍ.

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

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

من خلال البحث السلبي عن "displayName" عن طريق الخطأ، تمت إزالة نصف تكلفة v8::StackFrame::GetFunctionName. ينتقل النصف الآخر إلى البحث العام عن سمة "name". لحسن الحظ، استخدمنا بعض المنطق لتجنب عمليات البحث المكلفة لخاصية "name" على مثيلات Function (بدون تعديل) والتي تم تقديمها في V8 منذ فترة بهدف جعل Function.prototype.bind() نفسها أسرع. لقد نقلنا عمليات التحقق اللازمة التي أتاحت لنا تخطّي البحث العام المكلف في المقام الأول، ما أدّى إلى عدم ظهور v8::StackFrame::GetFunctionName في أي ملفات شخصية فكّرنا فيها بعد الآن.

الخلاصة

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

ندرك أنّه ما زالت هناك العديد من التحسينات المحتمَلة. مثلاً، لا تزال هناك زيادة في النفقات العامة عند استخدام أخطاء MutationObserver، كما هو موضّح في chromium:1077657. وفي الوقت الحالي، عالجنا المشاكل الرئيسية. وقد نعاود التواصل في المستقبل لتحسين أداء عمليات تصحيح الأخطاء.

تنزيل قنوات المعاينة

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

التواصل مع فريق "أدوات مطوري البرامج في Chrome"

يمكنك استخدام الخيارات التالية لمناقشة الميزات والتغييرات الجديدة في المشاركة، أو أي موضوع آخر مرتبط بـ "أدوات مطوري البرامج".

  • يمكنك إرسال اقتراحات أو ملاحظات إلينا عبر crbug.com.
  • يمكنك الإبلاغ عن مشكلة في "أدوات مطوري البرامج" باستخدام خيارات إضافية   المزيد > مساعدة > الإبلاغ عن مشاكل في "أدوات مطوري البرامج" في "أدوات مطوري البرامج".
  • نشر تغريدة على @ChromeDevTools
  • يمكنك إضافة تعليقات على الميزات الجديدة في الفيديوهات على YouTube في "أدوات مطوري البرامج" أو الفيديوهات على YouTube التي تتضمّن نصائح حول أدوات مطوّري البرامج.