问题背景
y 同学反映电脑换成 M2 后,使用 nopack 开发时经常会出现 CPU 占用 100% 的情况,已经严重影响到了开发效率
nopack: 公司内部的一个 bundless 工具,已支持 100+ 应用近 2年,相关分享见: 2021-10-14 凯多 巨石应用解决方案 nopack
相比于 webpack 这样的 bundler 📦 打包后产物只有少量的文件,Vite 与 nopack 这样的 bundless ⚡️ 工具不会在开发模式下打包应用的源代码。 这两者是基于浏览器 native ES Modules,这样的模式让热更新无比的快速,因为只需编译转换一个文件,不用像 bundler 那样从头开始打包
所以 bundless 工具开发模式下往往会出现大量的小文件请求,对于性能较差的电脑 Google Chrome 进程出现短暂的 CPU 占用 100% 的情况还是比较常见
该同学使用的是最强 M2,问题表现是 nopack 进程偶现 CPU 占用 100% 且不会下降,这属实让人一下难以接受
问题排查
一开始只能要 y 同学通过 node –cpu-prof 选项去启动 nopack,当程序退出时希望够从自动生成的 .cpuprofile 文件中找到突破口
–cpu-prof: Starts the V8 CPU profiler on start up, and writes the CPU profile to disk before exit.
遗憾的是如上图 .cpuprofile 文件中未能发现 🔍 有占用 CPU 过高的函数,没有发现较大价值的线索。另一个问题是 10次退出程序中大概只有 1次会生成 .cpuprofile 文件 🥲,分析文件的生成也不稳定 …
下一步的排查中我在 nopack 代码中加入了如下的心跳检测代码,确认进程是不是真瘫痪了
1 | setInterval((){ |
结果是当 CPU 占用到了 100% 时,setInterval 里面的 console 也停止了,此时基本确认了进程可能陷入了某种致命错误或者死循环中,这大概率是程序的 bug 了 🐛
最后只能寄希望于调试利器 lldb 了,来看看进程卡死后,最后运行的代码是什么
lldb: 是 macOS 和 iOS 平台下的一款命令行调试工具,它允许开发者在本地调试运行中的应用程序。lldb 使用了 gdb 的语法和命令,但是提供了更多的功能和本地调试器的支持,比如可以调试原生框架和 C++ 代码。
通过 lldb 线程的堆栈回溯,发现了最后堆栈的停留在了 node::http2::Http2Session::OnStreamClose
函数调用,这是一个很可疑的点
1 | * thread #1, name = 'nopack', queue = 'com.apple.main-thread', stop reason = signal SIGSTOP |
因为对比一个正常运行的 nopack 进程的堆栈回溯时,已经运行结束的 node::http2::Http2Session::OnStreamClose
等函数就没有出现在如下的堆栈中
1 | * thread #1, name = 'nopack', queue = 'com.apple.main-thread', stop reason = signal SIGSTOP |
前面说了 bundless 工具开发模式下往往会出现大量的小文件请求,所以 nopack 默认是 http2 协议启动,相比于 http1 协议能够大幅提升并发请求
而这里显示停留在了 Node.js http2 的 OnStreamClose 方法,如果尝试使用 http1 协议来启动没有问题就说明了 CPU 占用 100% 极有可能是 Node.js http2 在 M2 中的一个 bug
1 | // src/node_http2.cc |
问题小结
使用 http1 协议后暂未能再复现 CPU 占用 100% 的问题,至于是不是 Node.js http2 的 bug,还需打个 Node.js debug 包来进行排查与确认,只能后续需求不忙再跟进了