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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

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

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

צלם תמונה

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

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

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

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

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

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

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

מחיקת קובצי snapshot

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

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

הצגת קובצי snapshot

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

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

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

תצוגת סיכום

בהתחלה, ייפתח קובץ snapshot של אשכול בתצוגה Summary, עם רשימה של Constructors בעמודה. אפשר להרחיב את ה-constructors כדי לראות את האובייקטים שהם יוצרים.

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

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

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

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

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

מסנני יוצרים

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

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

  • כל האובייקטים: כל האובייקטים שצולמו בתמונת המצב הנוכחית. מוגדר כברירת מחדל.
  • Objects allocated before snapshot 1: אובייקטים שנוצרו ונשארו בזיכרון לפני שצילמתם את קובץ ה-snapshot הראשון.
  • Objects allocated between Snapshots 1 and Snapshots 2: הצגת ההבדל באובייקטים בין קובץ ה-snapshot האחרון לבין קובץ ה-snapshot הקודם. כל קובץ 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 לבדיקה ופועלים לפי השלבים הבאים:

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

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

תצוגת השוואה

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

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

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

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

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

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

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

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

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

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

הקטע Retainers

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

הקטע Retainers (מקזזים).

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

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

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

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

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

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

כדי למצוא אובייקט ב-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