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

Meggin Kearney
Meggin Kearney
Sofia Emelianova
Sofia Emelianova

מידע נוסף על צילום תמונות מצב של הזיכרון (heap snapshot) באמצעות האפשרות זיכרון > פרופילים > תמונת מצב של הזיכרון ואיתור דליפות זיכרון.

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

צלם תמונה

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

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

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

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

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

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

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

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

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

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

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

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

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

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

תצוגת סיכום

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

תצוגת גבול

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

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

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

התצוגה &#39;מקום מארח&#39;.

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

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

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

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

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

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

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

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

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

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

תתי-עצים של DOM