المتصفح الحديث هو مشروع معقد للغاية يمكن أن تؤدي فيه التغييرات غير الضارة إلى مفاجآت غير متوقعة. لذلك ، هناك العديد من الاختبارات الداخلية التي يجب أن تلتقط هذه التغييرات قبل الإصدار. لا يوجد الكثير من الاختبارات أبدًا ، لذلك من المفيد استخدام العلامات العامة للجهات الخارجية أيضًا.
اسمي Andrey Logvinov ، أعمل في مجموعة تطوير محرك Yandex.Browser في نيجني نوفغورود. سأخبر قراء هابر اليوم كيف تعمل إدارة الذاكرة في مشروع Chromium على سبيل المثال لمشكلة غامضة واحدة أدت إلى تدهور الأداء في اختبار
عداد السرعة . تستند هذه المشاركة إلى تقريري من Yandex.Inside event.

مرة واحدة على لوحة القيادة الأداء لدينا ، شهدنا تدهور في سرعة اختبار عداد السرعة. يقيس هذا الاختبار الأداء الكلي للمتصفح على تطبيق قريب من الواقع - قائمة مهام ، حيث يضيف الاختبار عناصر إلى القائمة ثم يخرجها. تتأثر نتائج الاختبار بكل من أداء محرك V8 JS وسرعة عرض الصفحات في محرك Blink. يتكون اختبار عداد السرعة من عدة اختبارات فرعية ، حيث يتم كتابة تطبيق الاختبار باستخدام أحد أطر JS الشائعة ، على سبيل المثال jQuery أو ReactJS. يتم تعريف النتيجة الإجمالية للاختبار على أنها متوسط النتائج لجميع الأطر ، ولكن الاختبار يسمح لك بمشاهدة أداء كل إطار على حدة. تجدر الإشارة إلى أن الاختبار لا يهدف إلى تقييم أداء الأطر ، حيث يتم استخدامه فقط لجعل الاختبار أقل تركيبًا وأكثر قربًا من تطبيقات الويب الحقيقية. أظهر التفصيل بواسطة الاختبار الفرعي أن التدهور يتم ملاحظته فقط لإصدار تطبيق الاختبار الذي تم إنشاؤه باستخدام jQuery. وهذا مثير للاهتمام بالفعل ، توافق.
يبدأ التحقيق في مثل هذه المواقف بشكل قياسي إلى حد ما - نحدد أي التزام معين بالشفرة أدى إلى المشكلة. للقيام بذلك ، نقوم بتخزين تجميعات Yandex.Browser لكل التزام (!) على مدار السنوات القليلة الماضية (سيكون من غير العملي إعادة التجميع ، لأن التجميع يستغرق عدة ساعات). يشغل هذا مساحة كبيرة على الخوادم ، لكنه عادة ما يساعد في العثور بسرعة على مصدر المشكلة. ولكن هذه المرة بسرعة لم تنجح. اتضح أن تدهور نتائج الاختبار تزامن مع التزام دمج الإصدار التالي من Chromium. النتيجة ليست مشجعة ، لأن الإصدار الجديد من Chromium يجلب عددًا كبيرًا من التغييرات دفعة واحدة.
نظرًا لأننا لم نتلق أي معلومات تشير إلى حدوث تغيير محدد ، كان علي إجراء دراسة موضوعية لهذه المشكلة. للقيام بذلك ، استخدمنا أدوات المطور لإزالة آثار الاختبار. لاحظنا ميزة غريبة - فواصل "ممزقة" لتنفيذ وظائف اختبار Javascript.

نزيل تتبعًا تقنيًا أكثر مع about: التتبع ونرى أنه
جمع البيانات المهملة (GC) في Blink.

يُظهر مسار الذاكرة أدناه أن توقف GC هذا لا يستغرق الكثير من الوقت فحسب ، ولكن أيضًا لا يساعد في إيقاف نمو استهلاك الذاكرة.

ولكن إذا قمت بإدراج مكالمة GC صريحة في الاختبار ، ثم نرى صورة مختلفة تماما - يتم الاحتفاظ الذاكرة في منطقة الصفر ولا تسرب. لذلك ، ليس لدينا أي تسرب للذاكرة ، والمشكلة تتعلق بميزات المجمع. نستمر في الحفر. نبدأ المصحح ونرى أن جامع القمامة قد تجاوز حوالي 500 ألف كائن! مثل هذا العدد من الكائنات لا يمكن أن يؤثر على الأداء. لكن من أين أتوا؟
ونحن هنا في حاجة إلى ظهور فلاش صغير عن جهاز تجميع القمامة في Blink. يزيل الكائنات الميتة ، لكنه لا يحرك الكائنات الحية ، والذي يسمح بالعمل مع المؤشرات العارية في المتغيرات المحلية في كود C ++. هذا النمط يستخدم بنشاط في وميض. لكن له أيضًا سعره - عند جمع القمامة ، يجب عليك
فحص مكدس الدفق ، وإذا كان هناك شيء مشابه لمؤشر لكائن من كومة (كومة) هناك ، فكر في الكائن وكل شيء يشير إليه بشكل مباشر أو غير مباشر. وهذا يؤدي إلى حقيقة أن بعض الأشياء التي يتعذر الوصول إليها تقريبًا وبالتالي "الميتة" قد تم تحديدها على أنها كائنات حية. لذلك ، هذا الشكل من جمع القمامة يسمى أيضا المحافظ.
نتحقق من اتصال مكدس المسح الضوئي وتخطيه. اختفت المشكلة.
ماذا يمكن أن يكون في كومة تحتوي على 500 ألف كائن؟ نضع نقطة توقف في وظيفة إضافة كائنات - من بين أشياء أخرى ، نرى أن هناك شكوكًا:
blink :: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...
مرجع جدول التجزئة هو المشتبه به المحتمل! نحن نختبر الفرضية من خلال تخطي إضافة هذا الرابط. اختفت المشكلة. حسنًا ، نحن على بعد خطوة واحدة من الإجابة.
نذكر ميزة أخرى لجامع البيانات المهملة في Blink: إذا رأى مؤشرًا إلى داخل جدول التجزئة ، فسيعتبر هذا علامة على التكرار المستمر على الجدول ، مما يعني أنه يعتبر كل الروابط الموجودة في هذا الجدول مفيدة وتستمر في تجاوزها. في حالتنا ، الخمول. ولكن ما هي وظيفة مصدر هذا الرابط؟
نتقدم بإطارات قليلة من المكدس أعلى ، ونأخذ الموضع الحالي للماسحة الضوئية ، وننظر إلى الإطار المكدس الذي تعمل فيه. هذه دالة تسمى
ScheduleGCIfNeeded . يبدو أنه هنا هو الجاني ، لكن ... ننظر إلى الكود المصدري للوظيفة ونرى أنه لا توجد جداول تجزئة هناك على الإطلاق. علاوة على ذلك ، يعد هذا جزءًا من جامع البيانات المهملة نفسه ، وهو ببساطة لا يحتاج إلى الرجوع إلى كائنات من كومة Blink. من أين جاء هذا الرابط "السيئ"؟
وضعنا نقطة توقف عند تغيير خلية الذاكرة ، حيث وجدنا رابطًا لجدول التجزئة. نرى أن إحدى الوظائف الداخلية تسمى V8PerIsolateData :: AddActiveScriptWrappable يكتب هناك. هناك ، يضيفون بعض عناصر HTML التي تم إنشاؤها من بعض الأنواع ، بما في ذلك الإدخال ، إلى جدول تجزئة واحد active_script_wrappables_. هذا الجدول ضروري لمنع إزالة العناصر التي لم تعد مرجعية من Javascript أو شجرة DOM ، ولكنها مرتبطة بأي نشاط خارجي يمكن ، على سبيل المثال ، إنشاء أحداث.
يأخذ جامع البيانات المهملة أثناء اجتياز الجدول العادي في الاعتبار حالة العناصر الموجودة فيه ويضعها في الاعتبار أو لا يضع عليها علامات ، ثم يتم حذفها في المرحلة التالية من التجميع. ومع ذلك ، في حالتنا ، ينبثق مؤشر إلى وحدة التخزين الداخلية لهذا الجدول عند مسح المكدس ، ويتم تمييز جميع عناصر الجدول على أنها حية.
لكن كيف بلغت القيمة من كومة دالة ما كومة أخرى؟!
فكر في ScheduleGCIfNeeded. تذكر أنه لم يتم العثور على أي شيء مفيد في التعليمات البرمجية المصدر لهذه الوظيفة ، ولكن هذا يعني فقط أن الوقت قد حان للذهاب إلى مستوى أقل والتحقق من
المحول البرمجي . يبدو prolog تفكيكها من دالة ScheduleGCIfNeeded كما يلي:
0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax
يمكن ملاحظة أن الوظيفة
تنتقل بشكل خاص إلى 0B8h ، ولا يتم استخدام هذا المكان بشكل إضافي. ولكن لهذا السبب ، يرى ماسحة المكدس ما تم تسجيله مسبقًا بواسطة وظائف أخرى. وبالمصادفة ، فإن المؤشر إلى داخل جدول التجزئة الذي تتركه وظيفة AddActiveScriptWrappable يدخل في هذا "الثقب". كما اتضح فيما بعد ، كان سبب ظهور "ثقب" في هذه الحالة هو
ماكرو VLOG debug داخل الوظيفة ، والذي يعرض معلومات إضافية في السجل.
ولكن لماذا يحتوي الجدول active_script_wrappable_ على مئات الآلاف من العناصر؟ لماذا لوحظ تدهور الأداء فقط في اختبار مسج؟ الإجابة على كلا السؤالين هي نفسها - في هذا الاختبار المحدد ، لكل تغيير (مثل علامة اختيار في مربع الاختيار) يتم إعادة إنشاء واجهة المستخدم بالكامل بالكامل. ينتج الاختبار عناصر تتحول على الفور إلى نفايات. ما تبقى من الاختبارات في عداد السرعة هي أكثر حكمة ولا تخلق عناصر غير ضرورية ، لذلك ، لا يلاحظ تدهور الأداء بالنسبة لهم. إذا كنت تقوم بتطوير خدمات الويب ، فعليك مراعاة ذلك حتى لا تنشئ عملًا غير ضروري للمتصفح.
ولكن لماذا نشأت المشكلة الآن فقط إذا كان ماكرو VLOG من قبل؟ لا توجد إجابة دقيقة ، ولكن على الأرجح ، أثناء التحديث ، تم تغيير الموضع النسبي للعناصر الموجودة في الحزمة ، وبسبب ذلك أصبح المؤشر إلى جدول التجزئة متاحًا عن طريق الخطأ للماسح الضوئي. في الواقع ، فزنا في اليانصيب. لإغلاق "الفتحة" بسرعة واستعادة الأداء ، أزلنا الماكرو debug VLOG. بالنسبة للمستخدمين ، لا فائدة منه ، ولاحتياجاتنا التشخيصية ، يمكننا دائمًا إعادة تشغيله. شاركنا أيضًا تجاربنا مع مطوري Chromium الآخرين. أكدت الإجابة مخاوفنا: هذه مشكلة أساسية لجمع القمامة المحافظة في بلينك ، والتي لا يوجد لديها حل منهجي.
روابط مثيرة للاهتمام
1. إذا كنت مهتمًا بمعرفة المزيد عن الحياة اليومية غير العادية لمجموعتنا ، فاستذكر
قصة المستطيل الأسود ، والتي أدت إلى تسارع ليس فقط Yandex.Browser ، ولكن مشروع Chromium بأكمله.
2. وأدعوك أيضًا إلى الاستماع إلى تقارير أخرى في حدث
Yandex التالي.
داخل الحدث يوم 16 فبراير ، التسجيل مفتوح ، وسيكون البث أيضًا.