概述
openharmony的dfx子系统提供了为应用框架以及系统底座核心模块的性能打点能力,每一处打点即是一个trace,其上附带了记录执行时间、运行时格式化数据、进程或线程信息等。开发者可以使用smartperf-host调试工具对trace进行解析,在其绘制的泳道图中,对应用运行过程中的性能热点进行分析,得出优化方案。本文旨在介绍openharmony中常用的trace,解释它们的含义和用途,并阐述如何通过这些trace来识别潜在的性能问题。同时,我们还将详细介绍trace的工作原理,帮助读者更好地理解这些trace及如何实现性能数据的采集和分析。通过本文的阅读,读者将对openharmony中的trace有一个深入的了解,为应用程序性能优化提供有力支持。
常用trace及含义
下面将从渲染流程入手,配合常用场景介绍常用trace。
渲染流程
与其他操作系统相同,openharmony也是由vsync信号控制每一帧绘制操作的时机。vsync信号是一个垂直同步信号,它指示显示器在垂直空白期之后开始下一帧的刷新。设备的屏幕以固定的频率发送vsync信号,以刷新率60hz举例,则屏幕每隔16.6ms发送一次vsync信号。在收到vsync信号后,ui后端引擎开始准备屏幕的下一帧绘制,然后应用程序提交渲染命令,用于描述图形绘制、纹理设置、着色器使用等。一旦应用程序提交了渲染命令,ui后端引擎会将其添加到渲染队列中,并在合适的时机执行这些渲染命令,通常会在后台线程执行,以确保主线程不被长时间阻塞。当这些渲染命令被ui后端引擎执行时,它们会被传递给图形系统render service进行处理,图形系统会根据命令进行相应的图形计算和渲染操作,如顶点变换、光照、纹理贴图等。在图形系统完成渲染后,渲染结果将被写入帧缓冲区。帧缓冲区是一个内存区域,存储用于显示器输出的图像数据。一旦帧缓冲区更新完成,ui后端引擎会等待直到下一个vsync信号到来,这个过程是为了确保渲染结果在显示器垂直消隐之前准备好。当下一个vsync信号到来时,ui后端引擎将已经准备好的帧缓冲区的内容发送给显示器,显示器根据这些数据刷新自己的像素,至此完成一整个渲染周期。如图1所示。
图1 渲染流程图
从trace角度来看,一帧的渲染流程如下:
(1)vsync信号到达;
(2)ui后端引擎进行第一帧绘制;
(3)向render service通信,传输绘制命令并请求一帧;
(4)render service对多个图层进行合并,计算刷新区域,然后进行渲染和绘制本帧;
(5)完成一帧绘制后交给屏幕。
一帧的渲染流程中的ui后端引擎的常用trace的含义如图2所示。
图2 ui后端引擎渲染trace泳道图
序号 | trace | 参数说明 | 描述 |
---|---|---|---|
1 | onvsyncevent now:%" priu64 " | 当前时间戳–纳秒级 | 收到vsync信号,渲染流程开始 |
2 | flushvsync | 刷新视图同步事件,包括记录帧信息、刷新任务、绘制渲染上下文、处理用户输入 | |
3 | uitaskscheduler::flushtask | 刷新ui界面,包括布局、渲染和动画等 | |
4 | flushmessages | 发送消息通知图形侧进行渲染 | |
5 | flushlayouttask | 执行布局任务 | |
6 | flushrendertask %zu | 当前页面上的需要渲染的节点的数量 | 总渲染任务执行 |
7 | layout | 节点布局 | |
8 | framenode::rendertask | 单个渲染任务执行 | |
9 | listlayoutalgorithm::measurelistitem:%d | 当前列表项索引 | 计算列表项的布局尺寸 |
图形图像子系统中的render service,是负责界面内容绘制的部件,处理由各个应用提交的统一渲染任务,将不同应用渲染的图层进行合并、送显。在收到每个vsync周期信号时,首先处理应用提交的指令,包括应用渲染树节点的新增、删除、修改,然后进行动画计算和遮挡计算,以上是为了对统一渲染树进行更新。接下来开始对渲染树执行绘制,首先预处理每个节点,计算绝对位置和脏区信息,然后针对脏区进行绘制,优先使用硬件合成器进行绘制,当遇到无法合成绘制的,交由gpu执行重绘,绘制的所有结果都将存入屏幕缓冲区,最后将绘制结果提交送显、上屏展示。
当vsync信号刷新时,如图3所示。
图3 rs侧渲染trace泳道图
序号 | trace | 描述 |
---|---|---|
1 | rsmainthread::docomposition | 合成渲染树上各节点图层 |
2 | rsmainthread::processcommand | 处理client端指令 |
3 | animate | 动画处理 |
4 | rsmainthread::calcocclusion | 遮挡计算 |
5 | processdisplayrendernode[x] | 单个显示器画面的绘制流程 |
6 | processsurfacenode:x | 单个节点的合成器处理 |
7 | repaint | 硬件合成器合成绘制 |
8 | redraw | 无法进行合成,则执行重绘 |
9 | renderframe | gpu执行绘制 |
10 | swapbuffers | 刷新屏幕缓冲区 |
11 | commit | 绘制结果提交上屏 |
懒加载
懒加载使用lazyforeach实现,lazyforeach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当lazyforeach在滚动容器中使用时,框架会根据滚动容器可视区域按需创建组件。当组件滑出可视区域外时,框架会进行组件销毁以降低内存占用。图4抓取的是懒加载过程中一帧的trace。
图4 懒加载trace泳道图
序号 | trace | 参数说明 | 描述 |
---|---|---|---|
1 | onidle, targettime:%" prid64 " | 时间戳,在这个时间之前完成该任务 | idle事件循环中检查是否有新的事件需要处理,如果有,则将任务调度器加入ui线程中并执行预测任务 |
2 | expiringitem_ count:[%zu] | 懒加载item的个数 | 预构建,包含处理所有懒加载项 |
3 | list predict | 添加预测布局任务 | |
4 | builder:buildlazyitem [%d] | 需创建的项目索引 | 在需要时创建项,并进行缓存 |
5 | layout[%s][self:%d][parent:%d] | tag标签,当前节点在uinode树中的索引,父节点在uinode树中的索引 | 当前帧节点布局 |
6 | build[%s][self:%d][parent:%d] | tag标签,当前节点在uinode树中的索引,父节点在uinode树中的索引 | 当前帧节点构建 |
7 | customnode:buildrecycle %s | js视图名称 | 触发复用渲染 |
8 | executejs | 执行js代码 |
页面加载
当触发页面加载时,openharmony会创建一个新的页面实例,然后按照特定的程序调用页面的生命周期方法。在生命周期方法中加载页面的布局,然后将数据绑定到页面上的视图元素,使页面能够显示和更新数据。图5抓取的是页面加载中一帧的trace。
图5 页面加载帧trace泳道图
序号 | trace | 参数说明 | 描述 |
---|---|---|---|
1 | pageroutermanager::runpage | 页面路由预处理及加载页面 | |
2 | pageroutermanager::loadpage | 加载页面并路由 | |
3 | jsideclarativeengine::loadpagesource | 加载一个javascript文件并将其解析为abc字节码 | |
4 | jsideclarativeengine::loadjswithmodule execute page code : %s | 页面url地址 | 执行页面代码 |
5 | build[%s][self:%d][parent:%d] | tag标签,当前节点在uinode树中的索引,父节点在uinode树中的索引 | 当前帧节点构建 |
6 | customnode:builditem %s | js视图名称 | 渲染子节点然后将其挂载到父节点上 |
7 | viewchangecallback(%d, %d) | 视图宽,视图高 | 视图变化回调 |
trace实践
以下示例采用lazyforeach
的方式遍历列表,并借助smartperf-host调试工具追踪代码执行流程。
在代码示例中,使用一个list容器组件,通过懒加载方式来创建出120个iconview自定义组件。在iconview组件中,使用了flex容器组件包含image和text子组件,形成了图文混合列表。
// src/main/ets/pages/lazyforeachpage.ets
@entry
@component
struct lazyforeachpage {
private iconitemsourcelist = new listdata();
abouttoappear() {
// 添加120个iconitem的数据
......
}
build() {
column() {
text('懒加载示例')
.fontsize(24)
.fontcolor(color.black)
.fontweight(fontweight.bold)
.textalign(textalign.start)
.width('90%')
.height(50)
list({ space: 20 }) {
lazyforeach(this.iconitemsourcelist, (item: iconitemmodel) => {
listitem() {
iconitem({ image: item.image, text: item.text })
}
}, (item: iconitemmodel, index) => index.tostring())
}
.divider({ strokewidth: 2, startmargin: 20, endmargin: 20 }) // 每行之间的分界线
.width('100%')
.height('100%')
.layoutweight(1)
}
.width('100%')
.height('100%')
.alignitems(horizontalalign.center)
}
}
// src/main/ets/view/iconview.ets
@component
export struct iconitem {
image: string | resource = '';
text: string | resource = '';
build() {
flex({ direction: flexdirection.row, justifycontent: flexalign.center, aligncontent: flexalign.center }) {
image(this.image)
.height(40)
.width(40)
.objectfit(imagefit.contain)
.margin({
left: 15
})
text(this.text)
.fontsize(20)
.fontcolor(color.black)
.width(100)
.height(50)
.textalign(textalign.center)
}
.width('100%')
.height(50)
}
}
下面使用smartperf-host调试工具抓取htrace文件,并生成一个跟踪泳道分析图,来了解示例代码的加载流程。跟踪泳道分析图被分为五个部分,每个部分都标注数字并框选出相应的标签,从而使得整体的过程能够得到更好的理解。
图6 lazyforeach遍历的列表的泳道分析图
接下来,逐一解析这五个模块的详情:
1.加载并路由lazyforeach页面
图7 加载并路由lazyforeach页面泳道图
h:jsideclarativeengine::loadpagesource
加载一个 javascript 文件,并且解析为 abc 字节码;h:flushpipelinewithoutanimation
清理渲染管道的操作;h:customnode:onappear
用于构建当前 onappear 生命周期的操作,并执行abouttoappear生命周期函数;h:customnode:builditem lazyforeachpage
渲染子节点并挂载在 lazyforeachpage 页面上。
2.对当前帧节点stage,执行布局任务、执行渲染任务并通知图形侧进行渲染
图8 对当前帧节点stage,执行布局任务、执行渲染任务并通知图形侧进行渲染泳道图
h:layout[stage][self:1][parent:0]
对当前帧节点stage,执行布局任务;(stage作为框架,承载着页面page节点。因此,标签的呈现会从stage开始)h:measure[%s][self:17][parent:16]
对page、column、row、image、text等组件布局尺寸计算;h:builder:buildlazyitem [0]
和h:listlayoutalgorithm::measurelistitem:0
分别为创建一个lazyitem项目和计算列表项的布局尺寸;h:layout[%s][self:38][parent:37]
对page、column、row、image、text等组件执行布局任务;
h:framenode::rendertask
执行渲染任务;h:requestnextvsync
请求下一帧vsync信号。
3.对当前帧节点flex,执行布局任务、执行渲染任务并通知图形侧进行渲染
图9 对当前帧节点flex,执行布局任务、执行渲染任务并通知图形侧进行渲染泳道图
h:layout[flex][self:63][parent:62]
对当前帧节点flex,执行布局任务**;**h:measure[%s][self:17][parent:16]
对image、text等组件布局尺寸计算;
h:framenode::rendertask
flex渲染任务执行;h:requestnextvsync
请求下一帧vsync信号。
4.构建前预处理数据及添加预测布局任务
图10 构建前预处理数据及添加预测布局任务泳道图
h:builder:buildlazyitem [11]
构建前预处理数据了11条数据;h:layout[listitem][self:76][parent:-1]
添加一条flex、image、text的预测布局;h:flushmessages
发送消息通知图形侧进行渲染。
5.合成渲染树上各节点图层任务
图11 合成渲染树上各节点图层任务泳道图
h:acquirebuffer
、h:processsurfacenode:entryview xywh[0 0 720 1280]
获取屏幕缓冲区并绘制entryview、systemui_statusbar、systemui_navigationbar等;h:repaint
硬件合成器合成绘制当前节点树。
自定义trace
开发者可以根据业务需求,使用hitracemeter进行自定义trace打点跟踪,目前支持arkts和native,具体使用细节可参考下方链接:
添加自定义trace后,可在smartperf-host调试工具上查看,自定义trace将以独立泳道的形式呈现在对应打点的进程下。
下图两条泳道使用了starttrace和finishtrace方法,表示程序运行过程中,指定标签从调用starttrace到调用finishtrace的耗时统计。图中记录了custom_trace_tag_1和custom_trace_tag_2两个标签,先后呈现了2个标签的耗时统计。
图12 自定义trace示例
下图两条泳道使用了tracebyvalue方法,表示程序运行过程中,指定trace在对应时间段内的状态值,状态值含义可按需传参,开发者可以通过鼠标放置在对应数据块上,来查看具体的状态值。图中记录了custom_trace_tag_2标签在红色方框标识的时间段内,打点状态值为2001。
图13 自定义状态值示例
性能打点原理
trace的生成依赖了dfx子系统中的hitrace组件,其中包含的hitracemeter模块为开发者提供系统性能打点接口,具体细节可参考下方链接:
hitracemeter拥有两套开始和结束打点接口,实现对逻辑行为的耗时统计。由于耗时统计大多数以方法为单位,所以hitracemeter也提供了快速打点单个方法执行耗时的宏定义hitrace_meter、hitrace_meter_name、hitrace_meter_fmt,使用它们,只需要在方法起始位置调用即可。这些宏定义依赖了方法内局部变量的生命周期,其原理是在方法开始时构造了一个打点实例,在实例构造函数中调用开始打点接口,当方法执行完毕,打点实例随着方法结束而执行析构,在实例析构函数中调用结束打点接口。
app中的打点示例
arkui框架子系统应用hitracemeter的例子,来源于arkui开发框架源码。
以下代码对hitracemeter进行接口封装,其原理与hitrace_meter等相同,依赖方法内局部变量的生命周期实现快速打点。
// frameworks/base/log/ace_trace.h
#define ace_scoped_trace(fmt, ...) acescopedtrace acescopedtrace(fmt, ##__va_args__)
#define ace_function_trace() ace_scoped_trace(__func__)
class ace_force_export acescopedtrace final {
public:
explicit acescopedtrace(const char* format, ...) __attribute__((__format__(printf, 2, 3)));
~acescopedtrace();
ace_disallow_copy_and_move(acescopedtrace);
private:
bool traceenabled_ { false };
};
以下代码是刷新视图同步事件,包括记录帧信息、刷新任务、绘制渲染上下文、处理用户输入。在方法开头调用宏定义ace_function_trace,将函数名flushvsync作为trace名称记录下来,并记录函数开始时间,在函数结束时记录函数结束时间,得出执行耗时。
// frameworks/core/pipeline/pipeline_context.cpp
void pipelinecontext::flushvsync(uint64_t nanotimestamp, uint32_t framecount)
{
ace_function_trace();
// 此处省略方法内的其他业务逻辑
// ...
}
rs中的打点示例
图形子系统应用hitracemeter的例子,来源于图形子系统源码。
以下代码对hitracemeter进行接口封装。
// utils/log/rs_trace.h
#include "hitrace_meter.h"
#define rosen_trace_begin(tag, name) starttrace(tag, name)
#define rs_trace_begin(name) rosen_trace_begin(hitrace_tag_graphic_agp, name)
#define rosen_trace_end(tag) finishtrace(tag)
#define rs_trace_end() rosen_trace_end(hitrace_tag_graphic_agp)
#define rs_trace_name(name) hitrace_meter_name(hitrace_tag_graphic_agp, name)
#define rs_trace_name_fmt(fmt, ...) hitrace_meter_fmt(hitrace_tag_graphic_agp, fmt, ##__va_args__)
#define rs_async_trace_begin(name, value) startasynctrace(hitrace_tag_graphic_agp, name, value)
#define rs_async_trace_end(name, value) finishasynctrace(hitrace_tag_graphic_agp, name, value)
#define rs_trace_int(name, value) counttrace(hitrace_tag_graphic_agp, name, value)
#define rs_trace_func() rs_trace_name(__func__)
以下代码在显示器画面绘制方法。在方法开头调用宏定义rs_trace_name,将函数名processdisplayrendernode与对应的显示器id组合后,作为trace名称记录下来,同时由于其本质是使用了快速打点单个方法的宏定义hitrace_meter_name,于是只需要调用一次,即可收集到processdisplayrendernode函数的执行起终点时间,得出执行耗时。
// rosen/modules/render_service/core/pipeline/rs_surface_capture_task.cpp
void rssurfacecapturevisitor::processdisplayrendernode(rsdisplayrendernode &node)
{
rs_trace_name("rssurfacecapturevisitor::processdisplayrendernode:" +
std::to_string(node.getid()));
// 此处省略方法内的其他业务逻辑
// ...
}
为了能让大家更好的学习鸿蒙(harmonyos next)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/fv7h05
《鸿蒙开发学习手册》:
如何快速入门:https://qr21.cn/fv7h05
- 基本概念
- 构建第一个arkts应用
- ……
开发基础知识:https://qr21.cn/fv7h05
- 应用基础知识
- 配置文件
- 应用数据管理
- 应用安全管理
- 应用隐私保护
- 三方应用调用管控机制
- 资源分类与访问
- 学习arkts语言
- ……
基于arkts 开发:https://qr21.cn/fv7h05
- ability开发
- ui开发
- 公共事件与通知
- 窗口管理
- 媒体
- 安全
- 网络与链接
- 电话服务
- 数据管理
- 后台任务(background task)管理
- 设备管理
- 设备使用信息统计
- dfx
- 国际化开发
- 折叠屏系列
- ……
鸿蒙开发面试真题(含参考答案):https://qr18.cn/f781ph
鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/f781ph
1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向
发表评论