基于List的滑动过程卡顿率问题分析&案例
1. 场景导入
卡顿率是用来判断页面操作是否流畅的一个指标,可以衡量应用启动、页面内滑动、页面转场操作是否流畅。在应用内使用ArkUI的List组件实现的列表,在页面滑动过程中,通过卡顿率来判断页面滑动是否流畅。
2. 性能指标
期望上屏时间是和帧率有关,如120FPS,期望上屏时间为1000ms / 120FPS = 8.3m。
2.1 性能衡量起止点介绍
以大于300mm/s的速度,连续3次,每次半屏。List组件的抛滑过程,可以通过应用进程下的H:APP_LIST_FLING泳道标识。性能衡量的起点为第一次抛滑开始点,衡量的结束点为第三次抛滑的结束点。
Tip:尾动效阶段系统会进行降帧处理,所以如果统计卡顿率的情况,通常只会统计从抛滑开始到尾动效起点的这一阶段。
3. 问题定位流程
3.1 常规定位前置流程
3.1.1 查看操作录屏辅助定位
处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如卡顿的页面布局情况、卡顿的现象等等。
3.1.2 Trace抓取
滑动帧率Trace抓取请参考【附录1: 滑动帧率Trace抓取方法】。
3.2 问题定位思路
滑动卡顿类问题的通用定位思路为先根据性能工厂的测试报告确认卡顿率是否达标。如果不达标则确认单次抛滑的起止点,在Present Fence泳道找到超时帧,根据Trace信息进一步确认问题点,确认责任领域并对齐处理,处理流程如下图:
3.2.1 确认起止点
问题起止点确认
参考[2.2 性能衡量起止点介绍](#2.2 性能衡量起止点介绍)
3.2.2 找问题点
3.2.2.1 判断卡顿帧进程
收藏Present Fence,H:APP_LIST_FLING,Frame泳道中的RS侧帧泳道和应用侧帧泳道。观察图形上屏超时是由应用侧帧超时引起的,还是由RS侧帧超时引起的。
应用进程问题
如下图,可以看到261,263,264帧耗时较长,最终264帧超时导致图形上屏时间超过了预期。
RS进程问题
RS进程丢帧可能是应用进程导致的,如上图RS侧丢帧,可以看到RS侧丢帧原因是由于应用进程的帧耗时较长,提交较晚导致。所以这种情况只分析应用侧丢帧原因即可。如果应用进程中没有丢帧且每帧耗时比较均匀,但是RS侧发生丢帧,则说明不是应用侧导致丢帧,此时只分析RS进程丢帧原因即可。
3.2.2.2 找丢帧Trace
点击卡顿帧查看详情,点击跳转应用进程。
分析详细丢帧Trace。
3.2.3 根本分析方法
应用侧的渲染流程如下图所示,了解ArkUI的渲染流程有助于我们定位应用侧的卡顿问题出现在哪个环节。
阶段 | 描述 |
Animation | 动画阶段,在动画过程中会对相应的组件标记脏区 |
Events | 事件处理阶段,比如手势事件处理。在手势处理过程中也会对组件标记脏区 |
UpdateUI | 组件在首次创建或状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会通过View的方法生成相应的组件树结构和属性样式修改任务。 |
Measure | 执行组件的大小测算任务。 |
Layout | 执行组件的布局任务。 |
Render | 执行绘制任务,执行完成后会标记请求刷新RSNode绘制 |
SendMessage | 请求刷新界面绘制 |
应用进程丢帧分析
跟据应用帧跳转到应用侧的Trace泳道,初步分析每帧耗时较长的阶段。
261帧中H:LazyForEach predict | LazyForEach的耗时11.93ms,是懒加载组件预创建耗时较长导致丢帧。263帧中H:CustomNode:BuildRecycle耗时9.644ms,由于组件复用耗时长丢帧。
264帧中H:CreateTaskMeasure[ListItem][self:4426][parent:0]耗时18.384ms,分析这个组件节点下面的组件布局的Trace,由于组件结构复杂嵌套层级多导致丢帧。
序号 | 所属泳道 | Trace点 | 描述 |
1 | 应用进程 | H:LazyForEach predict | LazyForEach预处理 |
2 | 应用进程 | H:CustomNode:BuildRecycle 自定义组件名 | 自定义组件的复用,包含执行aboutToReuse方法的耗时 |
3 | 应用进程 | H:CreateTaskMeasure[组件名][self:组件id][parent:父组件id] && H:Measure[组件名][self:组件id][parent:父组件id] | 执行组件的布局测量任务 |
通过ArkTS CallStack泳道,可以看到应用侧具体调用栈,进一步分析定位问题原因。如下图通过调用栈可以看到在递归进行组件复用时耗时较长,可以找应用看下是否组件嵌套层级过深;updateDirtyElement耗时长,应用侧可以分析下是否存在无关节点被触发更新;aboutToReuse耗时长可以看下应用侧该回调中是否存在耗时逻辑。
应用UI组件的嵌套情况,可以通过ArkUI Inspector查看。
经验总结:应用进程丢帧通常是组件结构嵌套层级深、耗时应用业务逻辑阻塞UI线程等问题导致。如果是UI结构复杂问题可以让应用通过减少嵌套层级、使用组件复用等方式优化。如果是有耗时业务逻辑,则可以通过将耗时逻辑放到Taskpool或Worker中优化。
RS进程丢帧分析
RS进程丢帧一般是由于界面结构过于复杂或者GPU负载过大等原因导致的。如果应用侧没有丢帧且每帧耗时比较平均,则可以初步判断应用侧没有问题,同时也可以通过应用侧Trace中H:SendCommands下的H:MarshRSTransactionData cmdCount查看应用提交的绘制指令树是否过多。
如下图RS侧丢帧原因是由于RS侧的H:RSUniRender:FlushFrame阶段耗时较长,此时可以找图形子系统进一步确认耗时根因。
经验总结:RenderService侧丢帧通常是应用侧UI线程阻塞提交绘制指令较慢导致,此时应当初步定位应用侧耗时长原因。如果应用侧无丢帧情况,绘制指令正常提交,则可以找图形子系统协助进一步分析丢帧原因。
4. 场景根因归档
4.1 耗时任务阻塞UI主线程
Stage模型下的线程主要有三类:主线程、TaskPool、Worker。主线程主要用于执行UI绘制、处理应用代码逻辑,TaskPool和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他CPU密集型任务。
当主线程存在耗时的计算任务时,会使主线程阻塞,导致应用丢帧。
4.1.1 问题根因分析
Present Fence泳道中超时上屏的帧是269号帧提交太晚导致。
通过帧详情跳转按钮进入到对应的Trace,UI线程没有提交绘制指令,但是处于运行状态。
通过ArkTS Callstack泳道可以主要的耗时点在:SearchResultCommonList.ts,AlbumM.ts,index.ts文件中对数据的解析和处理。
4.1.2 优化方案
在List滑动过程中对数据进行处理耗时较长,占用大量CPU资源,导致主线程被阻塞,这部分数据处理的相关业务逻辑与UI绘制无关,但却长时间占用CPU资源,导致UI线程被阻塞丢帧。可以将该数据处理逻辑放到TaskPool中利用多核并行化处理优化。除应用侧的耗时逻辑外,某些与UI绘制无关的耗时系统接口调用也可以放到TaskPool中优化。
附录1:滑动帧率Trace抓取方法
Step1:电脑连接上设备,在DevEco Studio上打开Profiler。
Step2:设备上运行需要测试的应用,在设备列表选择设备,选择要测试的应用,和主进程。
Step3:创建Frame模板,并点击录制,待所有泳道都进入到recording状态后。
Step4:执行滑动操作。
Step5:操作完成,点击结束录制,待分析完成后,可以在泳道上看到trace数据。
Step6:trace的路径点击Help -> Show Log in Explorer。
返回到上一层,找到.insight文件下。
附录2:List滑动场景通用Trace点说明
查看整体的帧信息
基于List组件的滑动过程中,在应用主进程下面会包含H:touchEventDispatch,H:APP_LIST_FLING,H:TRAILING_ANIMATION泳道。在Frame泳道下面有应用侧主线程帧泳道和Render Service帧泳道。在Render Service泳道下会有Present Fence泳道。
序号 | 主泳道 | 泳道名 | 说明 |
1 | Frame | 主线程名 | 应用侧帧信息 |
2 | Frame | render_service | RS侧帧信息 |
3 | Process | Present Fence | 图形上屏信号 |
4 | Process | H:touchEventDispatch | 手指开始拖动到抬起的阶段 |
5 | Process | H:APP_LIST_FLING | 手指按下开始拖动到抬手后的惯性滚动及最后尾动效的抛滑全过程 |
6 | Process | H:TRAILING_ANIMATION | 抛滑尾动效 |
查看应用侧单个帧信息
序号 | Trace | 描述 | 参数说明 |
1 | H:ReceiveVsync dataCount:24bytes now:[时间戳] expectedEnd:[时间戳] vsyncId:[index] | 收到Vsync信号 | 时间戳--纳秒级,index--应用侧vsyncId的序号 |
2 | H:OnVsyncEvent now:[时间戳] | 响应Vsync事件 | 时间戳--纳秒级 |
3 | H:OnIdle, targettime:[时间戳] | Vsync周期内的空闲时间,检查是否有新的事件需要处理 | 时间戳--纳秒级, 在这个时间之前完成该任务 |
4 | H:FlushVsync | 处理用户输入、刷新视图同步事件、计算帧信息、提交绘制渲染等 | |
5 | H:LazyForEach predict | LazyForEach预处理 | |
6 | H:List predict | List预处理 |
刷新视图同步事件
序号 | Trace | 描述 | 参数说明 |
1 | H:RunningCustomAnimation num:[1] | 自定义动画 | num中的数字表示动画的数量,如果大于0,表示还有动画在运行 |
2 | H:UITaskScheduler::FlushTask | 刷新UI界面,包括布局计算、渲染和提交等 | |
3 | H:FlushLayoutTask | 执行布局任务 | |
4 | H:CreateTaskMeasure[List][self:1643][parent:1642] | 创建测量任务 | 组件名、组件ID、父组件ID |
5 | H:Measure[List][self:1643][parent:1642][key:list] | 组件测量 | 组件名、组件ID、父组件ID |
6 | H:ListLayoutAlgorithm::MeasureListItem:17 | 计算列表项的布局尺寸 | 列表项索引 |
7 | H:CreateTaskLayout[List][self:1643][parent:1642] | 创建布局任务 | |
8 | H:Layout[List][self:1643][parent:1642][key:list] | 执行布局任务 | 组件名、组件ID、父组件ID |
9 | H:FlushRenderTask 1 | 执行渲染任务 | 节点数量 |
10 | H:FrameNode[List][id:1643]::RenderTask | 单个渲染任务执行 | 组件名,组件ID |
11 | H:FlushMessages | 绘制消息 | |
12 | H:SendCommands | 通知图形侧进行渲染 | |
13 | H:MarshRSTransactionData cmdCount:39 transactionFlag [41088,384] | 提交的渲染数据 | 进程ID、任务序号 |
14 | H:HandleOnAreaChangeEvent | 执行OnAreaChange任务 | 一般耗时在us级 |
15 | H:HandleVisibleAreaChangeEvent | 执行OnVisibleChange任务 | 一般耗时在us级 |
列表预创建
序号 | Trace | 描述 | 参数说明 |
1 | H:Builder:BuildLazyItem | 构建LazyItem | |
2 | H:CustomNode:BuildItem [DTAvatar][self:2820][parent:2819] | 构建自定义组件 | 组件名、组件ID、父组件ID |
3 | H:Create[Image][self:2827] | 组件创建 | 组件名、组件ID、父组件ID |
4 | H:CustomNode:OnAppear | OnAppear节点 | |
5 | H:aboutToAppear | 组件初始化生命周期 | |
6 | H:ExecuteJS | 运行ArkTS业务逻辑 |
列表预加载
序号 | Trace | 描述 | 参数说明 |
1 | H:CreateTaskMeasure[ListItem][self:2791][parent:-1] | 创建并执行测量任务 | 组件名、组件ID、父组件ID |
2 | H:CreateTaskLayout[ListItem][self:2791][parent:-1] | 创建并执行布局任务 | 组件名、组件ID、父组件ID |
3 | H:FlushMessages & H:SendCommands | 发送消息通知图形侧进行渲染 |