צילום תמונות מצב של ערימה (heap snapshot)

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

איך מתעדים קובצי snapshot של ערימה (heap) בקטע זיכרון > פרופילים > קובץ snapshot של ערימה ומאתרים דליפות זיכרון.

ניתוח פרופיל הערימה מציג את התפלגות הזיכרון לפי אובייקטים של JavaScript וצומתי DOM קשורים בדף. אפשר להשתמש בו כדי לצלם קובצי snapshot של ערימה (heap) ב-JS, לנתח תרשימי זיכרון, להשוות בין קובצי snapshot ולמצוא דליפות זיכרון. מידע נוסף זמין במאמר Objects retaining tree.

צלם תמונה

כדי לצלם תמונת מצב של ערימה:

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

סוג יצירת הפרופיל שנבחר ומופע VM של JavaScript.

כשהחלונית זיכרון טוענת ומנתחת את קובץ snapshot, הגודל הכולל של אובייקטי JavaScript שניתנים להגיע אליהם מוצג מתחת לשם קובץ ה-snapshot בקטע תמונות מצב של ערימות (heap snapshot).

הגודל הכולל של אובייקטים שאפשר להגיע אליהם.

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

תמונת מצב של אשכול של אובייקטים מפוזרים מסוג Item.

מחיקת קובצי snapshot

כדי להסיר את כל קובצי ה-snapshot, לוחצים על ניקוי כל הפרופילים:

ניקוי כל הפרופילים.

הצגת קובצי snapshot

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

הצגה תוכן מטרה
סיכום אובייקטים שמקובצים לפי שמות של constructor. אפשר להשתמש בו כדי לאתר אובייקטים ואת השימוש שלהם בזיכרון לפי סוג. המידע הזה שימושי למעקב אחרי דליפות DOM.
השוואה ההבדלים בין שתי תמונות מצב. אפשר להשתמש בו כדי להשוות בין שתי תמונות מצב (או יותר), לפני פעולה ואחריה. כדי לוודא שיש דליפת זיכרון ולזהות את הסיבה שלה, בודקים את השינוי בזיכרון שהתפנה ובמספר ההפניות.
אחסון בקונטיינר תוכן הערימה תצוגה טובה יותר של מבנה האובייקטים, וניתוח אובייקטים שיש אליהם הפניה במרחב השמות הגלובלי (window) כדי לגלות מה משאיר אותם בסביבה. אפשר להשתמש בו כדי לנתח חסימה ולעיין באובייקטים ברמה נמוכה.
נתונים סטטיסטיים תרשים עוגה של הקצאת זיכרון הצגת הגדלים בפועל של חלקי זיכרון שמוקצים לקוד, למחרוזות, למערכי JS, למערכים מסוגננים ולאובייקטים של המערכת.

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

תצוגת סיכום

בהתחלה, תיפתח תמונת מצב של הערימה (heap snapshot) בתצוגה Summary (סיכום) שבה רשומים בניינים בעמודה. אפשר להרחיב את המאפיינים ה-constructor כדי לראות את האובייקטים שהם יוצרים.

התצוגה 'סיכום' עם constructor מורחב.

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

המספרים לצד שמות המשתנים של ה-constructor מציינים את המספר הכולל של העצמים שנוצרו באמצעות ה-constructor. בתצוגה סיכום מוצגות גם העמודות הבאות:

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

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

מסנני יוצרים

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

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

  • כל האובייקטים: כל האובייקטים שתועדו על ידי קובץ ה-snapshot. מוגדר כברירת מחדל.
  • Objects allocated before snapshot 1: אובייקטים שנוצרו ונשארו בזיכרון לפני שצילמתם את קובץ ה-snapshot הראשון.
  • Objects allocated between Snapshots 1 and Snapshots 2: הצגת ההבדל בין האובייקטים בתמונת המצב האחרונה לבין תמונת המצב הקודמת. כל קובץ snapshot חדש מוסיף לרשימה הנפתחת סכום מצטבר של המסנן הזה.
  • מחרוזות כפולות: ערכי מחרוזות שנשמרו כמה פעמים בזיכרון.
  • אובייקטים שנשמרים על ידי צומתי DOM מנותקים: אובייקטים שנשמרים כי צומת DOM מנותק מפנה אליהם.
  • אובייקטים שנשמרו על ידי מסוף כלי הפיתוח: אובייקטים שנשמרו בזיכרון כי הם עברו הערכה או שהתרחשה אינטראקציה איתם דרך מסוף כלי הפיתוח.

רשומות מיוחדות בקטע 'סיכום'

בנוסף לקיבוע לפי קונסטרוקטורים, בתצוגה סיכום מתבצע גם קיבוץ של אובייקטים לפי:

  • פונקציות מובנות כמו Array או Object.
  • רכיבי HTML שמקובצים לפי התגים שלהם. לדוגמה, <div>, <a>, <img> ועוד.
  • פונקציות שהגדרתם בקוד.
  • קטגוריות מיוחדות שלא מבוססות על קונסטרוקטורים.

רשומות של קונסטרוקטור.

(array)

הקטגוריה הזו כוללת אובייקטים פנימיים שונים דמויי מערך שלא תואמים ישירות לאובייקטים שגלויים ב-JavaScript.

לדוגמה, התוכן של אובייקטים מסוג Array ב-JavaScript מאוחסן באובייקט פנימי משני בשם (object elements)[], כדי לאפשר שינוי קל יותר של הגודל. באופן דומה, המאפיינים שהוזכרו באובייקטי JavaScript מאוחסנים לעיתים קרובות באובייקטים פנימיים משניים בשם (object properties)[] שמופיעים גם בקטגוריה (array).

(compiled code)

הקטגוריה הזו כוללת נתונים פנימיים שנדרשים ל-V8 כדי להריץ פונקציות שהוגדרו על ידי JavaScript או WebAssembly. אפשר לייצג כל פונקציה במגוון דרכים, החל מקטנה ואיטית ועד גדולה ומהירה.

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

(concatenated string)

כש-V8 משרשרת שתי מחרוזות, למשל עם האופרטור + של JavaScript, הוא יכול לבחור לייצג את התוצאה באופן פנימי בתור 'מחרוזת משורשרת', שנקראת גם מבנה הנתונים Rope.

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

InternalNode

הקטגוריה הזו מייצגת אובייקטים שהוקצתה מחוץ ל-V8, כמו אובייקטים של C++‎ שהוגדרו על ידי Blink.

כדי לראות את שמות הכיתות ב-C++‎, משתמשים ב-Chrome for Testing ומבצעים את הפעולות הבאות:

  1. פותחים את כלי הפיתוח ומפעילים את הגדרות > ניסויים > הצגת אפשרות לחשוף נתונים פנימיים בתמונות מצב של אשכול.
  2. פותחים את החלונית Memory, בוחרים באפשרות Heap snapshot ומפעילים את האפשרות Expose internals (includes additional implementation-specific details).
  3. משחזרים את הבעיה שגרמה ל-InternalNode לשמור הרבה זיכרון.
  4. צילום תמונת מצב של הזיכרון. בתמונת המצב הזו, לאובייקטים יש שמות של כיתות ב-C++ במקום InternalNode.
(object shape)

כפי שמתואר במאמר מאפיינים מהירים ב-V8, ‏V8 עוקב אחרי כיתות מוסתרות (או צורות) כדי שאפשר יהיה לייצג ביעילות כמה אובייקטים עם אותם מאפיינים באותו סדר. הקטגוריה הזו מכילה את הכיתות המוסתרות האלה, שנקראות system / Map (לא קשורות ל-JavaScript Map), ונתונים קשורים.

(sliced string)

כש-V8 צריך לקחת מחרוזת משנה, למשל כשקוד JavaScript קורא ל-String.prototype.substring(), V8 עשוי להקצות אובייקט של מחרוזת חתוכה במקום להעתיק את כל התווים הרלוונטיים מהמחרוזת המקורית. האובייקט החדש הזה מכיל פוינטר למחרוזת המקורית ומתאר את טווח התווים מהמחרוזת המקורית שבו צריך להשתמש.

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

system / Context

אובייקטים פנימיים מסוג system / Context מכילים משתנים מקומיים מסגירה – היקף של JavaScript שאליו פונקציה בתצוגת עץ יכולה לגשת.

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

(system)

הקטגוריה הזו מכילה אובייקטים פנימיים שונים שעדיין לא סווגו (עדיין) באופן משמעותי יותר.

תצוגת השוואה

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

כדי לוודא שפעולה מסוימת לא יוצרת דליפות:

  1. צילום תמונת מצב של ערימה (heap snapshot) לפני ביצוע פעולה.
  2. מבצעים פעולה. כלומר, לבצע אינטראקציה עם דף באופן כלשהו שלדעתכם עלול לגרום לדליפת מידע.
  3. ביצוע פעולה הפוכה. כלומר, מבצעים את האינטראקציה ההפוכה וחוזרים עליה כמה פעמים.
  4. יוצרים קובץ snapshot נוסף של אשכול ומחליפים את התצוגה שלו להשוואה, ומשווים אותו לSnapshot 1.

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

השוואה לתמונת המצב 1.

תצוגת 'בלימה'

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

לתצוגה יש כמה נקודות כניסה:

  • אובייקטים מסוג DOMWindow. אובייקטים גלובליים לקוד JavaScript.
  • שורשי GC. בסיסים של GC שמשמשים את אוסף האשפה של המכונה הווירטואלית. שורשי GC יכולים להכיל מפות אובייקטים מובנות, טבלאות סמלים, סטאקים של חוטים של מכונות וירטואליות, מטמון של הידור, היקפי טיפולים ומטפלים גלובליים.
  • אובייקטים מקומיים. אובייקטים של דפדפן 'נדחפים' לתוך המכונה הווירטואלית של JavaScript כדי לאפשר אוטומציה, למשל צומתי DOM וכללי CSS.

תצוגת המקום המארח.

הקטע Retainers

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

הקטע &#39;שימורים&#39;.

בדוגמה הזו, המחרוזת שנבחרה נשמרת בנכס x של מכונה Item.

התעלמות מריטיינרים

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

האפשרות &#39;התעלמות מההתחייבות הזו&#39; בתפריט הנפתח.

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

חיפוש אובייקט ספציפי

כדי למצוא אובייקט ב-heap שנאסף, אפשר לחפש באמצעות Ctrl + F ולהזין את מזהה האובייקט.

מתן שמות לפונקציות כדי להבדיל בין פונקציות סגורות

כדאי לתת שמות לפונקציות כדי שתוכלו להבחין בין נעילה לנעילה בתמונת המצב.

לדוגמה, הקוד הבא לא משתמש בפונקציות בעלות שם:

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function() { // this is NOT a named function
    return largeStr;
  };

  return lC;
}

לעומת זאת, בדוגמה הזו:

function createLargeClosure() {
  var largeStr = new Array(1000000).join('x');

  var lC = function lC() { // this IS a named function
    return largeStr;
  };

  return lC;
}

פונקציה בעלת שם בתוך קלוזורה.

חשיפת דליפות ב-DOM

לכלי לניתוחי נתונים של אשכול יש יכולת לשקף יחסי תלות דו-כיווניים בין אובייקטים מבוססי-דפדפן (צומתי DOM וכללי CSS) לבין אובייקטים של JavaScript. כך אפשר לגלות דליפות שבלעדיהן לא היו גלויות, בגלל עצים משניים של DOM ששכחו לנתק.

דליפות DOM יכולות להיות גדולות יותר ממה שאתם חושבים. הנה דוגמה. מתי מתבצע האיסוף של האשפה ב-#tree?

  var select = document.querySelector;
  var treeRef = select("#tree");
  var leafRef = select("#leaf");
  var body = select("body");

  body.removeChild(treeRef);

  //#tree can't be GC yet due to treeRef
  treeRef = null;

  //#tree can't be GC yet due to indirect
  //reference from leafRef

  leafRef = null;
  //#NOW #tree can be garbage collected

#leaf שומר על הפניה להורה שלו (parentNode) ועל הפניה חזרה עד #tree, כך שרק כשleafRef מבוטל, העץ השלם שמתחת ל-#tree הוא מועמד ל-GC.

ענפי משנה של DOM