ניפוי באגים ב-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.

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

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

עכשיו אפשר לחזור לחלונית 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

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

דף הדגמה

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

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

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

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

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

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

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

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

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

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

תיאור מידע מעל המשתנה x במקור, שבו מוצג הערך שלו, 3

מכאן אפשר להריץ הוראות C++‏ step-in או step-over ולראות איך משתנים אחרים משתנים גם כן:

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

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

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

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

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

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

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

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

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

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

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

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

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

בדיקת זיכרון

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

אם אתם לא מתנגדים לחוויית ניפוי באגים מוגבלת יותר, ועדיין רוצים לנפות באגים בגרסה מבוססת-build שעברה אופטימיזציה, רוב פעולות האופטימיזציה יפעלו כצפוי, מלבד הטמעת פונקציות (inlining). אנחנו מתכננים לטפל בבעיות הנותרות בעתיד, אבל בינתיים, תוכלו להשתמש ב--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, ותוספי העזרה יוכלו לאתר אותו ולטעון אותו כשתפתחו את DevTools.

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