Chrome Renderer 进程 CPU 占用 100% 排查 (上)

image

背景

这几天在调试某个详情页时, 每当我打开 Chrome Devtool 时页面就立马会卡死, 大约过了3分钟后又能恢复响应。卡死的时候运行top命令查看 CPU 负载情况如上图所示Google Chrome Helper (Renderer)进程达到了 100% ⚠️

问题排查

如上图可以看到内存占用正常且没有一直增加排除了内存泄漏的可能, 然后打开 Mac 活动监视器找到该进程的打开的文件和端口面板发现也并没有占用过多的文件句柄排查了文件句柄泄漏的可能性

使用 lldb

Chrome

一开始直接准备使用 lldb 进入该进程, 不出意外失败了, 正式版本 Chrome 可不能轻易被调试

1
2
3
➜  sudo lldb -p 69951
(lldb) process attach --pid 69951
error: attach failed: attach failed (Not allowed to attach to process. Look in the console messages (Console.app), near the debugserver entries, when the attach failed. The subsystem that denied the attach permission will likely have logged an informative message about why it was denied.)

Chromium

好在 Chromium 也能顺利复现该问题, 继续使用 lldb 进入该进程。结果如下图虽然能够调试但显示的都是毫无价值的执行堆栈…

image

接着 lldb 中运行process save-core ./g.core命令生成 coredump 文件也没有发现线索(依稀记得上一次 Chrome CPU 占用 100% 时就是在 coredump 文件中发现了 React 某个代码片段重复了 3000+次, 最后定位到了是低版本 React 开发环境的一个 bug)

使用 Instruments (Xcode)

在 lldb 调试不乐观的情况下, 询问了一下 ChatGPT 说可以使用 Xcode 的 Instruments 工具进行分析。该工具的使用十分像我们熟悉的 Chrome Devtool Performance 面板, 首先点击开始记录然后再停止就能拿到火焰图

🤔 发现了可疑线索。如下图中Call Tree可以看到最后停留在重复运行了若干次0x10e0e4ea8地址的函数, 猜想进程可能陷入了某种死循环或者因为什么条件陷在了该函数没有能继续运行下去
image

编译 Chromium

经历了上面 lldb 与 Instruments 的尝试发现调试生产版本的 App 总会因为安全问题或者调试信息的缺失而无法定位到最终的原因, 没办法只能决定尝试本机编译一个 debug 版本的 Chromium

⚠️ 编译 Chromium 前你需要慎重确定两件事

  1. 电脑具备访问外网的能力(下载 Google 的工具链都需要翻墙)
  2. 电脑系统较新(我首次编译是 MacOS 12.x 编译出错查了一下是 SDK 缺失,最后花了 2小时升级到了 15.x)

最后就可以按照 Building Chromium for Mac 文档进行操作了。因为之前编译过 v8, 本次就省去了重新下载 Google 的工具链与配置环境的时间, 即使这样仅下载最新没有 git history 的 Chromium 与编译 Chromium 仍然花了接近 5个小时…

编译 debug 包

如下图就表示编译成功了
image

运行 debug 包

接着可以运行刚才编译成功的 debug 包, 如下图中可以看到 Chromium 自己的运行日志与加载的 taobao.com 网页中 JS 的 console.log 等都会出现在终端的日志中

image

问题浮现

好在 debug 版本的 Chromium 也能顺利复现该问题, 依然还是使用 lldb 进入该进程, 此时的执行堆栈的信息终于是完整的了

通过仔细分析下图的堆栈函数调用

  • -> frame 10 setScriptSource 看变量名字应该是给 JS Script 标签绑定文件内容
  • -> frame 9 PatchScript 给 JS Script 内容进行某种更新修改操作?
  • -> frame 7 CalculateDifference 什么情况下需要 diff 后修改 JS Script 内容?
  • -> frame 0 FindEditPath 这个应该就对应了之前发现的可疑0x10e0e4ea8地址的函数

image

问题定位

于是我在源码中增加了 2 行代码, 看看具体这个 JS Script 有何特殊之处?
image
然后再编译与运行 Chromium, 发现 JS Script 内容已经正常被打印了出来, 内容的最后一行是 sourcemap 的地址也就知道了该 JS 的文件名为 detail.js
image

问题结论

回想了一下 detail.js 的唯一特殊之处就是之前因为要复现问题 Overrides 了该文件的部分代码,这也说明了上面的堆栈 PatchScript 为什么在给 JS Script 内容进行某种更新修改操作

为了证实猜想于是取消了勾选Enable Local Overrides, 发现卡死问题不再复现, 问题得到解决 ✅ 。至于为什么勾选了会造成问题就得深入分析 FindEditPath 函数的代码了, To be continued…

image