加快 WebAssembly 偵錯速度

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

2020 年 Chrome 開發人員高峰會中,我們首次示範了 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

這個指令會產生 3 MB 的 wasm 二進位檔。大部分的問題,正如您所想的一樣,就是偵錯資訊。您可以使用 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

這個輸出內容會顯示產生的 wasm 檔案中的所有部分,其中大部分是標準 WebAssembly 區段,但還有數個名稱開頭為 .debug_ 的自訂區段。此時二進位檔就含有我們的偵錯資訊!如果將所有大小都加總,可發現偵錯資訊會佔據約 3 MB 的 2.3 MB。如果我們也對 emcc 指令執行 time 作業,就會發現機器上要執行大約 1.5 秒,這些數字是一個很好的基準,但這些數字太小,可能沒有人受到關注。在實際應用程式中,偵錯二進位檔可以輕鬆達到 GB 的大小,而且建構只需幾分鐘!

略過二進位

使用 Emscripten 建構 wasm 應用程式時,其中一個最終建構步驟是執行 Binaryen 最佳化工具。Binaryen 是一種編譯器工具包,可針對 WebAssembly (類似) 二進位檔進行最佳化及合法化。在建構過程中執行二進位檔案的費用高昂,但只有在特定情況下才需要使用。針對偵錯版本,如果不需要二進位檔案傳遞,我們可以大幅縮短建構時間。最常見的二進位票證,是將涉及 64 位元整數值的函式簽章合法化。只要使用 -sWASM_BIGINT 加入 WebAssembly 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 是否正在執行,並無意間重寫二進位檔。如此一來,我們就能確保一切都在快的路上。

即使我們的範例相當小,我們仍能看到略過二元法造成的影響!根據 time 的資料,這個指令會在 1 秒以內執行,因此比以前快一半!

進階調整

略過輸入檔案掃描

連結 Emscripten 專案時,emcc 會掃描所有輸入物件檔案和程式庫。它的目的是在程式中,實作 JavaScript 程式庫函式和原生符號之間的精確依附性。如果專案規模較大,這項額外掃描輸入檔案 (使用 llvm-nm) 可能會大幅增加連結時間。

您可以改為使用 -sREVERSE_DEPS=all 執行,藉此指示 emcc 加入 JavaScript 函式的所有可能原生依附元件。這會負擔較小的程式碼負擔,但可以加快連結時間,並對偵錯版本很有用。

以我們的範例較小的專案來說,這並沒有任何影響,但如果專案中有數百或數千個物件檔案,連結時間能確實縮短。

去除「名稱」部分

在大型專案中,特別是使用大量 C++ 範本的專案,WebAssembly 的「name」部分可能非常龐大。在範例中,這只佔整體檔案大小的一小部分 (請參閱上方 llvm-objdump 的輸出內容),但在某些情況下,可能非常顯著。如果應用程式的 "name" 部分非常龐大,而 Dwarf 偵錯資訊足以滿足您的偵錯需求,則建議您去除「名稱」區段:

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

這麼做會移除 WebAssembly 的「name」區段,同時保留 DWARF 偵錯區段。

偵測威脅

含有大量偵錯資料的二進位檔不只會造成建構時間壓力,也會造成偵錯時間。偵錯工具必須載入資料,且必須為其建立索引,才能快速回應查詢,例如「本機變數 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) 目前 Beta 版載入此分割偵錯資訊,我們必須將這些資訊全部組合到一個所謂的 DWARF 套件 (.dwp),如下所示:

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

將 2 個檔案封裝至 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

如此一來,我們就能概略瞭解含有偵錯資訊的「編譯單位」(概略來說是來源檔案)。在這個範例中,我們只有 mandelbrot.cc 的偵錯資訊。一般來說,我們會透過架構單元告訴我們這個架構,也就是這個檔案中的資料不完整,且有一個獨立的 .dwo 檔案含有其餘的偵錯資訊:

mandelbrot.wasm 和偵錯資訊

您也可以查看這個檔案中的其他資料表,例如這個行表顯示 wasm 位元碼與 C++ 行的對應關係 (嘗試使用 llvm-dwarfdump -debug-line)。

還可以查看單獨 .dwo 檔案中的偵錯資訊:

llvm-dwarfdump mandelbrot.dwo

mandelbrot.wasm 和偵錯資訊

TL;DR:使用偵錯防禦功能有什麼好處?

在處理大型應用程式的情況下,分割偵錯資訊有以下優點:

  1. 更快的連結:連結器不再需要剖析整個偵錯資訊。連結器通常需要剖析二進位檔中的整個 DWARF 資料。只要將大部分的偵錯資訊拆成個別檔案,連結器就能處理較小的二進位檔,進而加快連結速度 (對大型應用程式而言更是如此)。

  2. 加快偵錯速度:偵錯工具可以略過部分符號查詢作業時,剖析 .dwo/.dwp 檔案中的其他符號。處理部分查詢時 (例如針對 wasm-to-C++ 檔案行對應的要求),我們就不需要查看額外的偵錯資料。這樣我們就不必載入 和剖析額外的偵錯資料,可節省時間。

1:如果您的系統中沒有最新版 llvm-objdump,而且您使用的是 emsdk,可到 emsdk/upstream/bin 目錄中尋找。

下載預覽頻道

建議您使用 Chrome CanaryDevBeta 版做為預設的開發瀏覽器。透過這些預覽版本,您可以存取開發人員工具中的最新功能、測試最先進的網路平台 API,以及找出網站的問題,以免使用者發現問題。

與 Chrome 開發人員工具團隊聯絡

請使用下列選項,討論貼文中的新功能和異動,或與開發人員工具相關的其他事項。

  • 歡迎透過 crbug.com 提出建議或意見。
  • 使用「更多選項」更多 > 回報開發人員工具問題說明 >在開發人員工具中回報開發人員工具問題
  • 前往 @ChromeDevTools 張貼 Tweet。
  • 歡迎在「開發人員工具」推出「最新消息」YouTube 影片或「開發人員工具秘訣」YouTube 影片留言。