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

Benedikt Meurer
Benedikt Meurer

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

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

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

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

وقت تنفيذ Chrome Renderer

على الرغم من أنّنا توقّعنا إلى حدّ ما رؤية شيء مرتبط بجمع عمليات تتبُّع تسلسل استدعاء الدوال البرمجية، لم نتوقّع أن يتم تخصيص %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() في الإصدار 8 لمعرفة ما إذا كانت هناك بعض الميزات البسيطة التي يمكننا تحسينها.

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

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

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

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

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

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

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

Error.stack

كان بإمكاننا إنهاء المحادثة هنا. ولكن كان هناك شيء مريب يحدث، لأنّ "أدوات مطوّري البرامج" لا تستخدم أبدًا اسم الطريقة لإطارات الحزمة. في الواقع، لا تكشف فئة v8::StackFrame في واجهة برمجة تطبيقات C++ عن طريقة للوصول إلى اسم الطريقة. لذلك، يبدو أنّه كان من الخطأ أن ننتهي من الاتصال بـ JSStackFrame::GetMethodName() في المقام الأول. بدلاً من ذلك، يكون اسم الطريقة الوحيد في واجهة برمجة التطبيقات JavaScript stack trace API (ونعرضه فيها). لفهم هذا الاستخدام، يمكنك الاطّلاع على المثال البسيط التالي 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

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

تحسين مقاييس الأداء الدقيقة لـ StackTrace

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

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

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

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

أسماء الدوال

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

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

كان أول شيء برز هو التكلفة التراكمية لحساب الخط ورقم العمود. إنّ الجزء الأكثر تكلفة هنا هو احتساب إزاحة الحرف ضمن النص البرمجي (استنادًا إلى إزاحة الرمز الثنائي التي نحصل عليها من 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 (غير المُعدَّلة)، وقد طرحنا ذلك في الإصدار 8 منذ فترة لكي يكون Function.prototype.bind() أسرع. لقد نقلنا عمليات التحقّق اللازمة التي تسمح لنا بتخطّي البحث العام المكلف في المقام الأول، ونتيجةً لذلك لم يظهر v8::StackFrame::GetFunctionName في أي ملفات تجارية أخذناها في الاعتبار بعد الآن.

الخاتمة

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

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

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

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

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

استخدِم الخيارات التالية لمناقشة الميزات الجديدة أو التحديثات أو أي شيء آخر مرتبط بـ "أدوات مطوّري البرامج".