更快地调试 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 的 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(类似)二进制文件。在构建过程中运行 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 秒,比之前快了 0.5 秒!

高级调整

跳过输入文件扫描

通常,在关联 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 的类型是什么?”。

通过调试 fission,我们可以将二进制文件的调试信息拆分为两部分:一部分保留在二进制文件中,另一部分包含在单独的 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,我们只有一个源文件,但项目通常大于这个大小,并且包含多个文件。调试 fission 将为每个来源生成一个 .dwo 文件。为了能够加载此分屏调试信息的当前 Beta 版调试程序 (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 视频发表评论。