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

חוויה משופרת לניפוי באגים

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

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

קוד להוספה לרשימת ההתעלמות

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

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

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

GIF מונפש שמציג את כלי הפיתוח לפני ואחרי. שימו לב שבתמונה שלאחר השינוי, קוד המחבר מוצג בכלי הפיתוח בעץ, לא מוצגות יותר הצעות לקובצי המסגרת בתפריט 'פתיחה מהירה', ומוצגת בצד שמאל נתיב סטאק נקי יותר.

התוסף של מפת המקור x_google_ignoreList

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

בהמשך מופיעה מפת מקור של קובץ שנוצר, out.js. יש שני קבצים מקוריים מסוג sources שתרמו ליצירת קובץ הפלט: foo.js ו-lib.js. הקוד הראשון הוא קוד שכתב מפתח אתר, והקוד השני הוא מסגרת שבה הוא השתמש.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

ה-sourcesContent נכלל בשני המקורות המקוריים האלה, וקבצים אלה יוצגו כברירת מחדל בכלי הפיתוח ל-Chrome בכלי לניפוי הבאגים:

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

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

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

השדה x_google_ignoreList החדש מכיל אינדקס יחיד שמפנה למערך sources: 1. המשמעות היא שהאזורים שממופים אל lib.js הם למעשה קוד של צד שלישי שצריך להוסיף באופן אוטומטי לרשימת הפריטים להתעלמות.

בדוגמה מורכבת יותר, שמוצגת בהמשך, המדדים 2, 4 ו-5 מציינים שהאזורים שממופים אל lib1.ts,‏ lib2.coffee ו-hmr.js הם כולם קוד של צד שלישי שצריך להוסיף באופן אוטומטי לרשימת ההתעלמות.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

מפתחי framework או Bundler צריכים לוודא שמפות המקור שנוצרו במהלך תהליך ה-build כוללות את השדה הזה כדי להתחבר ליכולות החדשות האלה בכלי הפיתוח ל-Chrome.

x_google_ignoreList ב-Angular

החל מ-Angular v14.1.0, התוכן של התיקיות node_modules ו-webpack סומן כ'לצורך התעלמות'.

השינוי בוצע באמצעות שינוי ב-angular-cli על ידי יצירת פלאגין שמתחבר למודול Compiler של Webpack

הפלאגין של Webpack שהמהנדסים שלנו יצרו בשלב PROCESS_ASSETS_STAGE_DEV_TOOLING ומאכלס את השדה x_google_ignoreList במפות המקור עבור הנכסים הסופיים ש-Webpack יוצר והדפדפן נטען.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

דוחות קריסות מקושרים

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

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

כדי לטפל בבעיה הזו, DevTools חושף מנגנון שנקרא 'Async Stack Tagging API' באובייקט console, שמאפשר למפתחי המסגרת להציע רמזים לגבי המיקומים שבהם הפעולות מתוזמנות וגם לגבי המיקומים שבהם הפעולות האלה מתבצעות.

Async Stack Tagging API

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

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

בעזרת Async Stack Tagging אפשר לספק את ההקשר הזה, ודוח הקריסות האסינכרוני נראה כך:

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

כדי לעשות זאת, משתמשים בשיטה חדשה של console בשם console.createTask() ש-Async Stack Tagging API מספק. החתימה שלו היא:

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

קריאה ל-console.createTask() מחזירה מופע של Task, שאפשר להשתמש בו מאוחר יותר כדי להריץ את הקוד האסינכרוני.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

אפשר גם להטמיע פעולות אסינכרוניות בתוך פעולות אחרות, ו'הסיבות העיקריות' יוצגו ברצף ב-stack trace.

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

ה-API של תיוג מקבץ אסינכרוני ב-Angular

ב-Angular בוצעו שינויים ב-NgZone – הקשר הביצוע של Angular שנשמר במשימות אסינכררוניות.

כשמתזמנים משימה, המערכת משתמשת ב-console.createTask() אם הוא זמין. המכונה Task שנוצרת מאוחסנת לשימוש נוסף. כשמפעילים את המשימה, ‏NgZone משתמש במכונה השמורה של Task כדי להריץ אותה.

השינויים האלה נוספו ל-NgZone 0.11.8 של Angular באמצעות בקשות משיכה (pull requests) #46693 ו-#46958.

מסגרות ידידותיות לשיחות

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

ב-Angular, לא נדיר לראות מסגרות קריאה עם שמות כמו AppComponent_Template_app_button_handleClick_1_listener בנתוני מעקב הסטאק.

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

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

מסגרות שיחה ידידותיות בזווית

שינוי השם של פריימים של שיחות ב-Agular הוא מאמץ מתמשך. אנחנו צופים שהשיפורים האלה יתווספו בהדרגה עם הזמן.

במהלך הניתוח של תבניות ה-HTML שהכותבים כתבו, המהדר של Angular יוצר קוד TypeScript, שבסופו של דבר עובר תרגום לקוד JavaScript שהדפדפן טוען ומריץ.

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

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

במבט קדימה

השימוש ב-Angular כתוכנית טייס לבדיקה של העבודה שלנו היה חוויה נהדרת. נשמח לשמוע ממפתחי המסגרות ולקבל משוב על נקודות ההרחבה האלה.

יש עוד תחומים שאנחנו רוצים לבדוק. במיוחד, איך לשפר את חוויית היצירה של פרופילים ב-DevTools.