مكافحة تسرب الذاكرة في تطبيقات الويب

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



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


هناك خطأ ما

بالطبع ، يمكن أن تعاني المواقع التي يتم تقديمها على الخادم من مشكلة تسرب للذاكرة. ولكن هنا نتحدث عن ذاكرة الخادم. في الوقت نفسه ، من غير المرجح أن تتسبب هذه التطبيقات في حدوث تسرب للذاكرة على العميل ، حيث يقوم المتصفح بمسح الذاكرة بعد كل انتقال للمستخدم بين الصفحات.

لم يتم تغطية موضوع تسرب الذاكرة بشكل جيد في منشورات تطوير الويب. وعلى الرغم من ذلك ، فإنني على يقين تقريبًا من أن معظم التطبيقات غير العادية للصفحة الواحدة تعاني من تسرب للذاكرة - ما لم يكن لدى الفرق التي تتعامل معها أدوات موثوقة لاكتشاف هذه المشكلة وإصلاحها. النقطة هنا هي أنه في JavaScript من السهل للغاية تخصيص كمية معينة من الذاكرة بشكل عشوائي ، ثم ننسى فقط تحرير هذه الذاكرة.

سيشارك كاتب المقال ، الذي ننشر ترجمته اليوم ، مع القراء تجربته في مكافحة تسرب الذاكرة في تطبيقات الويب ، ويريد أيضًا تقديم أمثلة على اكتشافهم الفعال.

لماذا لا يكتب الكثير عن هذا؟


أولاً ، أريد أن أتحدث عن سبب كتابة القليل جدًا عن تسرب الذاكرة. أفترض هنا يمكنك العثور على عدة أسباب:

  • نقص شكاوى المستخدمين: معظم المستخدمين ليسوا مشغولين بمراقبة مدير المهام عن كثب أثناء تصفح الويب. عادة ، لا يواجه المطور شكاوى المستخدمين حتى يكون تسرب الذاكرة خطيرًا لدرجة أنه يتسبب في عدم القدرة على العمل أو إبطاء التطبيق.
  • : Chrome - , . .
  • : .
  • : «» . , , , , -.


تستخدم المكتبات والأطر الحديثة لتطوير تطبيقات الويب ، مثل React و Vue و Svelte ، النموذج المكون للتطبيق. في هذا النموذج ، فإن الطريقة الأكثر شيوعًا لإحداث تسرب للذاكرة هي شيء من هذا القبيل:

window.addEventListener('message', this.onMessage.bind(this));

هذا كل شئ. هذا هو كل ما هو مطلوب "لتجهيز" مشروع مع تسرب للذاكرة. للقيام بذلك ، ما عليك سوى استدعاء طريقة addEventListener لكائن عمومي (مثل window، أو <body>، أو شيء مشابه) ، وبعد ذلك ، عند إلغاء تركيب المكون ، نسيت إزالة مستمع الأحداث باستخدام طريقة removeEventListener .

لكن عواقب ذلك أسوأ ، حيث يحدث تسرب المكون بأكمله. هذا يرجع إلى حقيقة أن الأسلوب this.onMessageمرتبط this. إلى جانب هذا المكون ، يحدث تسرب لمكوناته الفرعية. من المحتمل جدًا أن تتسرب جميع عقد DOM المرتبطة بهذا المكون. ونتيجة لذلك ، يمكن أن يخرج الوضع عن السيطرة بسرعة كبيرة ، مما يؤدي إلى عواقب سيئة للغاية.

إليك كيفية حل هذه المشكلة:

//   
this.onMessage = this.onMessage.bind(this);
window.addEventListener('message', this.onMessage);
 
//   
window.removeEventListener('message', this.onMessage);

الحالات التي يحدث فيها تسرب للذاكرة في أغلب الأحيان


تقول لي التجربة أن تسرب الذاكرة يحدث غالبًا عند استخدام واجهات برمجة التطبيقات التالية:

  1. الطريقة addEventListener. هذا هو المكان الذي يحدث فيه تسرب للذاكرة في أغلب الأحيان. لحل المشكلة ، يكفي الاتصال في الوقت المناسب removeEventListener.
  2. setTimeout setInterval. , (, 30 ), , , , , clearTimeout clearInterval. , setTimeout, «» , , setInterval-. , setTimeout .
  3. API IntersectionObserver, ResizeObserver, MutationObserver . , , . . - , , , , , disconnect . , DOM , , -. -, . — <body>, document, header footer, .
  4. Promise-, , . , , — , . , , «» , . «» .then()-.
  5. مستودعات تمثلها كائنات عالمية. عندما تستخدم شيئًا مثل Redux للتحكم في حالة التطبيق ، يتم تمثيل مخزن الحالة بواسطة كائن عام. ونتيجة لذلك ، إذا كنت تتعامل مع هذا التخزين بلا مبالاة ، فلن يتم حذف البيانات غير الضرورية منه ، ونتيجة لذلك سيزداد حجمه باستمرار.
  6. نمو DOM لانهائي. إذا نفذت الصفحة التمرير اللامتناهي دون استخدام المحاكاة الافتراضية ، فهذا يعني أن عدد عقد DOM في هذه الصفحة يمكن أن ينمو بشكل غير محدود.

أعلاه ، درسنا المواقف التي يحدث فيها تسرب للذاكرة في أغلب الأحيان ، ولكن ، بالطبع ، هناك العديد من الحالات الأخرى التي تسبب المشكلة التي تهمنا.

تحديد تسرب الذاكرة


الآن انتقلنا إلى التحدي المتمثل في تحديد تسرب الذاكرة. بادئ ذي بدء ، لا أعتقد أن أي من الأدوات الموجودة مناسبة جدًا لذلك. جربت أدوات تحليل ذاكرة Firefox ، وجربت الأدوات من Edge و IE. تم اختباره حتى محلل أداء Windows. ولكن أفضل هذه الأدوات لا تزال أدوات مطوري Chrome. صحيح ، في هذه الأدوات هناك العديد من "الزوايا الحادة" ، والتي تستحق المعرفة.

من بين الأدوات التي يقدمها مطور Chrome ، نحن مهتمون أكثر بالملف الشخصي Heap snapshotمن علامة التبويب Memory، والذي يسمح لك بإنشاء لقطات من الكومة. هناك أدوات أخرى لتحليل الذاكرة في Chrome ، لكنني لم أتمكن من الاستفادة منها بشكل خاص في الكشف عن تسرب الذاكرة.


تتيح لك أداة Heap snapshot التقاط لقطات لذاكرة التدفق الرئيسي أو العاملين على الويب أو عناصر iframe.

إذا كانت نافذة أداة Chrome تبدو كالتي تظهر في الشكل السابق ، عند النقر فوق الزرTake snapshot، يتم التقاط معلومات حول جميع الكائنات الموجودة في ذاكرة الجهاز الظاهري المحدد جافا سكريبت للصفحة التي تم التحقيق فيها. يتضمن ذلك الكائنات المشار إليها فيwindow، والكائنات المشار إليها بواسطة عمليات رد الاتصال المستخدمة في المكالمةsetInterval، وما إلى ذلك. يمكن النظر إلى لقطة من الذاكرة على أنها "لحظة مجمدة" لعمل الكيان الذي تم التحقيق فيه ، والتي تمثل معلومات حول كل الذاكرة المستخدمة من قبل هذا الكيان.

بعد التقاط الصورة ، نصل إلى الخطوة التالية لإيجاد التسريبات. وهو يتألف من إعادة إنتاج سيناريو قد يحدث فيه تسرب للذاكرة وفقًا للمطور. على سبيل المثال ، يتم فتح وإغلاق نافذة مشروطة معينة. بعد إغلاق النافذة المماثلة ، من المتوقع أن يعود حجم الذاكرة المخصصة إلى المستوى الذي كان موجودًا قبل فتح النافذة. لذلك ، يلتقطون صورة أخرى ، ثم يقارنونها بالصورة التي تم التقاطها سابقًا. في الواقع ، مقارنة الصور هي أهم ميزة تهمنا Heap snapshot.


نأخذ اللقطة الأولى ، ثم نتخذ إجراءات يمكن أن تسبب تسرب للذاكرة ، ثم نأخذ لقطة أخرى. إذا لم يكن هناك تسرب ، فسيكون حجم الذاكرة المخصصة متساوياً

، صحيح أن هذاHeap snapshotبعيد عن الأداة المثالية. هناك بعض القيود التي يجب معرفتها عن:

  1. حتى إذا نقرت على الزر الصغير في اللوحة Memoryالتي تبدأ في جمع القمامة ( Collect garbage) ، فلكي تتأكد من مسح الذاكرة حقًا ، قد تحتاج إلى التقاط عدة صور متتالية. عادة ما يكون لدي ثلاث طلقات. هنا يجدر التركيز على الحجم الكلي لكل صورة - في النهاية ، يجب أن تستقر.
  2. -, -, iframe, , , . , JavaScript. — , , .
  3. «». .

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

نشق طريقنا من خلال ضجيج المعلومات


لقد وجدت أن أفضل طريقة لاختراق ضجيج المعلومات هي تكرار الإجراءات التي من المفترض أن تتسبب في تسرب للذاكرة. على سبيل المثال ، بدلاً من فتح وإغلاق نافذة الوسائط مرة واحدة فقط بعد التقاط اللقطة الأولى ، يمكن القيام بذلك 7 مرات. لماذا 7؟ نعم ، إذا كان فقط لأن 7 هو رئيس ملحوظ. ثم تحتاج إلى أخذ لقطة ثانية ، ومقارنتها مع الأولى ، اكتشف ما إذا كان جسم معين "تسرب" 7 مرات (أو 14 مرة ، أو 21 مرة).


قارن لقطات الكومة. يرجى ملاحظة أننا نقوم بمقارنة الصورة رقم 3 مع الصورة رقم 6. الحقيقة هي أنني أخذت ثلاث طلقات متتالية حتى يحصل Chrome على المزيد من جلسات جمع القمامة. بالإضافة إلى ذلك ، لاحظ أن بعض الأشياء "تسربت" 7 مرات.

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

الآن قد يكون لديك سؤال حول لماذا يجب أن تولي اهتماما خاصا لعدد الكائنات ، وليس لكمية الذاكرة الإجمالية. هنا يمكننا القول أننا نسعى بشكل حدسي للحد من كمية الذاكرة "المتسربة". في هذا الصدد ، قد تعتقد أنه يجب عليك مراقبة إجمالي حجم الذاكرة المستخدمة. لكن هذا النهج ، لسبب واحد مهم ، لا يناسبنا بشكل خاص.

إذا كان هناك شيء "تسرب" ، يحدث ذلك لأن (إعادة سرد جو أرمسترونغ ) تحتاج إلى موزة ، ولكن ينتهي بك الأمر مع موزة ، والغوريلا التي تحمله ، بالإضافة إلى كل الغابة. إذا ركزنا على المقدار الكلي للذاكرة ، فسيكون ذلك بمثابة "قياس" الغابة ، وليس الموز الذي يهمنا.


غوريلا تأكل موزة

الآن نعود إلى المثال أعلاه معaddEventListener. مصدر التسرب هو مستمع للأحداث يشير إلى وظيفة. وهذه الوظيفة بدورها تشير إلى مكون ربما يحتفظ بروابط لمجموعة من الأشياء الجيدة مثل المصفوفات والأوتار والأشياء.

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

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

ربط شجرة التحليل


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


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

في المثال أعلاه ، هناك متغير يسمىsomeObjectالمشار إليه في الإغلاق (context) المشار إليه من قبل مستمع الأحداث. إذا نقرت على الرابط المؤدي إلى شفرة المصدر ، فسيتم عرض نص البرنامج المفهوم إلى حد ما:

class SomeObject () { /* ... */ }
 
const someObject = new SomeObject();
const onMessage = () => { /* ... */ };
window.addEventListener('message', onMessage);

إذا قارنا هذا الرمز بالرقم السابق ، يتبين أن contextالشكل هو إغلاق onMessageيشير إلى someObject. هذا مثال مصطنع . يمكن أن يكون تسرب الذاكرة الحقيقية أقل وضوحًا بكثير.

تجدر الإشارة إلى أن أداة لقطة الكومة لديها بعض القيود:

  1. إذا قمت بحفظ ملف لقطة ثم تحميله مرة أخرى ، فستفقد الروابط إلى الملفات ذات الرمز. هذا ، على سبيل المثال ، بعد تنزيل لقطة ، لن يكون من الممكن معرفة أن رمز إغلاق مستمع الحدث موجود في السطر 22 من الملف foo.js. نظرًا لأن هذه المعلومات مهمة للغاية ، فإن حفظ ملفات لقطة كومة الذاكرة المؤقتة ، أو ، على سبيل المثال ، نقلها إلى شخص ما ، يكاد يكون عديم الفائدة.
  2. WeakMap, Chrome , . , , , , . WeakMap — .
  3. Chrome , . , , , , . , object, EventListener. object — , , , «» 7 .

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

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

تحليل تسرب الذاكرة الآلي


أريد أن أبدأ هذا القسم بحقيقة أنني لم أجد طريقة جيدة لأتمتة اكتشاف تسرب الذاكرة. يحتوي Chrome على API.memory API الخاص به ، ولكن لأسباب تتعلق بالخصوصية ، لا يسمح لك بجمع بيانات مفصلة بما فيه الكفاية. ونتيجة لذلك ، لا يمكن استخدام واجهة برمجة التطبيقات هذه في الإنتاج للكشف عن التسريبات. ناقشت مجموعة عمل أداء W3C سابقًا أدوات الذاكرة ، لكن أعضاءها لم يتفقوا بعد على معيار جديد مصمم لاستبدال واجهة برمجة التطبيقات هذه.

في بيئات الاختبار ، يمكنك زيادة درجة دقة إخراج البيانات performance.memoryباستخدام علامة Chrome - معلومات دقيقة عن الذاكرة. لا يزال من الممكن إنشاء لقطات كومة الذاكرة المؤقتة باستخدام فريق Chromedriver الخاص : takeHeapSnapshot . هذا الفريق لديه نفس القيود التي ناقشناها بالفعل. من المحتمل أنك إذا استخدمت هذا الأمر ، للأسباب الموضحة أعلاه ، فمن المنطقي الاتصال به ثلاث مرات ، ثم أخذ ما تم استلامه فقط نتيجة المكالمة الأخيرة.

نظرًا لأن مستمعي الأحداث هم أكثر مصادر تسرب الذاكرة شيوعًا ، سأتحدث عن تقنية أخرى لاكتشاف التسرب أستخدمها. وهو يتألف من إنشاء بقع قرد لواجهة برمجة التطبيقات addEventListenerوفي removeEventListenerعد الروابط للتحقق من أن عددهم يعود إلى الصفر. هنا مثال لكيفية القيام بذلك.

في أدوات مطوري Chrome ، يمكنك أيضًا استخدام واجهة برمجة التطبيقات الأصلية getEventListeners لمعرفة مستمعي الأحداث المرتبطين بعنصر معين. ومع ذلك ، لا يتوفر هذا الأمر إلا من شريط أدوات المطور.

أريد أن أضيف أن ماتياس بينينز أخبرني عن واجهة برمجة تطبيقات أخرى مفيدة لأدوات Chrome. هذه استعلامات / عناصر . باستخدامه ، يمكنك الحصول على معلومات حول جميع الكائنات التي تم إنشاؤها باستخدام مُنشئ معين. فيما يلي بعض المواد الجيدة حول هذا الموضوع حول أتمتة كشف تسرب الذاكرة في Puppeteer.

ملخص


لا يزال البحث عن تسريبات الذاكرة وإصلاحها في تطبيقات الويب في مهدها. لقد تحدثت هنا عن بعض التقنيات التي كانت في حالتي جيدة الأداء. ولكن يجب الاعتراف بأن تطبيق هذه التقنيات لا يزال محفوفًا بصعوبات معينة ويستغرق وقتًا طويلاً.

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

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

القراء الأعزاء! هل واجهت تسرب للذاكرة في مشاريع الويب الخاصة بك؟


All Articles