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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

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

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

צלם תמונה

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

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

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

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

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

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

תמונת מצב של ערימה (heap snapshot) של אובייקטים מפוזרים של פריטים.

ניקוי תמונות מצב

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

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

הצגת תמונות מצב

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

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

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

תצוגת סיכום

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

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

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

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

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

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

מסננים לבנייה

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

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

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

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

בנוסף לקיבוץ לפי בונים, התצוגה Summary (סיכום) מקבצת אובייקטים לפי:

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

רשומות של בונה.

(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. פותחים את החלונית זיכרון, בוחרים באפשרות radio_button_checked תמונת מצב של הזיכרון ומפעילים את radio_button_checked חשיפה של רכיבים פנימיים (כולל פרטים נוספים שספציפיים להטמעה).
  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. צילום תמונת מצב של הזיכרון לפני ביצוע פעולה.
  2. ביצוע פעולה. כלומר, כדאי לבצע פעולות בדף באופן שלדעתכם עלול לגרום לדליפה.
  3. מבצעים פעולה הפוכה. כלומר, מבצעים את האינטראקציה ההפוכה וחוזרים עליה כמה פעמים.
  4. מצלמים תמונת מצב נוספת של הזיכרון ומשנים את התצוגה להשוואה, ומשווים אותה ל-Snapshot 1.

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

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

תצוגת גבול

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

התצוגה מספקת מספר נקודות כניסה:

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

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

הקטע 'שימורים'

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

הקטע 'שימורים'.

בדוגמה הזו, המחרוזת שנבחרה נשמרת במאפיין x של מופע Item.

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

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

האפשרות 'התעלמות מהריטיינר הזה' בתפריט הנפתח.

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

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

כדי למצוא אובייקט בערימה שנאספת, אפשר לחפש באמצעות 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

כלי ליצירת תמונת מצב של ערימה (heap profiler) מאפשר לשקף יחסי תלות דו-כיווניים בין אובייקטים מותאמים בדפדפן (צומתי 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