在 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
從個別物件建立 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、Dev 或 Beta 版做為預設的開發瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!
與 Chrome 開發人員工具團隊聯絡
請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。
- 請前往 crbug.com 提交意見回饋和功能要求。
- 在開發人員工具中,依序按一下「更多選項」>「說明」>「回報開發人員工具的問題」,然後使用 回報開發人員工具的問題。
- 前往 @ChromeDevTools 張貼 Tweet。
- 在 YouTube 影片「What's new in DevTools」或「DevTools 提示」YouTube 影片中留言。