اعتاد مطوّرو الويب ألا يلاحظوا أي تأثير في الأداء أو يلاحظون تأثيرًا بسيطًا عند تصحيح أخطاء الرموز البرمجية. ومع ذلك، لا ينطبق هذا التوقّع على جميع المواقع الإلكترونية. لا يتوقع مطوّر C++ أبدًا أن يصل إصدار تصحيح الأخطاء من تطبيقه إلى مستوى الأداء في مرحلة الإنتاج، وفي السنوات الأولى من Chrome، كان فتح "أدوات مطوّري البرامج" يؤثر بشكل كبير في أداء الصفحة.
إنّ عدم ملاحظة هذا الانخفاض في الأداء بعد الآن هو نتيجة سنوات من الاستثمار في إمكانات تصحيح الأخطاء في DevTools وV8. ومع ذلك، لن نتمكّن أبدًا من تقليل التأثير الإضافي على الأداء في "أدوات مطوّري البرامج" إلى الصفر. إنّ ضبط نقاط التوقف والتنقّل في الرمز البرمجي وجمع عمليات تتبُّع تسلسل استدعاء الدوال البرمجية وتسجيل أثر النشاط، وما إلى ذلك، تؤثر جميعها في سرعة التنفيذ بدرجة متفاوتة. بعد كل شيء، إنّ مراقبة أيّ شيء تؤدي إلى تغييره.
ولكن بالطبع، يجب أن تكون النفقات العامة لخدمة "أدوات مطوّري البرامج" معقولة، مثل أي أداة تصحيح أخطاء. لاحظنا مؤخرًا زيادة كبيرة في عدد التقارير التي تشير إلى أنّه في بعض الحالات، قد تؤدي أدوات المطوّرين إلى إبطاء التطبيق إلى درجة يتعذّر معها استخدامه. يمكنك الاطّلاع أدناه على مقارنة جنبًا إلى جنب من التقرير chromium:1069425، توضّح زيادة الأداء الناتجة عن فتح "أدوات مطوّري البرامج" فقط.
كما يظهر في الفيديو، يتراوح معدّل التباطؤ بين 5 و10 مرات، وهو معدّل غير مقبول بوضوح. كانت الخطوة الأولى هي فهم الوقت الذي يستغرقه كل إجراء وسبب هذا التباطؤ الكبير عند فتح DevTools. باستخدام Linux perf في عملية Chrome Renderer، تبيّن لنا التوزيع التالي لوقت تنفيذ أداة التحويل الشاملة:
على الرغم من أنّنا توقّعنا إلى حدّ ما رؤية شيء مرتبط بجمع عمليات تتبُّع تسلسل استدعاء الدوال البرمجية، لم نتوقّع أن يتم تخصيص %90 تقريبًا من إجمالي وقت التنفيذ لتمثيل إطارات تسلسل استدعاء الدوال البرمجية. يشير الرمز هنا إلى عملية حلّ أسماء الدوالّ ومواقع المصدر المحدّدة - أرقام السطر والعمود في النصوص البرمجية - من إطارات الحزمة الأولية.
الاستنتاج من اسم الطريقة
كان الأمر الأكثر غرابة هو أنّ معظم الوقت يُستغرَق في دالة JSStackFrame::GetMethodName()
في الإصدار 8، على الرغم من أنّنا عرفنا من خلال التحقيقات السابقة أنّ دالة JSStackFrame::GetMethodName()
ليست غريبة في عالم مشاكل الأداء. تحاول هذه الدالة احتساب اسم الطريقة للإطارات التي تُعتبر طلبات استدعاء للطريقة (الإطارات التي تمثّل طلبات استدعاء للدالة من النوع obj.func()
بدلاً من func()
). أظهرت نظرة سريعة على الرمز البرمجي أنّه يعمل من خلال إجراء تنقّل كامل للعنصر وسلسلة النماذج الأولية والبحث عن
- سمات البيانات التي يكون
value
فيها هو الإغلاقfunc
- سمات الموصّل التي يكون فيها
get
أوset
مساويًا للإغلاقfunc
على الرغم من أنّ هذا الإجراء ليس رخيصًا، إلا أنّه لا يبدو أنّه يوضّح سبب هذا التباطؤ الشديد. لذلك، بدأنا بالبحث في المثال الذي تم الإبلاغ عنه في chromium:1069425، وتبيّن لنا أنّه تم جمع عمليات تتبُّع تسلسل استدعاء الدوالّ للمهام غير المتزامنة بالإضافة إلى رسائل السجلّ التي نشأت من classes.js
، وهو ملف JavaScript بحجم 10 ميغابايت. بعد التدقيق، تبيّن أنّه كان في الأساس وقت تشغيل Java بالإضافة إلى رمز التطبيق الذي تم تجميعه إلى JavaScript. احتوت عمليات تتبُّع تسلسل استدعاء الدوالّ على عدة إطارات تتضمّن طرقًا يتم استدعاؤها على عنصر A
، لذا اعتقدنا أنّه من المفيد معرفة نوع العنصر الذي نتعامل معه.
يبدو أنّ مترجم Java إلى JavaScript أنشأ عنصرًا واحدًا يتضمّن 82,203 وظيفة، ما بدأ يثير الاهتمام بوضوح. بعد ذلك، عدنا إلى JSStackFrame::GetMethodName()
في الإصدار 8 لمعرفة ما إذا كانت هناك بعض الميزات البسيطة التي يمكننا تحسينها.
- تعمل هذه الدالة عن طريق البحث أولاً عن
"name"
للدالة كخاصية في العنصر، وإذا تم العثور عليها، تتحقّق من أنّ قيمة السمة تتطابق مع الدالة. - إذا لم يكن للدالة اسم أو لم يكن للكائن سمة مطابقة، يتم الرجوع إلى البحث العكسي من خلال تصفّح جميع سمات الكائن ونماذجه الأولية.
في مثالنا، تكون جميع الدوال مجهولة الهوية وتتضمّن سمات "name"
فارغة.
A.SDV = function() {
// ...
};
كان العثور الأول على أنّ البحث العكسي قد تم تقسيمه إلى خطوتَين (يتم تنفيذهما للكائن نفسه وكل كائن في سلسلة النماذج الأولية):
- استخراج أسماء جميع السمات التي يمكن عدّها
- يمكنك إجراء بحث عام عن المواقع لكل اسم، واختبار ما إذا كانت قيمة الموقع الناتجة تتطابق مع الإغلاق الذي نبحث عنه.
يبدو أنّ هذا الإجراء سهل جدًا، لأنّ استخراج الأسماء يتطلّب الاطّلاع على جميع المواقع الإلكترونية. بدلاً من إجراء الدورتَين - O(N) لاستخراج الاسم وO(N log(N)) للاختبارات - يمكننا إجراء كل شيء في جولة واحدة والتحقّق من قيم السمات مباشرةً. وقد أدّى ذلك إلى تسريع الدالة بأكملها بمقدار من مرّتين إلى 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()
بشكل كبير، وفي الواقع، تشير اختبارات الأداء أيضًا إلى أنّ التغييرات التي أجريناها جعلت الأمور أسرع بكثير.
ولكن بالعودة إلى موضوع "أدوات مطوّري البرامج في 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)، سنحسب أيضًا اسم الطريقة، فور طلب أي معلومات حول إطار تسلسل استدعاء الدوال البرمجية. تبيّن أنّ أدوات المطوّرين تطلب دائمًا معلومات المصدر والنص البرمجي على الفور.
استنادًا إلى هذا الوعي، تمكّنا من إعادة صياغة تمثيل إطارات الذاكرة المؤقتة وتبسيطه بشكل كبير وجعله أكثر كفاءة، بحيث لا تدفع عمليات الاستخدام في V8 وChromium الآن سوى تكلفة احتساب المعلومات التي يطلبونها. وقد أدّى ذلك إلى تحسين كبير في أداء DevTools وحالات استخدام Chromium الأخرى التي لا تحتاج سوى إلى جزء من المعلومات حول إطارات تسلسل استدعاء الدوال البرمجية (اسم النص البرمجي وموقع المصدر في شكل إزاحة السطر والعمود)، كما فتح الباب أمام المزيد من تحسينات الأداء.
أسماء الدوالّ
بعد الانتهاء من عمليات إعادة التنظيم المذكورة أعلاه، تم تقليل النفقات العامة للترميز (الوقت المستغرَق في v8_inspector::V8Debugger::symbolize
) إلى %15 تقريبًا من إجمالي وقت التنفيذ، وتمكّنا من معرفة الوقت الذي يقضيه V8 عند (جمع) ترميز إطارات الحزمة للاستخدام في DevTools.
كان أول ما لفت انتباهنا هو التكلفة التراكمية لاحتساب رقم الصف والعمود. إنّ الجزء الأكثر تكلفة هنا هو احتساب إزاحة الأحرف ضمن النص البرمجي (استنادًا إلى إزاحة الرمز الثنائي التي نحصل عليها من V8)، وتبيّن أنّنا أجرينا ذلك مرتين بسبب إعادة التنظيم أعلاه، مرة عند احتساب رقم السطر ومرة أخرى عند احتساب رقم العمود. ساعدت تخزين موضع المصدر في نُسخ v8::internal::StackFrameInfo
في حلّ هذه المشكلة بسرعة وإزالة v8::internal::StackFrameInfo::GetColumnNumber
نهائيًا من أي ملفات تجارية.
كان من بين النتائج الأكثر إثارة للاهتمام بالنسبة إلينا أنّ v8::StackFrame::GetFunctionName
كان مرتفعًا بشكل مفاجئ في جميع الملفات الشخصية التي اطّلعنا عليها. بعد التوغّل في التفاصيل، تبيّن لنا أنّه كان من غير الضروري احتساب الاسم الذي سنعرضه للدالة في إطار الحزمة في DevTools.
- البحث أولاً عن خاصية
"displayName"
غير عادية وإذا أدّى ذلك إلى الحصول على خاصية بيانات ذات قيمة سلسلة، سنستخدمها، - أو الرجوع إلى البحث عن موقع
"name"
عادي والتحقّق مرة أخرى ممّا إذا كان ذلك يؤدي إلى عرض موقع بيانات قيمته سلسلة، - وفي النهاية، يتم الرجوع إلى اسم تصحيح أخطاء داخلي يستنتجه منظِّم 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
ملحوظًا، كما هو موضّح في chromium:1077657، ولكن في الوقت الحالي، لقد عالجنا المشاكل الرئيسية، وقد نعود في المستقبل لتسهيل أداء تصحيح الأخطاء.
تنزيل قنوات المعاينة
ننصحك باستخدام إصدار Canary أو Dev أو الإصدار التجريبي من Chrome كمتصفّح التطوير التلقائي. تتيح لك قنوات المعاينة هذه الوصول إلى أحدث ميزات DevTools، وتتيح لك اختبار واجهات برمجة تطبيقات منصات الويب المتطوّرة، وتساعدك في العثور على المشاكل في موقعك الإلكتروني قبل أن يعثر عليها المستخدمون.
التواصل مع فريق "أدوات مطوّري البرامج في Chrome"
استخدِم الخيارات التالية لمناقشة الميزات الجديدة أو التحديثات أو أي شيء آخر مرتبط بـ "أدوات مطوّري البرامج".
- يمكنك إرسال الملاحظات وطلبات الميزات إلينا على crbug.com.
- يمكنك الإبلاغ عن مشكلة في "أدوات مطوّري البرامج" باستخدام رمز خيارات إضافية > مساعدة > الإبلاغ عن مشكلة في "أدوات مطوّري البرامج" في "أدوات مطوّري البرامج".
- يمكنك نشر تغريدة على Twitter على @ChromeDevTools.
- يمكنك إضافة تعليقات على فيديوهات YouTube التي تعرض الميزات الجديدة في "أدوات مطوّري البرامج" أو فيديوهات YouTube التي تعرض نصائح حول "أدوات مطوّري البرامج".