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

這個指令會產生 3MB 的二進位檔案。大致上,這大多是偵錯資訊。您可以使用 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。如果我們一併 time emcc 指令,會發現機器上的執行時間約為 1.5 秒。這些數據能奠定一個不錯的基準線,但規模較小,可能全無人會關注。不過在實際應用程式中,偵錯二進位檔可以輕鬆達到 GB 的大小,而且需要幾分鐘才能完成建構!

略過二進位檔

使用 Emscripten 建構 wasm 應用程式時,最終建構步驟之一就是執行 Binaryen 最佳化工具。Binaryen 是編譯器工具包,可用來最佳化 WebAssembly (類似) 二進位檔,並將其合法化。在版本中執行 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 標記是為了有效測量結果。有助於偵測二進位檔案何時執行,並在非預期的情況下重新編寫二進位檔。如此一來,我們就能確保一切都在快速的進行。

即使我們的範例太小,我們仍然可以看到略過二元期的影響!根據 time 的資料,這個指令只執行不到 1 秒,所以比先前快半了一半!

進階調整

略過輸入檔案掃描程序

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

可以改用 -sREVERSE_DEPS=all 執行,讓 emcc 加入所有可能的 JavaScript 函式原生依附元件。這會佔用少量程式碼,但可能會加快連結時間,對於偵錯版本相當實用。

對小專案而言,這不會造成太大的影響,但如果您的專案中有數百甚至數千個物件檔案,可以有效縮短連結時間。

移除「名稱」部分

在大型專案中,特別是使用大量 C++ 範本的應用程式,WebAssembly 的「name」區段可能會非常龐大。在我們的範例中,這只是整體檔案大小的一小部分 (請參閱上面的 llvm-objdump 輸出內容),但在某些情況下,可能非常顯著。如果應用程式的「name」部分非常龐大,且 dwarf 偵錯資訊足以滿足您的偵錯需求,那麼移除「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) 載入此分割偵錯資訊,我們必須將所有內容組合成所謂的 DWARF 套件 (.dwp),如下所示:

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

將 dwo 檔案加入 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 Canary 版開發人員版Beta 版做為預設開發瀏覽器。這些預覽管道可讓您存取開發人員工具的最新功能、測試最先進的網路平台 API,並在使用者使用之前就在網站上發現問題!

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

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

  • 請透過 crbug.com 提交建議或意見回饋。
  • 如要回報開發人員工具的問題,請在開發人員工具中依序點選「更多選項」圖示 更多   >「說明」 >「回報開發人員工具的問題」
  • @ChromeDevTools 張貼推文。
  • 歡迎對開發人員工具的 YouTube 影片或開發人員工具秘訣 (YouTube 影片) 提供意見。