加快 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

這個指令會產生 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 (類似) 二進位檔。在建構作業中執行 Binaryen 的成本相當高,但只有在特定情況下才需要執行。對於偵錯版本,如果能避免需要使用 Binaryen 的傳遞,就能大幅縮短建構時間。最常見的必要 Binaryen 傳遞作業,是將涉及 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 執行時是否意外重寫二進位檔。這樣一來,我們就能確保持續在快速路徑上。

雖然我們的範例相當小,但我們還是可以看到略過 Binaryen 的效果!根據 time 的資料,這個指令的執行時間不到 1 秒,比先前快了半秒!

進階調整

略過輸入檔案掃描

通常在連結 Emscripten 專案時,emcc 會掃描所有輸入物件檔案和程式庫。這麼做是為了在程式中,實作 JavaScript 程式庫函式和原生符號之間的確切依附元件。對於較大型的專案,這項額外的輸入檔案掃描作業 (使用 llvm-nm) 可能會大幅增加連結時間。

您可以改為使用 -sREVERSE_DEPS=all 執行,這樣 emcc 就會包含 JavaScript 函式的所有可能原生依附元件。這種做法雖然會產生少許的程式碼大小負載,但可以加快連結時間,並可用於偵錯版本。

對於像本例中那樣規模較小的專案,這項做法並不會帶來太大差異,但如果專案中有數百或數千個物件檔案,這項做法就能大幅縮短連結時間。

移除「name」部分

在大型專案中,尤其是使用大量 C++ 範本的專案,WebAssembly 的「name」部分可能會非常龐大。在本範例中,這只是整體檔案大小的一小部分 (請參閱上方 llvm-objdump 的輸出內容),但在某些情況下,這可能會占據相當大的空間。如果應用程式的「name」部分非常大,而矮人偵錯資訊足以滿足偵錯需求,那麼移除「name」部分可能會帶來好處:

$ 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。啟用 DWARF 符號第 5 版 (目前並非預設值) 是另一個可協助我們更快開始偵錯的訣竅。有了這個選項,系統就能在預設的 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 和偵錯資訊

重點摘要:使用偵錯分割的優點為何?

如果要處理大型應用程式,分割偵錯資訊有幾項優點:

  1. 更快速的連結:連結器不再需要剖析整個偵錯資訊。連接器通常需要剖析二進位檔中的全部 DWARF 資料。透過將大部分的偵錯資訊去除並放入個別檔案,連結器就能處理較小的二進位檔,進而縮短連結時間 (尤其是大型應用程式)。

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

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

下載預覽管道

建議您使用 Chrome CanaryDevBeta 版做為預設的開發瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!

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

請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。