איך אפשר להאיץ פי 10 את דוחות הקריסות בכלי הפיתוח ל-Chrome

בנדיקט מורר
בנדיקט מורר

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

העובדה שהירידה הזו בביצועים כבר לא מורגשת, היא תוצאה של שנים של השקעה ביכולות ניפוי באגים של DevTools ו-V8. עם זאת, לעולם לא נוכל להפחית את תקורת הביצועים של כלי הפיתוח לאפס. הגדרת נקודות עצירה (breakpoint), מעבר דרך הקוד, איסוף דוחות קריסות, תיעוד של מעקב ביצועים וכו', משפיעים על מהירות הביצוע במידה שונה. אחרי הכל, התבוננות במשהו משנה אותו.

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

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

זמן הביצוע של כלי הרינדור של Chrome

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

השערה של שם השיטה

מה שהפתיעה עוד יותר הייתה שכמעט כל הזמן מגיע לפונקציה JSStackFrame::GetMethodName() ב-V8 - על אף שידענו מחקירות קודמות ש-JSStackFrame::GetMethodName() אינו גורם זר במדינה של בעיות ביצועים. הפונקציה הזו מנסה לחשב את שם השיטה עבור מסגרות שנחשבות להפעלת שיטה (מסגרות שמייצגות הפעלות של פונקציות בצורה obj.func() ולא של func()). מבט מהיר בקוד גילה שהשיטה עובדת על ידי ביצוע מעבר מלא של האובייקט ושרשרת אב הטיפוס שלו, וחיפוש

  1. נכסי נתונים שהvalue שלהם היא הסגירה של func, או
  2. מאפייני גישה (accessor) כאשר get או set שווה לסגירה של func.

עכשיו, אף על פי שזה לא נשמע זול במיוחד, לא נשמע שזה יכול להסביר את ההאטה האיומה הזו. לכן, התחלנו להתעמק בדוגמה שדווחה ב-chromium:1069425 וגילינו שהנתונים לגבי מעקבי קריסות נאספו עבור משימות אסינכרוניות וכן עבור הודעות ביומן שמקורן ב-classes.js – קובץ JavaScript בגודל 10MiB. בחינה מדוקדקת יותר גילתה שלמעשה מדובר בקוד זמן ריצה של Java עם קוד אפליקציה שהותאם ל-JavaScript. דוחות הקריסה כללו מספר פריימים עם שיטות שהופעלו על אובייקט A, אז חשבנו שכדאי להבין באיזה סוג של אובייקט אנחנו מתמודדים.

דוחות קריסות של אובייקט

נראה שהידור Java ל-JavaScript יצר אובייקט יחיד שמכיל 82,203 פונקציות אדירות - זה התחיל להיות מעניין. אחר כך חזרנו לJSStackFrame::GetMethodName() של V8 כדי להבין אם אפשר לבחור שם מאמץ פירות.

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

בדוגמה שלנו, כל הפונקציות הן אנונימיות ויש להן מאפייני "name" ריקים.

A.SDV = function() {
   // ...
};

הממצא הראשון היה שהחיפוש ההפוך פוצל לשני שלבים (בוצעו עבור האובייקט עצמו ועבור כל אובייקט בשרשרת אב הטיפוס שלו):

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

זה נראה כמו פרי תלוי למדי, כי חילוץ השמות כבר מחייב מעבר הליכה בין כל הנכסים. במקום לבצע את שני המעברים – O(N) לחילוץ השם ו-O(N Log(N)) לבדיקות – אנחנו יכולים לעשות הכול במעבר אחד ולבדוק ישירות את ערכי המאפיין. כך הפונקציה עובדת בערך 2-10x מהר יותר.

הממצא השני היה מעניין עוד יותר. למרות שהפונקציות היו פונקציות אנונימיות מבחינה טכנית, מנוע ה-V8 תיעד מה שאנחנו מכנים שם משוער להן. בליטרלים של פונקציות שמופיעים בצד שמאל של המטלות בצורה obj.foo = function() {...}, מנתח V8 משנן את "obj.foo" כשם משוער לליטרל של הפונקציה. כלומר, במקרה שלנו, למרות שלא היה לנו שם עצם פרטי שהיינו יכולים פשוט לחפש, היה לנו משהו קרוב מספיק: בדוגמה A.SDV = function() {...} שלמעלה, היה לנו "A.SDV" כשם המשוער, והיינו יכולים לחלץ את שם הנכס מהשם המשוער על ידי חיפוש הנקודה האחרונה, ולאחר מכן לחפש את הנכס "SDV" באובייקט. זה הצליח כמעט בכל המקרים והחליף מעבר מלא ויקר בחיפוש אחד של נכס. שני השיפורים האלה הניבו חלק מ-CL ו הפחיתו באופן משמעותי את ההאטה בדוגמה שדווחה ב-chromium:1069425.

Error.stack

יכול להיות שהיינו קוראים לזה יום כאן. אבל במקום משהו חשוד, כלי הפיתוח לא משתמשים אף פעם בשם השיטה למסגרות של מקבצים. למעשה, המחלקה v8::StackFrame ב-API של C++ לא חושפת אפילו דרך להגיע לשם השיטה. נראה שזה לא נכון במקרה שאנחנו מתקשרים אל JSStackFrame::GetMethodName() מלכתחילה. במקום זאת, המקום היחיד שבו אנחנו משתמשים בשם השיטה (וחושפים אותה) נמצא ב-JavaScriptStack Tracking API. כדי להבין את השימוש הזה, אפשר להיעזר בדוגמה הפשוטה הבאה error-methodname.js:

function foo() {
    console.log((new Error).stack);
}

var object = {bar: foo};
object.bar();

כאן יש לנו את הפונקציה foo שמותקנת מתחת לשם "bar" ב-object. הרצת קטע הקוד ב-Chromium מניבה את הפלט הבא:

Error
    at Object.foo [as bar] (error-methodname.js:2)
    at error-methodname.js:6

כאן מוצג חיפוש שם השיטה בזמן ההפעלה: מסגרת המקבצים העליונה מוצגת כקריאה לפונקציה foo במופע של Object באמצעות השיטה bar. לכן בנכס error.stack הלא סטנדרטי נעשה שימוש רב ב-JSStackFrame::GetMethodName(), ולמעשה, בדיקות הביצועים שלנו מצביעות גם על כך שהשינויים שלנו הפכו את הדברים למהירים יותר משמעותית.

האצת מהירות על נקודות השוואה מיקרו של StackTrace

אבל בנוגע לנושא של כלי הפיתוח ל-Chrome, נראה שהעובדה ששם השיטה מחושב על אף שלא נעשה שימוש ב-error.stack לא נראה תקין. יש כאן היסטוריה שעוזרת לנו: בעבר, ל-V8 היו שני מנגנונים נפרדים לאיסוף ולייצוג של דוח קריסות לשני ממשקי ה-API השונים שתוארו למעלה (ה-API של v8::StackFrame C++ וה-API של מעקב הקריסות ב-JavaScript). האפשרות לבצע את אותה שתי דרכים (כמעט) הייתה מובילה לשגיאות ולעיתים קרובות גרמה לחוסר עקביות ולבאגים. לכן, בסוף 2018 פתחנו פרויקט שבו החלטנו על צוואר בקבוק אחד לשמירה על תיעוד של דוחות קריסות.

הפרויקט הזה זכה להצלחה גדולה והפחית באופן משמעותי את מספר הבעיות שקשורות לאיסוף של דוחות קריסות. רוב המידע שסופק באמצעות המאפיין error.stack הלא סטנדרטי חושב גם הוא בצורה מדורגת ורק כשזה היה נחוץ באמת, אבל כחלק מהארגון מחדש (Refactoring) החלנו את אותו הטריק על v8::StackFrame אובייקטים. כל המידע על מסגרת המחסנית מחושב בפעם הראשונה שהופעלה בה שיטה כלשהי.

באופן כללי, הפעולה הזו משפרת את הביצועים, אבל לצערנו התברר שהיא מנוגדת לאופן שבו נעשה שימוש באובייקטים האלה של API מסוג C++ ב-Chromium ובכלי פיתוח. באופן ספציפי, מכיוון שהצגנו מחלקה חדשה של v8::internal::StackFrameInfo שכללה את כל המידע על מסגרת מחסנית שנחשפה דרך v8::StackFrame או דרך error.stack, תמיד נחשבנו את קבוצת-העל של המידע שסופק על ידי שני ממשקי ה-API. כלומר, עבור השימושים ב-v8::StackFrame (ובמיוחד בכלי הפיתוח) היינו מחשבים את שם השיטה ברגע שנקבל מידע על מסגרת ערימה. מתברר שכלי הפיתוח תמיד מבקשים באופן מיידי פרטי מקור וסקריפט.

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

שמות הפונקציות

בעזרת ארגון מחדש של הערכים שצוינו למעלה, תקורה של סימולציית סימולציית נתונים (משך השימוש ב-v8_inspector::V8Debugger::symbolize) הופחתה לכ-15% מזמן הביצוע הכולל, והצלחנו לראות בצורה ברורה יותר איפה ב-V8 השקיעו זמן (איסוף ו) סימל מסגרות של מקבץ לצריכה ב-DevTools.

עלות הסמלים

הדבר הראשון שבלט היה העלות המצטברת עבור חישוב מספרי השורות והעמודות. החלק היקר כאן הוא למעשה חישוב קיזוז התווים בסקריפט (על סמך קיזוז התווים בסקריפט (על סמך קיזוז ה-bytecode שאנחנו מקבלים מ-V8), ומסתבר שבעקבות ארגון הקוד מחדש שלמעלה, עשינו זאת פעמיים, פעם אחת במהלך חישוב מספר השורה ופעם אחרת עבור חישוב מספר העמודה. שמירת מיקום המקור במטמון במכונות v8::internal::StackFrameInfo עזרה לפתור את הבעיה במהירות וביטלת לחלוטין את v8::internal::StackFrameInfo::GetColumnNumber מהפרופילים.

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

  1. קודם מחפשים את הנכס "displayName" הלא סטנדרטי, ואם הוא יניב נכס נתונים עם ערך מחרוזת, נשתמש בו
  2. אם לא מחפשים את נכס "name" הרגיל ואז בודקים שוב אם נוצר נכס נתונים שהערך שלו הוא מחרוזת,
  3. ובסופו של דבר חוזרים לשם פנימי של ניפוי באגים שמוסק על ידי המנתח של V8 ומאוחסן בליטרל של הפונקציה.

הנכס "displayName" נוסף כפתרון זמני עבור המאפיין "name" במכונות Function שבהן נעשה שימוש לקריאה בלבד ולא ניתן להגדיר אותו ב-JavaScript. עם זאת, הוא אף פעם לא עבר סטנדרטי ולא זוהה שימוש בהיקף רחב, כי הכלים למפתחים של הדפדפן הוסיפו מסקנות של שם הפונקציות שעושות את העבודה ב-99.9% מהמקרים. בנוסף ל-ES2015, המאפיין "name" במכונות Function ניתן להגדרה, וכך ביטל לחלוטין את הצורך בנכס "displayName" מיוחד. החיפוש השלילי של "displayName" הוא יקר ולא באמת נחוץ (ES2015 הושק לפני יותר מחמש שנים), לכן החלטנו להסיר את התמיכה בנכס fn.displayName הלא סטנדרטי מ-V8 (ומכלי פיתוח).

לאחר חיפוש שלילי של "displayName", הוסרה מחצית מהעלות של v8::StackFrame::GetFunctionName. החצי השני עובר לחיפוש הכללי של המאפיין "name". למרבה המזל, כבר היינו צריכים להשתמש בהיגיון מסוים כדי למנוע חיפוש יקר של נכס "name" במכונות (ללא שינוי) של Function, שהשקנו ב-V8 לפני זמן מה כדי להפוך את Function.prototype.bind() למהיר יותר. העברנו את הבדיקות הנדרשות, שמאפשרות לנו לדלג על החיפוש הגנרי היקר מלכתחילה. כתוצאה מכך, v8::StackFrame::GetFunctionName לא מופיע יותר באף פרופיל שחשבנו עליו.

סיכום

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

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

מורידים את הערוצים של התצוגה המקדימה.

כדאי להשתמש ב-Chrome Canary, Dev או בטא כדפדפן הפיתוח שמוגדר כברירת מחדל. ערוצי התצוגה המקדימה האלה מעניקים לך גישה לתכונות החדשות של כלי הפיתוח, בודקים ממשקי API מתקדמים של פלטפורמת האינטרנט ומוצאים בעיות באתר לפני שהמשתמשים נתקלים בבעיות!

יצירת קשר עם צוות כלי הפיתוח ל-Chrome

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

  • שלחו לנו הצעה או משוב בכתובת crbug.com.
  • כדי לדווח על בעיה בכלי הפיתוח, לוחצים על אפשרויות נוספות   עוד   > עזרה > דיווח על בעיות בכלי הפיתוח בכלי הפיתוח.
  • אפשר לשלוח ציוץ אל @ChromeDevTools.
  • אפשר לכתוב תגובות לגבי 'מה חדש' בסרטוני YouTube או בקטע 'טיפים לשימוש בכלי הפיתוח' בסרטוני YouTube.