מבוא
קיבלתם אימייל על כך שהביצועים של המשחק או האפליקציה שלכם באינטרנט נמוכים לאחר פרק זמן מסוים. אתם בודקים את הקוד ולא רואים שום דבר חריג, עד שאתם פותחים את הכלים לבדיקת ביצועי הזיכרון ב-Chrome ורואים את זה:

אחד מהקולגות שלכם צוחק כי הוא מבין שיש לכם בעיית ביצועים שקשורה לזיכרון.
בתצוגת התרשים של הזיכרון, דפוס השיניים הזה מעיד על בעיית ביצועים קריטית פוטנציאלית. ככל שהשימוש בזיכרון יגדל, כך גם אזור התרשים יגדל בתיעוד של ציר הזמן. כשהתרשים יורד באופן פתאומי, זה סימן ש-Garbage Collector הופעל וניקה את אובייקטי הזיכרון שצוינו.

בתרשים כזה אפשר לראות שיש הרבה אירועי Garbage Collection, שיכולים להזיק לביצועים של אפליקציות האינטרנט. במאמר הזה נסביר איך לשלוט בשימוש בזיכרון, כדי לצמצם את ההשפעה על הביצועים.
עלויות איסוף אשפה וביצועים
מודל הזיכרון של JavaScript מבוסס על טכנולוגיה שנקראת אוסף אשפה. בשפות רבות, המתכנת אחראי ישירות להקצאת זיכרון ולשחרור זיכרון מMemory Heap של המערכת. עם זאת, מערכת של מנקה מנהלת את המשימה הזו בשם המתכנת, כלומר אובייקטים לא משוחררים ישירות מהזיכרון כשהמתכנת מבטל את ההפניה אליהם, אלא בשלב מאוחר יותר כשהאלגוריתמים ההיוריסטיים של מנקה הזבל מחליטים שזה כדאי. תהליך ההחלטה הזה מחייב את מנקה הזבל לבצע ניתוח סטטיסטי מסוים על אובייקטים פעילים ולא פעילים, והביצוע שלו נמשך זמן מה.
איסוף אשפה מוצג לעיתים קרובות כהפך לניהול ידני של זיכרון, שבו המתכנת צריך לציין אילו אובייקטים צריך לבטל את ההקצאה שלהם ולהחזיר למערכת הזיכרון.
התהליך שבו GC משחרר זיכרון הוא לא בחינם, ובדרך כלל הוא גורם לירידה בביצועים הזמינים על ידי הקצאת חלון זמן לביצוע העבודה. בנוסף, המערכת עצמה מחליטה מתי להריץ את התהליך. אין לכם שליטה על הפעולה הזו. פולס GC יכול להתרחש בכל שלב במהלך ביצוע הקוד, והוא יחסום את ביצוע הקוד עד שהוא יושלם. משך הזמן של הפולס הזה בדרך כלל לא ידוע לכם. זמן הריצה שלו תלוי באופן שבו התוכנית משתמשת בזיכרון בכל זמן נתון.
אפליקציות עם ביצועים גבוהים מסתמכות על גבולות ביצועים עקביים כדי להבטיח חוויה חלקה למשתמשים. מערכות של ניקוי אשפה עלולות לפגוע ביעד הזה, כי הן יכולות לפעול במועדים אקראיים למשך פרקי זמן אקראיים, וכך לקצץ בזמן הזמין לאפליקציה כדי לעמוד ביעדי הביצועים שלה.
הפחתת תנועת הנתונים בזיכרון, הפחתת המיסים על איסוף אשפה
כפי שצוין, פולס GC יתבצע לאחר שקבוצת שיטות ניתוח נתונים (heuristics) תזהה שיש מספיק אובייקטים לא פעילים כך שפולס יהיה מועיל. לכן, המפתח להקטנת משך הזמן ש-Garbage Collector לוקח מהאפליקציה הוא למנוע כמה שיותר מקרים של יצירה וביטול של אובייקטים באופן מוגזם. התהליך הזה של יצירה או שחרור של אובייקטים בתדירות גבוהה נקרא 'תנודות בזיכרון'. אם אתם יכולים לצמצם את התנודות בזיכרון במהלך מחזור החיים של האפליקציה, תוכלו גם לצמצם את משך הזמן של GC מההרצה. כלומר, צריך להסיר או לצמצם את מספר האובייקטים שנוצרו ונמחקו, כלומר, צריך להפסיק להקצות זיכרון.
התהליך הזה יעביר את תרשים הזיכרון מהמצב הזה :

לזה:

במודל הזה אפשר לראות שהתרשים כבר לא כולל תבנית של שיניים של מסור, אלא עלייה משמעותית בהתחלה, ואז עלייה איטית עם הזמן. אם נתקלת בבעיות בביצועים בגלל תנודות בשימוש בזיכרון, זהו סוג התרשים שצריך ליצור.
מעבר ל-JavaScript בזיכרון סטטי
Static Memory JavaScript היא טכניקה שכוללת הקצאה מראש, בתחילת האפליקציה, של כל הזיכרון שיידרש למשך כל משך החיים שלה, וניהול הזיכרון הזה במהלך הביצוע כשאין יותר צורך באובייקטים. אפשר להגיע ליעד הזה בכמה שלבים פשוטים:
- משתמשים בכלים למדידת ביצועים באפליקציה כדי לקבוע מהו המספר המקסימלי הנדרש של אובייקטים בזיכרון פעיל (לכל סוג) במגוון תרחישים לדוגמה של שימוש
- צריך להטמיע מחדש את הקוד כדי להקצות מראש את הסכום המקסימלי הזה, ולאחר מכן לאחזר אותם או לשחרר אותם באופן ידני במקום לעבור לזיכרון הראשי.
בפועל, כדי להשיג את היעד הראשון צריך לבצע חלק מהיעד השני, אז נתחיל שם.
מאגר אובייקטים
במילים פשוטות, אוסף אובייקטים הוא תהליך שמאפשר לשמור קבוצה של אובייקטים שלא בשימוש שיש להם סוג משותף. כשצריך אובייקט חדש לקוד, במקום להקצות אובייקט חדש מ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);
}
הסכום שבוחרים קשור מאוד להתנהגות של האפליקציה. לפעמים הסכום המקסימלי התיאורטי הוא לא האפשרות הטובה ביותר. לדוגמה, אם בוחרים באפשרות 'ממוצע מקסימלי', יכול להיות שגודל הזיכרון שנדרש יהיה קטן יותר למשתמשים שאינם משתמשים מתקדמים.
לא פתרון קסם
יש סיווג שלם של אפליקציות שבהן דפוסי הצטברות של זיכרון סטטי יכולים להיות יתרון. עם זאת, כפי שציין Renato Mangini, אחד מהמפתחים של Chrome DevRel, יש כמה חסרונות.
סיכום
אחת הסיבות לכך ש-JavaScript מתאימה במיוחד לאינטרנט היא שהיא שפה מהירה, מהנה וקלה לתחילת העבודה. הסיבה העיקרית לכך היא שהמגבלות על תחביר הן נמוכות, והמערכת מטפלת בבעיות הזיכרון בשבילכם. אתם יכולים להמשיך לכתוב קוד ולתת ל-AWS לבצע את העבודה השחורה. עם זאת, באפליקציות אינטרנט בעלות ביצועים גבוהים, כמו משחקי HTML5, GC יכול לעתים קרובות לפגוע בקצב הפריימים הנדרש באופן קריטי, וכתוצאה מכך לפגוע בחוויית המשתמש. בעזרת כלי מדידה זהירים והטמעה של מאגרי אובייקטים, תוכלו להפחית את העומס הזה על קצב הפריימים ולפנות את הזמן הזה לדברים מגניבים יותר.
קוד המקור
יש הרבה הטמעות של מאגרי אובייקטים באינטרנט, אז לא אעיק עליכם עם עוד אחת. במקום זאת, אפנה אתכם למאמרים הבאים, לכל אחד מהם יש ניואנסים ספציפיים להטמעה. חשוב לדעת את זה, כי לכל שימוש באפליקציה עשויות להיות צרכים ספציפיים להטמעה.
- מאגר האובייקטים של Gamecore.js
- Beej’s Object Pools
- מאגר אובייקטים פשוט במיוחד של Emehrkay
- מאגר אובייקטים שמתמקד במשחקים של סטיבן למברט
- הגדרת objectPool של RenderEngine