最近在钻研 WebAssembly,也写了几篇全面介绍的文章:
- 高级前端进阶:我是如何把 C/C++ 代码跑在浏览器上的?
- 快 11K Star 的 WebAssembly,你应该这样学
本文是学习 WebAssembly 系列的第三篇文章,也是想探索一下 Chrome 开发者工具对 WebAssembly 的调试反对度如何,通过这个探索的过程,咱们会理解到 Chrome 调试工具各种方面的应用办法以及作用,挖掘你可能不晓得的一些知识点。
所以本文既能够当做学习应用 Chrome Devtools 调试工具的一篇比拟全面的文章,也能够当做是介绍现阶段咱们如何在浏览器中对 WebAssembly 相干的代码进行调试,帮忙你成为一个合格的调试工程师:)。
WebAssembly 的原始调试形式
Chrome 开发者工具目前曾经反对 WebAssembly 的调试,尽管存在一些限度,然而针对 WebAssembly 的文本格式的文件能进行单个指令的剖析以及查看原始的堆栈追踪,具体见如下图:
上述的办法对于一些无其余依赖函数的 WebAssembly 模块来说能够很好的运行,因为这些模块只波及到很小的调试范畴。然而对于简单的利用来说,如 C/C++ 编写的简单利用,一个模块依赖其余很多模块,且源代码与编译后的 WebAssembly 的文本格式的映射有较大的区别时,上述的调试形式就不太直观了,只能靠猜的形式能力了解其中的代码运行形式,且大多数人很难以看懂简单的汇编代码。
更加直观的调试形式
古代的 JavaScript 我的项目在开发时通常也会存在编译的过程,应用 ES6 进行开发,编译到 ES5 及以下的版本进行运行,这个时候如果须要调试代码,就波及到 Source Map 的概念,source map 用于映射编译后的对应代码在源代码中的地位,source map 使得客户端的代码更具可读性、更不便调试,然而又不会对性能造成很大的影响。
而 C/C++ 到 WebAssembly 代码的编译器 Emscripten 则反对在编译时,为代码注入相干的调试信息,生成对应的 source map,而后装置 Chrome 团队编写的 C/C++ Devtools Support 浏览器扩大,就能够应用 Chrome 开发者工具调试 C/C++ 代码了。
这里的原理其实就是,Emscripten 在编译时,会生成一种 DWARF 格局的调试文件,这是一种被大多数编译器应用的通用调试文件格式,而 C/C++ Devtools Support 则会解析 DWARF 文件,为 Chrome Devtools 在调试时提供 source map 相干的信息,使得开发者能够在 89+ 版本以上的 Chrome Devtools 上调试 C/C++ 代码。
调试简略的 C 利用
因为 DWARF 格局的调试文件能够提供解决变量名、格式化类型打印消化、在源代码中执行表达式等等,当初就让咱们理论来编写一个简略的 C 程序,而后编译到 WebAssembly 并在浏览器中运行,查看理论的调试成果吧。
首先让咱们进入到之前创立的 WebAssembly 目录下,激活 emcc 相干的命令,而后查看激活成果:
cd emsdk && source emsdk_env.sh
emcc --version # emcc (Emscripten gcc/clang-like replacement) 1.39.18 (a3beeb0d6c9825bd1757d03677e817d819949a77)
接着在 WebAssembly 创立一个 temp
文件夹,而后创立 temp.c
文件,填充如下内容并保留:
#include <stdlib.h>
void assert_less(int x, int y) {if (x >= y) {abort();
}
}
int main() {assert_less(10, 20);
assert_less(30, 20);
}
上述代码在执行 asset_less
时,如果遇到 x >= y
的状况会抛出异样,终止程序执行。
在终端切换目录到 temp
目录下执行 emcc
命令进行编译:
emcc -g temp.c -o temp.html
上述命令在一般的编译模式上,退出了 -g
参数,通知 Emscripten 在编译时为代码注入 DWARF 调试信息。
当初能够开启一个 HTTP 服务器,能够应用 npx serve .
,而后拜访 localhost:5000/temp.html
查看运行成果。
须要确保曾经装置了 Chrome 扩大:https://chrome.google.com/web…,以及 Chrome Devtools 降级到 89+ 版本。
为了查看调试成果,须要设置一些内容。
- 关上 Chrome Devtools 外面的 WebAssembly 调试选项
设置完之后,在工具栏顶部会呈现一个 Reload 的蓝色按钮,须要从新加载配置,点击一下就好。
- 设置调试选项,在遇到异样的中央暂停
- 刷新浏览器,而后你会发现断点停在了
temp.js
,由 Emscripten 编译生成的 JS 胶水代码,而后顺着调用栈去找,能够查看到temp.c
并定位到抛出异样的地位:
能够看到,咱们胜利在 Chrome Devtools 外面查看了 C 代码,并且代码停在了 abort()
处,同时还能够相似咱们调试 JS 时一样,查看以后 scope 下的值:
如上述能够查看 x
、y
值,将鼠标浮动到 x
上还能够显示此时的值。
查看简单类型值
实际上 Chrome Devtools 不仅能够查看原 C/C++ 代码中一些变量的一般类型值,如数字、字符串,还能够查看更加简单的构造,如构造体、数组、类等内容,咱们拿另外一个例子来展示这个成果。
咱们通过一个在 C++ 外面绘制 曼德博图形 的例子来展现上述的成果,同样在 WebAssembly 目录下创立 mandelbrot
文件夹,而后增加 `mandelbrot.cc
文件,并填入如下内容:
#include <SDL2/SDL.h>
#include <complex>
int main() {
// 初始化 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);
// 为画板填充随机的色彩
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,
};
}
// 计算 曼德博 汇合并绘制 曼德博 图形
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);
}
}
// 将咱们在 canvas 绘制的内容渲染进去
SDL_RenderPresent(renderer);
// SDL_Quit();}
上述代码差不多 50 行左右,然而援用了两个 C++ 规范库:SDL 和 complex numbers,这使得咱们的代码变得有一点简单了,咱们接下来编译上述代码,来看看 Chrome Devtools 的调试成果如何。
通过在编译时带上 -g
标签,通知 Emscripten 编译器带上调试信息,并寻求 Emscripten 在编译时注入 SDL2 库以及容许库在运行时能够应用任意内存大小:
emcc -g mandelbrot.cc -o mandelbrot.html \
-s USE_SDL=2 \
-s ALLOW_MEMORY_GROWTH=1
同样应用 npx serve .
命令开启一个本地的 Web 服务器,而后拜访 http://localhost:5000/mandelb… 能够看到如下成果:
关上开发者工具,而后能够搜寻到 mandelbrot.cc
文件,咱们能够看到如下内容:
咱们能够在第一个 for 循环外面的 palette
赋值语句哪一行打一个断点,而后从新刷新网页,咱们发现执行逻辑会暂停到咱们的断点处,通过查看右侧的 Scope 面板,能够看到一些有意思的内容。
应用 Scope 面板
咱们能够看到简单类型如 center
、palette
,还能够开展它们,查看简单类型外面具体的值:
间接在程序中查看
同时将鼠标挪动到 palette
等变量下面,同样能够查看值的类型:
在控制台中应用
同时在控制台外面也能够通过输出变量名获取到值,仍然能够查看简单类型:
还能够对简单类型进行取值、计算相干的操作:
应用 watch 性能
咱们也能够把应用调试面板外面的 watch 性能,增加 for 循环外面的 i 到 watch 列表,而后恢复程序执行就能够看到 i 的变动:
更加简单的步进调试
咱们同样能够应用另外几个调试工具:step over、step in、step out、step 等,如咱们应用 step over,向后执行两步:
能够查看到以后步的变量值,也能够在 Scope 面板中看到对应的值。
针对非源码编译的第三方库进行调试
在之前咱们只编译了 mandelbrot.cc
文件,并在编译时要求 Emscripten 为咱们提供内建的 SDL 相干的库,因为 SDL 库并不是咱们从源码编译而来,所以不会带上调试相干的信息,所以咱们仅仅在 mandelbrot.cc
外面能够通过查看 C++ 代码的模式来调试,而对于 SDL 相干的内容则只能查看 WebAssembly 相干的代码来进行调试。
如咱们在 41 行,SDL_SetRenderDrawColor 调用处打上断点,并应用 step in 进入到函数外部:
会变成如下的模式:
咱们又回到了原始的 WebAssembly 的调试模式,这也是难以避免的一种状况,因为咱们在开发过程中可能会遇到各种第三方库,然而咱们并不能保障每个库都能从源码编译而来且带上了相似 DWARF 的调试信息,绝大部分状况下咱们无法控制第三方库的行为;而另外一种状况则是有时咱们会在生产状况下遇到问题,而生产环境也是没有调试信息的。
上述情况临时还没有比拟好的解决办法,然而开发者工具却改良了上述的调试体验,将所有的代码都打包成繁多的 WebAssembly 文件,对应到咱们这次就是 mandelbrot.wasm
文件,这样咱们再也无需放心其中的某段代码到底来自那个源文件。
新的命名生成策略
之前的调试面板外面,针对 WebAssembly 只有一些数字索引,而对于函数则连名字都没有,如果没有必要的类型信息,那么很难追踪到某个具体的值,因为指针将以整数的模式展现进去,但你不晓得这些整数背地存储着什么。
新的命名策略参考了其余反汇编工具的命名策略,应用了 WebAssembly 命名策略局部的内容、import/export 的门路相干的内容,能够看到咱们当初的调试面板中针对函数能够展现函数名相干的信息:
即便遇到了程序谬误,基于语句的类型和索引也能够生成相似 $func123
这样的名字,大大提高了栈追踪和反汇编的体验。
查看内存面板
如果想要调试此时程序占用的内存相干的内容,能够在 WebAssembly 的上下文下,查看 Scope 面板里的 Module.memories.$env.memory
,然而这只能看到一些独立的字节,无奈理解到这些字节对应到的其余数据格式,如 ASCII 格局。然而 Chrome 开发者工具还为咱们提供了一些其余更加弱小的内存查看模式,当咱们右键点击 env.memory
时,能够抉择 Reveal in Memory Inspector panel:
或者点击 env.memory
旁边的小图标:
能够关上内存面板:
从内存面板外面能够查看以十六进制或 ASCII 的模式查看 WebAssembly 的内存,导航到特定的内存地址,将特定数据解析成各种不同的格局,如十六进制 65 代表的 e 这个 ASCII 字符。
对 WebAssembly 代码进行性能剖析
因为咱们在编译时为代码注入了很多调试信息,运行的代码是未经优化且简短的代码,所以运行时会很慢,所以如果为了评估程序运行的性能,你不能应用 performance.now
或者 console.time
等 API,因为这些函数调用取得的性能相干的数字通常不能反馈真实世界的成果。
所以如果须要对代码进行性能剖析,你须要应用开发者工具提供的性能面板,性能面板外面会全速运行代码,并且提供不同函数执行时破费工夫的明确断点信息:
能够看到上述几个比拟典型的工夫点如 161ms,或者 461ms 的 LCP 与 FCP,这些都是能反馈真实世界下的性能指标。
或者你能够在加载网页时敞开控制台,这样就不会波及到调试信息等相干内容的调用,能够确保比拟实在的成果,等到页面加载实现,而后再关上控制台查看相干的指标信息。
在不同的机器上进行调试
当在 Docker、虚拟机或者其余原创服务器上进行构建时,你可能会遇到那种构建时应用的源文件门路和本地文件系统上的文件门路不统一,这会导致开发者工具在运行时能够在 Sources 面板里展现出有这个文件,然而无奈加载文件内容。
为了解决这个问题,咱们须要在之前装置的 C/C++ Devtools Support 配置外面设置门路映射,点击扩大的“选项”:
而后增加门路映射,在 old/path 里填入之前的源文件构建时的门路,在 new/path 里填入当初存在本地文件系统上的文件门路:
上述映射的性能和一些 C++ 的调试器如 GDB 的 set substitute-path
以及 LLDB 的 target.source-map
很像。这样开发者工具在查找源文件时,会查看是否在配置的门路映射里有对应的映射,如果源门路无奈加载文件,那么开发者工具会尝试从映射门路加载文件,否则会加载失败。
调试优化性构建的代码
如果你想调试一些在构建时进行优化后的代码,可能会取得不太现实的调试体验,因为进行优化构建时,函数内联在一起,可能还会对代码进行重排序或去除一部分无用的代码,这些都可能会混同调试者。
目前开发者工具除了对函数内联时不能搞很好的反对外,可能反对绝大部分优化后代码的调试体验,为了缩小函数内联反对能力欠缺带来的调试影响,倡议在对代码进行编译时退出 -fno-inline
标记来勾销优化构建时(通常是带上 -O
参数)对函数进行内联解决的性能,将来开发者工具会修复这个问题。所以针对之前提到的简略 C 程序的编译脚本如下:
emcc -g temp.c -o temp.html \
-O3 -fno-inline
将调试信息独自存储
调试信息蕴含代码的详细信息,定义的类型、变量、函数、函数作用域、以及文件地位等任何有利于调试器应用的信息,所以通常调试信息比源代码还要大。
为了减速 WebAssembly 模块的编译和加载速度,你能够在编译时将调试信息拆分成独立的 WebAssembly 文件,而后独自加载,为了实现拆分独自文件,能够在编译时退出 -gseparate-dwarf
操作:
emcc -g temp.c -o temp.html \
-gseparate-dwarf=temp.debug.wasm
进行上述操作之后,编译之后的主利用代码只会存储一个 temp.debug.wasm
的文件名,而后在代码加载时,插件会定位到调试文件的地位并将其加载进开发者工具。
如果咱们想同时进行优化构建,并将调试信息独自拆分,并在之后须要调试时,加载本地的调试文件进行调试,在这种场景下,咱们须要重载调试文件存储的地址来帮忙插件可能找到这个文件,能够运行如下命令来解决:
emcc -g temp.c -o temp.html \
-O3 -fno-inline \
-gseparate-dwarf=temp.debug.wasm \
-s SEPARATE_DWARF_URL=file://[temp.debug.wasm 在本地文件系统的存储地址]
在浏览器中调试 ffmpeg 代码
通过这篇文章咱们深刻理解了如何在浏览器中调试通过 Emscripten 构建而来的 C/C++ 代码,上述解说了一个一般无依赖的例子以及一个依赖于 C++ 规范库 SDL 的例子,并且解说了现阶段调试工具能够做的事件和限度,接下来咱们就通过学到的常识来理解如何在浏览器中调试 ffmpeg 相干的代码。
带上调试信息的构建
咱们只须要批改在之前的文章中提到的构建脚本 build-with-emcc.sh
,退出 -g
对应的标记:
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd ffmpeg-4.3.2-3
ARGS=(
-g # 在这里增加,通知编译器须要增加调试
-I. -I./fftools -I$BUILD_DIR/include
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib
-Qunused-arguments
-o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c
-lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread
-O3 # Optimize code with performance first
-s USE_SDL=2 # use SDL2
-s USE_PTHREADS=1 # enable pthreads support
-s PROXY_TO_PTHREAD=1 # detach main() from browser/UI main thread
-s INVOKE_RUN=0 # not to run the main() in the beginning
-s EXPORTED_FUNCTIONS="[_main, _proxy_main]" # export main and proxy_main funcs
-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" # export preamble funcs
-s INITIAL_MEMORY=268435456 # 268435456 bytes = 268435456 MB
)
emcc "${ARGS[@]}"
cd -
而后以此执行其余操作,最初通过 node server.js
运行咱们的脚本,而后关上 http://localhost:8080/ 查看成果如下:
能够看到,咱们在 Sources 面板外面能够搜寻到构建后的 ffmpeg.c
文件,咱们能够在 4865 行,在循环操作 nb_output
时打一个断点:
而后在网页中上传一个 avi
格局的视频,接着程序会暂停到断点地位:
能够发现,咱们仍然能够像之前一样在程序中鼠标挪动下来查看变量值,以及在右侧的 Scope 面板里查看变量值,以及能够在控制台中查看变量值。
相似的,咱们也能够进行 step over、step in、step out、step 等简单调试操作,或者 watch 某个变量值,或查看此时的内存等。
能够看到通过这篇文章介绍的常识,你能够在浏览器中对任意大小的 C/C++ 我的项目进行调试,并且能够应用目前开发者工具提供的绝大部分性能。
参考链接
- https://www.infoq.com/news/20…
- https://developer.chrome.com/…
- https://lucumr.pocoo.org/2020…
- https://v8.dev/docs/wasm-comp…
- Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)”)
- Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium
❤️/ 感激反对 /
以上便是本次分享的全部内容,心愿对你有所帮忙 ^_^
喜爱的话别忘了 分享、点赞、珍藏 三连哦~
欢送关注公众号 程序员巴士 ,来自字节、虾皮、招银的三端兄弟,分享编程教训、技术干货与职业规划,助你少走弯路进大厂。