JavaScript של זיכרון סטטי עם מאגרי אובייקטים

Colt McAnlis
Colt McAnlis

מבוא

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

תמונת מצב מציר הזמן של הזיכרון

אחד מהקולגות שלכם צוחק כי הוא מבין שיש לכם בעיית ביצועים שקשורה לזיכרון.

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

מה המשמעות של השיניים

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

איסוף אשפה ועלויות ביצועים

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

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

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

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

הפחתת תנועת הנתונים בזיכרון, הפחתת המיסים על איסוף אשפה

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

התהליך הזה יעביר את תרשים הזיכרון מהמצב הזה:

תמונת מצב מציר הזמן של הזיכרון

לזה:

Static Memory 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);
}

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

רחוק מכדור כסף

יש סיווג שלם של אפליקציות שבהן דפוסים של גידול בזיכרון יכולים לנצח. עם זאת, כפי שציין Renato Mangini, אחד מהמפתחים של Chrome DevRel, יש כמה חסרונות.

סיכום

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

קוד המקור

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

קובצי עזר