PC上“大文件打开”场景多线程性能优化实践
1、背景介绍
在PC应用的HarmonyOS改造过程中,我们发现“大文件打开”场景与竞品的性能差异较大,此场景对于PC上办公类软件是高频操作,对于这类场景,并行化处理效果显著是个不错的解决思路。以下是针对“超大富文本打开”场景做的优化案例,介绍相关的分析和优化经验。
2、问题分析
首先是通过SmartPerf中的Hitrace工具生成应用的执行流程耗时以及线程间的依赖关系图。图1是SmartPerf绘制的原始Hitrace泳道图,从图中可以看出:富文本打开进程下3个主要线程的依赖关系,其中7589线程是ArkTS壳线程,负责拉起QT的窗口绘制线程7623(QT采用单线程自渲染,窗口绘制后会进行渲染),而窗口绘制线程在执行中又会拉起文件解析线程17844,并在两个阶段等待解析的结果,窗口绘制是整个场景的主要耗时。
图1:大文件打开过程泳道图
因为PC应用大部分是用C++语言开发,其界面绘制渲染也是由QT框架独立完成,涉及到的ArkTS相关的trace事件较少,图1中无法再得出更多的结论,需要手动为程序源码增加trace事件。
3、根因定位
增加trace找到相对最耗时的底层函数切片,从图2中我们发现是png图片的解析函数readPngImage,且该函数被调用了多次。通过源码向上溯源可以发现是由函数render调用的,将图片用作三方应用首页和侧边栏填充的对象。因为示例中图片较大、富文本页面较多,而应用的窗口绘制线程是串行处理的,所以多次执行成了拖慢整个流程的瓶颈。
图2:增加相对最耗时的底层函数后的泳道图
进一步向上溯源,我们发现render是在paintEvent事件处理的回调函数中被调用的。为了弄清楚热点函数的顶层调用逻辑,我们梳理了QT框架的窗口绘制流程:“打开”按钮的鼠标点击事件处理函数会调用QT框架中的DrawWidget函数,该函数会递归执行,完成对整个窗口的控件树的先序遍历。每遍历一个widget,该函数就会根据阴影及层叠关系计算出该widget的大小,并向事件循环中注入一个paintEvent。最终这些paintEvent的处理回调函数会完成真正的界面绘制。
4、优化方案设计及效果验证
根据以上分析,我们思考是否有可能在界面绘制前,也就是富文本解析过程中,利用线程池对这些图片进行并行预加载,这样可以充分利用PC多核的优势。如图3所示,我们设计一种图片cache,通过这个cache将负责解析图片的子线程与窗口绘制线程解耦,即子线程可以并发向cache中加入已解析完的图片,窗口绘制线程后访问cache时,如果所需的图片还未解析完,即cache未命中,可以继续使用原有的串行机制自行解析,从而避免生产者-消费者模式可能导致的阻塞。同时,cache机制也设计了线程安全机制,即通过mutex互斥锁保证cache的增/删/改/查操作的原子性。
图3:方案优化图
希望实现并行预加载,还需要解决另一个问题:绘制过程中使用的图片和刚解析出来的图片并不一致,图片还需要被缩放变换,以满足小窗中显示缩略图的尺寸。优化前图片是边解析边缩放,优化后解析出来的图片像素在缩放中又被遍历一遍,所以会新增一部分开销,如图4所示。我们的处理方法是在文件解析时给定一个近似的缩放大小,数值来自后续对小窗绘制大小的调试采集平均值,这样可以避免大多数场景下缩放带来的开销。小窗的大小在文件打开操作执行时即可以获取,直接使用该大小即可。
图4:使用图片与解析结果图片不一致优化方案示意图
经过并行化预加载改造后,如图5所示,可以看到图片解析任务被分发到其他子线程执行,文件解析线程的耗时略有增加,但是窗口绘制线程的耗时明显减小,整个场景耗时减少了3s多。
图5:优化后泳道图
5、总结
本文以超大富文本打开场景为例,展示了PC应用大文件处理的并行化预加载机制的设计思路、实践要点,以及最终取得的优化效果。
相关经验总结如下:
- PC应用大多面向办公场景,在单核性能不占优的情况下,大文件处理可能会成为比较明显的性能瓶颈,此时应该考虑对主线程负责窗口绘制的逻辑和文件处理的逻辑进行解耦,并对文件处理实施并行化改造。对于文件处理融入窗口绘制过程中的情况,还需要考虑将并行化的处理提前。
- PC应用大部分都是用C/C++等Native语言编写,主要耗时部分大多不会触发系统自带的trace事件,导致难以用SmartPerf定位性能瓶颈。此时可以考虑先用Hiperf进行采样分析,然后对其中发现的热点函数增加trace打点。
- 在实施并行化改造的过程中,可以采用非阻塞的cache模式,即在并发任务未完成时--cache miss时,主线程仍然具备自行处理的能力,避免空等。另外,并发访问数据结构的临界区一定要尽可能控制的小,尽量减少互斥锁对性能的影响。
更多推荐
所有评论(0)