找到运行耗时的代码


发布于 2020-10-31


最近在做Chromium的性能优化,降低UI的卡顿率。

在我的印象中Chromium似乎是不卡的,因为设计好的线程、进程模型,那些耗时的代码逻辑,都抛到非UI线程上,或者其他的进程里去执行了。另外可能我的电脑配置比较好,所以一直用Chrome浏览器都是感觉很流畅的。

本地分析UI线程卡顿

本地分析UI线程卡顿,如果卡顿比较明显,容易复现的话,直接用wpt(window performance toolkit)就可以了。用wpr(windows performance recorder)抓取卡顿线程,用wpa(windows performance recorder)分析卡顿。可以参考我之前写的博客WPA分析卡顿

线上检测UI线程的卡顿

检测UI线程的卡顿的逻辑:创建一个专有的监控线程,比如定时的200ms往UI线程上抛一个空的检测任务并立刻返回监控线程,下次定时器触发的时候,看看上次的任务有没有返回,如果返回了,则说明UI在200ms内可以响应,没有卡顿。如果上次任务没有返回,则认为UI线程发生了卡顿,如下图所示:

ui hang

监控数据显示,Chromium浏览器上发生的UI卡顿比我意料的要多很多。

线上抓取UI卡顿现场

上面的线上检测UI线程卡顿逻辑,当定时器触发时,上次的检测任务没有返回则认为UI线程正在卡顿中。这时候有两种办法:

  • 在监控线程上抓取当前进程的dump。通过分析dump可以知道当时进程卡顿时的各个线程状态,可以看到UI线程当时卡顿的调用栈
  • 提前拿到UI线程的句柄,检测到卡顿时,通过GetThreadContext拿到UI线程的上下文,然后通过StackWalk去拿到UI线程的卡顿调用栈

第一种通过抓取dump的方法,监控起来对用户性能影响比较大。生成的dump文件也比较大。另外分析起来也不是很方便。唯一的好处就是抓取的信息比较多,能够综合分析用户当时卡顿的情况。

第二种方法仅仅是拿到当时UI线程卡顿的调用栈偏移地址,所以操作比较轻量,对用户性能的影响比较下。另外抓取的调用栈偏移地址数据量比较小,可以直接打点返回来,还可以通过栈顶地址进行聚类分析,找到卡顿热点函数。

第二种方法看起来很美好,但是有个致命的缺点,就是抓取的卡顿调用栈信息不够准确。如果想要抓取比较准确的卡顿调用栈,则在调用GetThreadContext拿到UI线程上下文前,必须先调用SuspendThread把UI线程挂起,抓取完信息之后再调用ResumeThread恢复UI线程。但是SuspendThread的文档里警告说SuspendThread是用于调试的目的,如果被挂起的线程用于同步对象,则会导致一些死锁。根据我的实际经验,SuspendThread确实风险比较大。另外进程还加载了很多系统和3方的dll模块,也是导致分析到的卡顿信息不够准确。

因为CPU跑的很快,从实际的结果来看,这两种方法也不是总能精确的抓到卡顿的地方。

Cygnus Solutions

Cygnus Solutions是GNU几个主要软件产品的维护者,也是GCC项目的主要贡献者。Cygnus还是Cygwin最早的开发者, Cygwin由一个POSIX层和一组移植到微软windows操作系统的GNU工具集组成。

GCC编译器中有个编译器开关-finstrument-functions,打开这个开关之后,编译会在编译的每个函数插入两个插桩函数,如下图所示:

cyg profiler

在进入函数的时候插入__cyg_profile_func_enter函数,在函数退出时插入__cyg_profile_func_exit,这样我们在函数进入时记录一下时间,在函数退出时再计算一下函数执行了多久,就能拿到所有函数的执行时间数据了。

Chromium是用Clang/LLVM,也是继承了GCC的这个-finstrument-functions开关。

因为编译器为每个函数都插入了两个函数,因此要保证的实现插入函数对性能的影响比较小。此外也要保证实现插入函数的逻辑尽可能简单。如果调用了一些c/c++的库函数,那些库函数也被插入了插桩函数,会引起循环调用。