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

مقدمة

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

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

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

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

ماذا تعني أسنان المنشار

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

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

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

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

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

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

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

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

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

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

إلى هذا:

رمز JavaScript لذاكرة ثابتة

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

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

JavaScript للذاكرة الثابتة هي تقنية تتضمن التخصيص المُسبَق، في بداية تطبيقك، لجميع الذاكرة التي ستحتاج إليها طوال فترة تشغيله، وإدارة هذه الذاكرة أثناء التنفيذ عندما لا تكون الكائنات مطلوبة بعد الآن. يمكننا تحقيق هذا الهدف من خلال اتّباع بضع خطوات بسيطة:

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

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

تجمع الكائنات

بعبارات بسيطة، تجميع الكائنات هو عملية الاحتفاظ بمجموعة من العناصر غير المستخدَمة التي تتشارك نوعًا ما. عندما تحتاج إلى عنصر جديد لرمزك، بدلاً من تخصيص عنصر جديد من خلال النظام 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 ريناتو مانجيني إلى بعض السلبيات.

الخاتمة

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

رمز المصدر

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

المراجع