JavaScript ثابت للذاكرة مع مجموعات الكائنات

مقدمة

فإذا تلقيت رسالة إلكترونية تخبرك بمستوى الأداء السيئ للعبة الويب أو تطبيق الويب بعد مرور فترة زمنية معيّنة، تتعمق في الرمز ولا ترى أي شيء مميز إلى أن تفتح أدوات أداء الذاكرة في Chrome، وسترى ما يلي:

لقطة من المخطط الزمني للذاكرة

يضحك أحد زملائك في العمل لأنّه أدرك أنّك تواجه مشكلة في الأداء تتعلّق بالذاكرة.

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

ما المقصود بالأسنان المنشارية

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

تكاليف جمع القمامة والأداء

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

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

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

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

تقليل استهلاك الذاكرة، وتخفيض الضرائب على جمع القمامة

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

ستؤدي هذه العملية إلى نقل الرسم البياني للذاكرة من هذا :

لقطة من المخطط الزمني للذاكرة

لهذا:

نص JavaScript للذاكرة الثابتة

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

الانتقال إلى محتوى JavaScript للذاكرة الثابتة

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

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

في الواقع، يتطلب منا إنجاز رقم 1 القيام ببعض الشيء الثاني، لذلك دعنا نبدأ من هناك.

مجموعة العناصر

باختصار، تجميع الكائنات هو عملية الاحتفاظ بمجموعة من العناصر غير المستخدَمة التي تشترك في نوع معيّن. عندما تحتاج إلى عنصر جديد لرمزك، بدلاً من تخصيص عنصر جديد من خلال النظام Memory Heap، يمكنك بدلاً من ذلك إعادة تدوير أحد العناصر غير المستخدَمة من المجموعة. بعد الانتهاء من استخدام الرمز الخارجي مع الكائن، بدلاً من رفعه إلى الذاكرة الرئيسية، يتم إرجاعه إلى مجموعة البيانات. ولن يتم جمع البيانات غير الصحيحة بسبب عدم إزالة الإشارة إلى العنصر (ما يُعرف أيضًا بحذفه) من الرمز. من خلال استخدام وحدات تجميع العناصر، سيعيد المبرمج التحكّم في الذاكرة، ما يقلل من تأثير أداة تجميع البيانات المهملة على الأداء.

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

var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};

//..... do some stuff with the object that we need to do

gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference

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

التخصيص المسبق للعناصر

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

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

function init() {
  //preallocate all our pools. 
  //Note that we keep each pool homogeneous wrt object types
  gEntityObjectPool.preAllocate(256);
  gDomObjectPool.preAllocate(888);
}

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

بدلاً من أن يكون حلاً حاسمًا

هناك تصنيف كامل للتطبيقات التي تساهم فيها أنماط نمو الذاكرة الثابتة في تحقيق النجاح. مع ذلك، هناك بعض العيوب كما أشار زميلك من مطوّري برامج Chrome DevRel Renato Mangini.

الخلاصة

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

رمز مصدر

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

المراجع