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

Ingvar Stepanyan
Ingvar Stepanyan

הכביש עד עכשיו

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

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

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

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

לפני שנתחיל, חשוב לזכור שזו עדיין גרסת בטא של הממשק החדש. השימוש בגרסה האחרונה של כל הכלים הוא באחריותכם בלבד. אם תיתקלו בבעיות, תוכלו לדווח עליהן בכתובת https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

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

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

כדאי גם להפעיל ניפוי באגים ב-WebAssembly גם בניסויים של כלי הפיתוח. פותחים את כלי הפיתוח ל-Chrome, לוחצים על סמל גלגל השיניים () בפינה השמאלית העליונה של חלונית כלי הפיתוח, עוברים לחלונית ניסויים ומסמנים את התיבה ניפוי באגים ב-WebAssembly: הפעלת תמיכה ב-DWARF.

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

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

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

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

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

כלי הפיתוח הושהו בפונקציה &#39;assert_less&#39; והם מציגים ערכים של &#39;x&#39; ו-&#39;y&#39; בתצוגת ההיקף

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ניפוי באגים מסוג Raw WebAssembly

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

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

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

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

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

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

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

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

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

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

בדיקת זיכרון

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

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

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

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

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

תרחישים מתקדמים ואזהרות

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

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

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

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

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

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

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

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

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

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

הדף &#39;אפשרויות&#39; בתוסף ניפוי הבאגים C/C++

הקידומת הראשונה התואמת 'wins' (מנצחים). אם כבר למדתם על כלים אחרים לניפוי באגים ב-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://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

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

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

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

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

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