在 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 的 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_
开头的自定义部分。二进制文件中包含我们的调试信息!如果我们将所有大小相加,就会发现调试信息占据了 3MB 文件的大约 2.3MB。如果我们同时time
执行 emcc
命令,则会发现在计算机上大约需要 1.5 秒才能完成运行。这些数字是一个不错的基准,但它们太小了,可能没人会注意到。不过,在实际应用中,调试二进制文件的大小很容易达到数 GB,并且需要几分钟才能构建!
跳过 Binaryen
使用 Emscripten 构建 wasm 应用时,其最终构建步骤之一是运行 Binaryen 优化器。Binaryen 是一个编译器工具包,可优化和合法化 WebAssembly(类似)二进制文件。在 build 过程中运行 Binaryen 的开销相当高,但只有在特定情况下才需要这样做。对于调试 build,如果不需要 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 函数的所有可能的原生依赖项。这的代码开销较小,但可以缩短链接时间,并且对调试 build 非常有用。
对于像我们示例这样小的项目没有实际意义,但如果您的项目中有数百甚至数千个对象文件,则可以有效缩短链接时间。
移除“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 来启用该 API:
$ 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
下面,我们展示了不同的命令以及通过在不使用调试数据进行编译、使用调试数据以及最后使用调试数据和调试 fission 进行编译时生成的文件。
拆分 DWARF 数据时,部分调试数据会与二进制文件一起存储,而大部分数据会放入 mandelbrot.dwo
文件(如上所示)。
对于 mandelbrot
,我们只有一个源文件,但项目通常大于这个大小,并且包含多个文件。调试分裂会为每个模块生成一个 .dwo
文件。为了让当前的调试程序 Beta 版 (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 到 C++ 文件的行映射的请求),我们无需查看其他调试数据。这样一来,我们就不必加载和解析额外的调试数据,从而节省了时间。
1:如果您的系统上没有较新版本的 llvm-objdump
,并且您使用的是 emsdk
,则可以在 emsdk/upstream/bin
目录中找到它。
下载预览渠道
请考虑将 Chrome Canary、开发者版或 Beta 版用作您的默认开发浏览器。通过这些预览版渠道,您可以使用最新的 DevTools 功能、测试尖端的 Web 平台 API,并帮助您在用户发现问题之前发现网站上的问题!
与 Chrome 开发者工具团队联系
您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。
- 请访问 crbug.com 向我们提交反馈和功能请求。
- 在开发者工具中使用 更多选项 > 帮助 > 报告开发者工具问题来报告开发者工具问题。
- 向 @ChromeDevTools 发送推文。
- 在 “开发者工具的新变化”YouTube 视频或 “开发者工具提示”YouTube 视频中留言。