ניפוי באגים ב-WebAssembly באמצעות כלים מודרניים

Ingvar Stepanyan
Ingvar Stepanyan

הדרך עד עכשיו

לפני שנה, הכרזנו על תמיכה ראשונית בניפוי באגים של WebAssembly מקורי בכלי הפיתוח של Chrome.

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

  • פתרון של שמות של משתנים
  • סוגי הדפסה יפה
  • הערכת ביטויים בשפות המקור
  • …ועוד הרבה יותר!

היום אנחנו שמחים להציג את התכונות שהובטחו, ואת ההתקדמות שצוותי Emscripten ו-Chrome DevTools עשו במהלך השנה, במיוחד לאפליקציות C ו-C++‎.

לפני שנתחיל, חשוב לזכור שזו עדיין גרסה בטא של הממשק החדש. אתם צריכים להשתמש בגרסה האחרונה של כל הכלים על אחריותכם, ואם נתקלתם בבעיות, תוכלו לדווח עליהן בכתובת https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

נתחיל באותה דוגמה פשוטה ל-C כמו בפעם הקודמת:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

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

emcc -g temp.c -o temp.html

עכשיו אפשר להציג את הדף שנוצר משרת HTTP מקומי (למשל, באמצעות serve) ולפתוח אותו ב-Chrome Canary בגרסה האחרונה.

הפעם נצטרך גם תוסף עזרה שמשתלב עם Chrome DevTools ומסייע להבין את כל פרטי ניפוי הבאגים שמקודדים בקובץ WebAssembly. כדי להתקין אותו, נכנסים לקישור הבא: goo.gle/wasm-debugging-extension

מומלץ גם להפעיל ניפוי באגים של WebAssembly בקטע ניסויים ב-DevTools. פותחים את כלי הפיתוח ל-Chrome, לוחצים על סמל גלגל השיניים () בפינה הימנית העליונה של חלונית כלי הפיתוח, עוברים לחלונית Experiments ומסמנים את התיבה WebAssembly Debugging: Enable DWARF support.

חלונית הניסויים בהגדרות של כלי הפיתוח

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

עכשיו אפשר לחזור לחלונית Sources, להפעיל את האפשרות Pause on exceptions (סמל ⏸), לסמן את האפשרות Pause on caught exceptions ולטעון מחדש את הדף. כלי הפיתוח אמורים להשהות את הבדיקה בחריג:

צילום מסך של חלונית המקורות שבו מוסבר איך מפעילים את האפשרות &#39;השהיה בחריגות שזוהו&#39;

כברירת מחדל, הבדיקה נעצרת בקוד הדבקה שנוצר על ידי Emscripten, אבל בצד שמאל מוצגת תצוגה של Call Stack שמייצגת את stacktrace של השגיאה, וניתן לנווט לשורת ה-C המקורית שהפעילה את abort:

DevTools הושהה בפונקציה assert_less ומוצגים הערכים של x ו-y בתצוגת ההיקף

עכשיו בתצוגה Scope אפשר לראות את השמות והערכים המקוריים של המשתנים בקוד C/C++ , וכבר לא תצטרכו להבין מה המשמעות של שמות לא מעוותים כמו $localN ומהם הקשרים שלהם לקוד המקור שכתבתם.

הכלל הזה חל לא רק על ערכים פרימיטיביים כמו מספרים שלמים, אלא גם על סוגי נתונים מורכבים כמו מבנים, כיתות, מערכי נתונים וכו'.

תמיכה בסוגים מתקדמים

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

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

אפשר לראות שהאפליקציה הזו עדיין קטנה למדי – היא קובץ יחיד שמכיל 50 שורות קוד – אבל הפעם השתמשתי גם בממשקי API חיצוניים, כמו ספריית SDL לצורכי גרפיקה וגם מספרים מורכבים מהספרייה הרגילה של C++‎.

אעבדק אותו עם אותו הדגל -g כמו למעלה כדי לכלול מידע על ניפוי באגים, ואבקש מ-Emscripten לספק את ספריית SDL2 ולאפשר שימוש בזיכרון בגודל שרירותי:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

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

דף הדגמה

כשפותחים את כלי הפיתוח, שוב רואים את קובץ ה-C++ המקורי. הפעם, עם זאת, אין שגיאה בקוד (וואו!), אז במקום זאת נגדיר נקודת עצירה בתחילת הקוד.

כשנטען מחדש את הדף, מנתח הבאגים יעצור ממש בתוך המקור ב-C++‎:

כלי הפיתוח הושהה בקריאה ל-`SDL_Init`

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

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

כלי הפיתוח הושהו בתוך הלולאות המוערמות

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

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

חלונית מסוף שמציגה את התוצאה של &#39;palette[10].r&#39;

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

הסבר קצר על המשתנה &#39;x&#39; במקור, שמציג את הערך שלו &#39;3&#39;

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

הסבר קצר ותצוגה &#39;היקף&#39; שמציגה ערכים של &#39;צבע&#39;, &#39;נקודה&#39; ומשתנים אחרים

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

ניפוי באגים גולמי ב-WebAssembly

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

כלי הפיתוח שמוצגת בהם תצוגת פירוק של &#39;mandelbrot.wasm&#39;

חזרנו לחוויה הגולמית של ניפוי הבאגים ב-WebAssembly.

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

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

קודם כול, אם בעבר השתמשתם בניפוי באגים גולמי ב-WebAssembly, יכול להיות שתבחינו שפירוק כולו מוצג עכשיו בקובץ אחד, כך שאין יותר צורך לנחש את הפונקציה מקורות ברשומת wasm-53834e3e/ wasm-53834e3e-7 שמתאימה לה.

סכימה חדשה ליצירת שמות

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

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

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

בדיקת זיכרון

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

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

תפריט ההקשר של &#39;env.memory&#39; בחלונית ההיקף, שבו מוצג הפריט &#39;בדיקת הזיכרון&#39;

אחרי הלחיצה, יופיע Memory Inspector, שבו תוכלו לבדוק את הזיכרון של WebAssembly בתצוגות הקסדצימליות ו-ASCII, לנווט לכתובות ספציפיות ולפרש את הנתונים בפורמטים שונים:

החלונית &#39;בודק הזיכרון&#39; ב&#39;כלי פיתוח&#39; שמציגה תצוגות הקסדצימליות ו-ASCII של הזיכרון

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

יצירת פרופיל של קוד WebAssembly

כשפותחים את כלי הפיתוח, קוד WebAssembly עובר 'ירידה לרמה' לגרסה ללא אופטימיזציה כדי לאפשר ניפוי באגים. הגרסה הזו איטית בהרבה, ולכן אי אפשר להסתמך על console.time, ‏ performance.now ושיטות אחרות למדידת מהירות הקוד בזמן ש-DevTools פתוח, כי המספרים שיוצגו לא ייצגו בכלל את הביצועים בפועל.

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

חלונית ליצירת פרופילים שמוצגות בה פונקציות שונות של Wasm

לחלופין, אפשר להריץ את האפליקציה כש-DevTools סגור, ולפתוח אותו בסיום כדי לבדוק את מסוף.

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

פיתוח ובדיקת באגים במכונות שונות (כולל Docker או מארח)

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

כדי לפתור את הבעיה הזו, הטמענו פונקציונליות של מיפוי נתיב באפשרויות של התוסף ל-C/C++. בעזרתו אפשר למפות מחדש נתיבים שרירותיים ולעזור לכלי הפיתוח לאתר מקורות.

לדוגמה, אם הפרויקט במכונה המארחת נמצא מתחת לנתיב C:\src\my_project, אבל נבנה בתוך קונטיינר Docker שבו הנתיב הזה מיוצג בתור /mnt/c/src/my_project, תוכלו למפות אותו מחדש במהלך ניפוי הבאגים על ידי ציון הנתיבים האלה כקידומות:

דף האפשרויות של תוסף ניפוי הבאגים של C/C++

הקידומת הראשונה שתואמת "מנצחת". אם אתם מכירים כלי לניפוי באגים אחרים ב-C++, האפשרות הזו דומה לפקודה set substitute-path ב-GDB או להגדרה target.source-map ב-LLDB.

ניפוי באגים בגרסאות build שעברו אופטימיזציה

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

אם אתם לא מתכוננים מחוויית ניפוי באגים מוגבלת יותר ואתם עדיין רוצים לנפות באגים ב-build שעבר אופטימיזציה, רוב האופטימיזציות יפעלו כמצופה, חוץ מהוספה של פונקציות. אנחנו מתכננים לטפל בבעיות הנותרות בעתיד, אבל בינתיים, תוכלו להשתמש ב--fno-inline כדי להשבית את התכונה כשאתם מבצעים הידור עם אופטימיזציות ברמת -O, למשל:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

הפרדת המידע על תוצאות ניפוי הבאגים

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

כדי לזרז את הטעינה וההדרכה של מודול WebAssembly, מומלץ לפצל את פרטי ניפוי הבאגים האלה לקובץ WebAssembly נפרד. כדי לעשות זאת ב-Emscripten, מעבירים את הדגל -gseparate-dwarf=… עם שם הקובץ הרצוי:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

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

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

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

כדי להמשיך...

וואו, היו המון תכונות חדשות!

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

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

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

בינתיים, נשמח לעזרה שלך. אפשר לנסות את גרסת הבטא הנוכחית בקוד שלך ולדווח על בעיות שנמצאות בכתובת https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

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

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

פנייה לצוות של כלי הפיתוח ל-Chrome

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