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

Philip Pfaffe
Kim-Anh Tran
Kim-Anh Tran
Eric Leese
Eric Leese
Sam Clegg

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

ניפוי באגים גמיש

בואו להמשיך מהנקודה שבה הפסקנו את הפוסט שלנו לשנת 2020. זו הדוגמה שבדקנו אז:

#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();
}

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

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

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH

הפקודה הזו יוצרת קובץ בינארי של 3MB Wam. רוב המידע הזה, כצפוי, הוא מידע על ניפוי באגים. אפשר לאמת זאת באמצעות הכלי llvm-objdump [1], לדוגמה:

$ llvm-objdump -h mandelbrot.wasm

mandelbrot.wasm:        file format wasm

Sections:
Idx Name          Size     VMA      Type
  0 TYPE          0000026f 00000000
  1 IMPORT        00001f03 00000000
  2 FUNCTION      0000043e 00000000
  3 TABLE         00000007 00000000
  4 MEMORY        00000007 00000000
  5 GLOBAL        00000021 00000000
  6 EXPORT        0000014a 00000000
  7 ELEM          00000457 00000000
  8 CODE          0009308a 00000000 TEXT
  9 DATA          0000e4cc 00000000 DATA
 10 name          00007e58 00000000
 11 .debug_info   000bb1c9 00000000
 12 .debug_loc    0009b407 00000000
 13 .debug_ranges 0000ad90 00000000
 14 .debug_abbrev 000136e8 00000000
 15 .debug_line   000bb3ab 00000000
 16 .debug_str    000209bd 00000000

בפלט הזה מוצגים כל הקטעים בקובץ Wam שנוצר. רובם הם קטעי WebAssembly רגילים, אבל יש גם כמה מקטעים בהתאמה אישית ששמם מתחיל ב-.debug_. זה המקום שבו הקובץ הבינארי מכיל את המידע על תוצאות ניפוי הבאגים! אם נוסיף את כל הגדלים, נראה שפרטי ניפוי הבאגים מהווים כ-2.3MB מתוך הקובץ בנפח 3MB. אם מריצים גם את הפקודה time emcc, רואים שבמכונה שלנו חלפו כ-1.5 שניות עד שהיא הושלמה. המספרים האלה יוצרים בסיס מצוין, אבל הם קטנים כל כך שכנראה אף אחד לא יתרגש מהם. עם זאת, באפליקציות אמיתיות, קובץ הבינארי של ניפוי הבאגים יכול להגיע בקלות לגודל של כמה ג'יגה-בייטים, ויצירתו עשויה להימשך כמה דקות.

דילוג על Binaryen

כשמפתחים אפליקציה ב-Wasm באמצעות Emscripten, אחד משלבי ה-build הסופיים הוא הפעלת כלי האופטימיזציה של Binaryen. Binaryen היא ערכת כלי מהדר שמאפשרת אופטימיזציה והפיכת קבצים בינאריים (כמו בינארי) ל-WebAssembly וחוקיים. הפעלת Binaryen כחלק מה-build היא יקרה למדי, אבל היא נדרשת רק בתנאים מסוימים. לגבי גרסאות build לצורך ניפוי באגים, אפשר לזרז את זמן ה-build באופן משמעותי אם נימנע מהצורך בפעולות של Binaryen. הבדיקה הנפוצה ביותר שצריך לבצע ב-Binaryen היא כדי להכשיר חתימות של פונקציות שכוללות ערכים שלמים של 64 ביט. כדי להימנע מכך, אפשר לבחור בשילוב של WebAssembly BigInt באמצעות -sWASM_BIGINT.

$ emcc -sUSE_SDL=2 -g -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

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

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

שיפורים מתקדמים

דילוג על סריקת קובצי קלט

בדרך כלל, כשמקשרים פרויקט Emscripten, emcc יסרוק את כל הקבצים והספריות של אובייקטי הקלט. הוא עושה זאת כדי להטמיע יחסי תלות מדויקים בין פונקציות של ספריות JavaScript לבין סמלים מקומיים בתוכנית. בפרויקטים גדולים יותר, הסריקה הנוספת של קובצי הקלט (באמצעות llvm-nm) יכולה להאריך משמעותית את זמן הקישור.

אפשר להריץ את הקוד עם -sREVERSE_DEPS=all במקום זאת, כדי להורות ל-emcc לכלול את כל יחסי התלות האפשריים של פונקציות JavaScript. השיטה הזו גורמת לעומס קטן על קוד, אבל היא יכולה לזרז את זמני הקישורים, והיא יכולה להיות שימושית לגרסאות build של ניפוי באגים.

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

הסרת הקטע 'שם'

בפרויקטים גדולים, במיוחד בפרויקטים שבהם נעשה שימוש רב בתבניות C++‎, הקטע 'name' ב-WebAssembly יכול להיות גדול מאוד. בדוגמה שלנו, הוא רק חלק זעיר מגודל הקובץ הכולל (ראו את הפלט של llvm-objdump למעלה), אבל במקרים מסוימים הוא יכול להיות משמעותי מאוד. אם הקטע 'שם' של האפליקציה גדול מאוד, והמידע על תוצאות ניפוי הבאגים של הגמד מספיק לצרכים שלכם לניפוי באגים, כדאי להסיר את הקטע 'שם':

$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm

הפעולה הזו תסיר את הקטע 'name' ב-WebAssembly ותשמור על קטעי ניפוי הבאגים של DWARF.

ניפוי באגים ב-Fission

קובצי בינארי עם הרבה נתוני ניפוי באגים לא רק מגדילים את זמן ה-build, אלא גם את זמן ניפוי הבאגים. הכלי לניפוי באגים צריך לטעון את הנתונים וליצור בשבילו אינדקס, כדי שיוכל להגיב במהירות לשאילתות, כמו "What's the type של המשתנה המקומי x? ".

בקיעה של ניפוי באגים מאפשרת לנו לפצל את המידע על תוצאות ניפוי הבאגים בקובץ בינארי לשני חלקים: אחד שנשאר בקובץ הבינארי והשני שנמצא בקובץ אובייקט DWARF נפרד (.dwo). אפשר להפעיל אותו על ידי העברת הדגל -gsplit-dwarf ל-Emscripten:

$ emcc -sUSE_SDL=2 -g -gsplit-dwarf -gdwarf-5 -O0 -o mandelbrot.html mandelbrot.cc  -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

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

את הפקודות השונות ואיזה קבצים נוצרים

כשמפצלים את נתוני DWARF, חלק מנתוני ניפוי הבאגים נמצאים יחד עם הנתונים הבינאריים, ואילו חלקם מועבר לקובץ mandelbrot.dwo (כפי שמתואר למעלה).

ב-mandelbrot יש לנו רק קובץ מקור אחד, אבל בדרך כלל פרויקטים גדולים יותר וכוללים יותר מקובץ אחד. תהליך הפיצול לצורך ניפוי באגים יוצר קובץ .dwo לכל אחת מהן. כדי שגרסת הבטא הנוכחית של מנתח הבאגים (0.1.6.1615) תוכל לטעון את המידע המפוצל על תוצאות ניפוי הבאגים, אנחנו צריכים לארוז את כל המידע הזה בחבילה שנקראת DWARF (.dwp) כך:

$ emdwp -e mandelbrot.wasm -o mandelbrot.dwp

קיבוץ קובצי dwo לחבילת DWARF

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

מה קורה עם DWARF 5?

אולי שמתם לב שגנבנו דגל נוסף לפקודה emcc שלמעלה, -gdwarf-5. הפעלה של גרסה 5 של סמלי DWARF, שכבר לא מוגדרת כברירת המחדל, היא טריק נוסף שיעזור לנו להתחיל לנפות באגים מהר יותר. בעזרתו, מידע מסוים מאוחסן בקובץ הבינארי הראשי שלא נכלל בגרסה 4 שמוגדרת כברירת מחדל. באופן ספציפי, אנחנו יכולים לקבוע את הקבוצה המלאה של קובצי המקור רק מהקובץ הבינארי הראשי. כך אפשר לבצע פעולות בסיסיות כמו הצגת עץ המקור המלא והגדרת נקודות עצירה ללא טעינה וניתוח של נתוני הסמל המלאים. כך ניקוי הבאגים באמצעות סמלים מפוצלים מהיר הרבה יותר, ולכן אנחנו תמיד משתמשים בדגלים -gsplit-dwarf ו--gdwarf-5 בשורת הפקודה ביחד.

פורמט ניפוי הבאגים DWARF5 מאפשר גם גישה לתכונה שימושית נוספת. הוא מוסיף אינדקס שמות לנתוני ניפוי הבאגים שייווצרו כשמעבירים את הדגל -gpubnames:

$ emcc -sUSE_SDL=2 -g -gdwarf-5 -gsplit-dwarf -gpubnames -O0 -o mandelbrot.html mandelbrot.cc -sALLOW_MEMORY_GROWTH -sWASM_BIGINT -sERROR_ON_WASM_CHANGES_AFTER_LINK

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

לחובבי הנושא: הצגת נתוני ניפוי הבאגים

אפשר להשתמש ב-llvm-dwarfdump כדי להציץ בנתוני DWARF. ננסה את זה:

llvm-dwarfdump mandelbrot.wasm

כך אנחנו מקבלים סקירה כללית על 'יחידות ה-Compile' (כלומר, קובצי המקור) שיש לנו מידע על תוצאות ניפוי הבאגים שלהם. בדוגמה הזו, יש לנו רק את פרטי ניפוי הבאגים של mandelbrot.cc. המידע הכללי יעזור לנו לדעת שיש לנו יחידה שלד, כלומר שיש לנו נתונים חלקיים בקובץ הזה, ויש קובץ .dwo נפרד שמכיל את שאר פרטי ניפוי הבאגים:

mandelbrot.wasm ומידע על תוצאות ניפוי הבאגים

אפשר לבדוק גם טבלאות אחרות בקובץ הזה, למשל בטבלת השורות שמציגה את המיפוי של בייטים ב-Wasm לשורות C++ (אפשר לנסות להשתמש ב-llvm-dwarfdump -debug-line).

אפשר גם לעיין במידע על תוצאות ניפוי הבאגים שכלול בקובץ .dwo הנפרד:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm ומידע על תוצאות ניפוי הבאגים

אמ;לק: מהו היתרון של שימוש ב-debug fission?

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

  1. קישור מהיר יותר: ה-linker כבר לא צריך לנתח את כל פרטי ניפוי הבאגים. בדרך כלל, מקשרים צריכים לנתח את כל נתוני DWARF שבקובץ הבינארי. על ידי הסרת חלקים גדולים מפרטי ניפוי הבאגים בקבצים נפרדים, ה-linkers מטפלים בקבצים בינאריים קטנים יותר. התוצאה היא זמני קישור מהירים יותר (במיוחד באפליקציות גדולות).

  2. ניפוי באגים מהיר יותר: מנתח הבאגים יכול לדלג על ניתוח הסמלים הנוספים בקבצים .dwo/.dwp עבור חלק מחיפושי הסמלים. בחיפושים מסוימים (כמו בקשות לגבי מיפוי שורות של קבצים מסוג wasm ל-C++), אנחנו לא צריכים לבדוק את נתוני ניפוי הבאגים הנוספים. כך אנחנו חוסכים זמן, כי אין צורך לטעון ולנתח את נתוני ניפוי הבאגים הנוספים.

1: אם אין לכם בגרסה עדכנית של llvm-objdump במערכת ואתם משתמשים ב-emsdk, תוכלו למצוא אותו בספרייה emsdk/upstream/bin.

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

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

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

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