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

مقدمة

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

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

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

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

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

في رسم بياني مثل هذا، يمكنك أن ترى أن هناك الكثير من أحداث Garbage Collection التي تحدث، والتي يمكن أن تكون ضارة بأداء تطبيقات الويب الخاصة بك. ستتناول هذه المقالة كيفية التحكّم في استخدام الذاكرة، ما يقلّل من التأثير في الأداء.

جمع البيانات غير الضرورية وتكاليف الأداء

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

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

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

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

تقليل الاستخدام الزائد للذاكرة وتقليل رسوم تجميع المهملات

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

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

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

إلى هذا:

Static Memory Javascript

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

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

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

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

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

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

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

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

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 ريناتو مانجيني، هناك بعض العيوب.

الخاتمة

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

رمز مصدر

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

المراجع