ניפוי באגים ב-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, לוחצים על סמל גלגל השיניים () בפינה השמאלית העליונה של החלונית של כלי הפיתוח, עוברים לחלונית ניסויים ומסמנים את התיבה 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++‎:

כלי הפיתוח הושהו בקריאה &#39;SDL_Init&#39;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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