更快地调试 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。如果我们同时对 emcc 命令执行 time 操作,则会在我们的机器上看到运行大约需要 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 非常有用。

对于像我们示例这样小的项目,这并没有什么实际区别,但如果您的项目中包含数百甚至数千个目标文件,则可以显著缩短链接时间。

去除“名称”部分

在大型项目中,尤其是那些频繁使用 C++ 模板的项目,WebAssembly 的“name”部分可能会很大。在我们的示例中,它只占整体文件大小的一小部分(参见上面的 llvm-objdump 输出),但在某些情况下可能会非常重要。如果应用的“name”部分非常大,并且 dwarf 调试信息足以满足您的调试需求,则去除“name”部分会很有帮助:

$ emstrip --no-strip-all --remove-section=name mandelbrot.wasm

这将去除 WebAssembly 的“name”部分,同时保留 DWARF 调试部分。

调试故障

包含大量调试数据的二进制文件不仅会给构建时间带来压力,还会给调试时间带来压力。调试程序需要加载数据并为数据构建索引,以便快速响应诸如“What's the type of the local variable x?”之类的查询。

通过 Debug fission,我们可以将二进制文件的调试信息拆分为两部分:一部分保留在二进制文件中,另一部分包含在单独的所谓的 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,我们只有一个源文件,但项目通常会比这个文件大,并且包含多个文件。调试 fission 会为其中每个元素生成一个 .dwo 文件。为了使调试程序的当前 Beta 版 (0.1.6.1615) 能够加载此拆分调试信息,我们需要将所有这些信息捆绑到一个所谓的 DWARF 软件包 (.dwp) 中,如下所示:

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

将 dwo 文件捆绑到 DWARF 软件包中

基于单个对象构建 DWARF 软件包的优势在于,您只需提供一个额外文件!目前,我们正努力在未来版本中加载所有单个对象。

DWARF 5 是什么?

您可能已经注意到,我们在上面的 emcc 命令中嵌入了另一个标志 -gdwarf-5。启用版本 5 的 DWARF 符号(目前还不是默认版本)是帮助我们更快地开始调试的另一个技巧。有了它,某些信息会存储在默认版本 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 到 C++ 文件的行映射请求),我们不需要查看其他调试数据。这节省了我们的时间,而无需加载并解析其他调试数据。

1:如果您的系统中没有最新版本的 llvm-objdump,而您使用的是 emsdk,则可以在 emsdk/upstream/bin 目录中找到它。

下载预览渠道

您可以考虑将 Chrome Canary 版Dev 版Beta 版用作默认开发浏览器。通过这些预览渠道,您可以使用最新的开发者工具功能,测试先进的网络平台 API,并在用户采取行动之前发现网站上的问题!

与 Chrome 开发者工具团队联系

使用以下选项讨论博文中的新功能和变化,或讨论与开发者工具有关的任何其他内容。

  • 通过 crbug.com 提交建议或反馈。
  • 使用开发者工具中的更多选项   了解详情   > Help > Report a DevTools issues来报告开发者工具问题。
  • 发推文:@ChromeDevTools
  • 请在 YouTube 视频或“开发者工具提示”YouTube 视频中留言说明“开发者工具的新变化”。