更快地调试 WebAssembly

Philip Pfaffe
Kim-Anh Tran
Kim-Anh Tran
Eric Leese
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(类似)二进制文件。在 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 来启用此功能:

$ 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 到 C++ 文件的行映射的请求),我们无需查看其他调试数据。这为我们节省了时间,无需加载和解析额外的调试数据。

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

下载预览渠道

不妨考虑将 Chrome Canary 版开发者版Beta 版用作默认开发浏览器。通过这些预览版渠道,您可以使用最新的 DevTools 功能、测试尖端的 Web 平台 API,并帮助您在用户发现问题之前发现网站上的问题!

与 Chrome 开发者工具团队联系

您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。