在 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
從個別物件中建構 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
檔案含有其餘的偵錯資訊:
您也可以查看此檔案中的其他資料表,例如顯示了將 wasm 位元碼對應至 C++ 行的行表格 (請嘗試使用 llvm-dwarfdump -debug-line
)。
我們也可以查看獨立 .dwo
檔案中的偵錯資訊:
llvm-dwarfdump mandelbrot.dwo
重點摘要:使用偵錯連接有什麼好處?
如果要使用大型應用程式,分割偵錯資訊有以下優點:
快速連結:不再需要剖析整個偵錯資訊。連接器通常需要剖析二進位檔中的整個 DWARF 資料。透過將大部分偵錯資訊分解成不同的檔案,連結器會處理較小的二進位檔,進而加快連結時間 (對於大型應用程式而言更是如此)。
快速偵錯:偵錯工具可以略過剖析
.dwo
/.dwp
檔案中其他符號的程序,以便執行某些符號查詢。針對部分查詢 (例如 wasm-to-C++ 檔案行對應要求),我們不需要查看額外的偵錯資料。這可以節省我們的時間,不需要載入及剖析額外的偵錯資料。
1:如果您的系統沒有最新版本的 llvm-objdump
,而且您使用的是 emsdk
,可以在 emsdk/upstream/bin
目錄中找到該版本。
下載預覽管道
考慮使用 Chrome Canary 版、開發人員版或 Beta 版做為預設開發瀏覽器。這些預覽管道可讓您存取開發人員工具的最新功能、測試最先進的網路平台 API,並在使用者使用之前就在網站上發現問題!
與 Chrome 開發人員工具團隊聯絡
使用下列選項,討論文章的新功能和異動,以及其他與開發人員工具相關的事項。
- 請透過 crbug.com 提交建議或意見回饋。
- 如要回報開發人員工具的問題,請在開發人員工具中依序點選「更多選項」圖示 >「說明」 >「回報開發人員工具的問題」。
- 在 @ChromeDevTools 張貼推文。
- 歡迎對開發人員工具的 YouTube 影片或開發人員工具秘訣 (YouTube 影片) 提供意見。