背景
近几年React Native技术栈的普及增速快,但其生态尚处于早期阶段,在架构、性能质量、周边设施上仍有很多优化、填充和演进的空间。尤其在性能监控评估方面以往针对原生、web的方式方法在React Native上不适用,目前Qunar大客户端机票团队已经规模化使用React Native,在收获跨平台交付效率、快速迭代热发布能力的同时我们希望能规范化应用的性能水平,具备性能自动化监控评估的APM设施。
解决什么问题
回到这个方案的初衷,是要解决这几个方面的问题:
性能应用水难以量化并导致调优工作滞后
客户端怎么样算是性能好性能坏,在通常是没有一个明确的界定方式的,事实上客户端的性能调优工作多数是被动滞后,开发阶段大家用几个手机点一点觉得流畅就是流畅,但其他真实用户或是对体验有较高要求的群体确实会有不一样的感受结论。
RD对性能认知不足
即便生态发展迅速,React Native确实是一项非官方生态的新技术,这个生态所积累的深度学习资源和最佳实践经验不足够丰富,部分开发者对其性能方面的认知还不足。
怎么办 -- 设计目标与实现拆解
针对开发和项目过程痛点,我们期望的目标如下:
- 项目性能评估客观可量化
- 自动化侦测性能缺陷
- 问题定位辅助决策
方案拆解思路:首先是找到针对React Native的性能相关性数据,也就是当性能出现下滑时,可以通过哪些维度的数据表现出来;然后有了数据样本需要进行记录;之后时对瞬时和一段周期内的样本进行自动化分析;最后时提供结果反馈。
解决方案
首先是性能相关性数据的实时采样模块,包括MRT(消息响应及时性)、GCP(绘图指令生效推迟)、逻辑同步帧率等。
其次是对样本的记录,记录模块目前支持两种记录模式,一种是存储在手机本地,一种是提供外放协议可以把数据投递到外部对接系统。
之后是实时分析,基于记录的各个维度数据进行缺陷侦测并生成预警。通过输出模块输出到开发者日志和可视化报表,这里我们后期有计划自动生成对应项目的性能bug对接到QA系统。
如何采集相关性数据、分析规则与调优策略
在行业缺乏相关方案的背景下,最难地方在于寻找React Native应用的性能相关性数据都是什么,在哪里,围绕RN的实现原理我们挖掘到了这些维度:
- MRT(消息相响应及时性)
- GCP(绘图指令延迟)
- 无SCU优化、冗余render调用侦测
- 绘图帧率与逻辑同步帧率
- 关键线程CPU负载
- 内存用量
- 流量消耗
下面着重讲解几个代表性的性能相关性数据自动化采集分析和对应的调优策略
MRT(消息响应及时性)
抛开具体实现,React Native中Native域与JS域互操作的最简化模型如下,两边各有一个基于消息队列模型的线程,js侧这个线程就是javascript代码执行的线程,native侧是native_module_thread用来执行js测投递过来的NativeModule调用。两侧的互操作是基于jni向对方投递消息,不同于大多数原生开发的单域仅是内存地址对应的代码块,因为涉及到线程切换、反射、任务队列、双向异步,这个过程并没有那么及时,尤其涉及到视图频繁变更、手势事件传递这些场景下相对原生方案延迟还是蛮大的。
MRT侦测、预警
我们的APM支持对这个指标进行实时采样,并针对异常峰值进行自动化预警反馈。这样RD在开发阶段可以及时感知到诸“如按钮点击了onPress没有调用“、“js函数执行了但没有在native上生效的“这些在过去比较隐蔽的性能缺陷。
MRT优化策略
- 减少native/js互操作频次,比如移到单域处理(比如Animation的useNativeDriver的设计符合这一原则)
- 减少native_module_thread任务堆积。比如提高算法效率、利用多核CPU设计并行计算算法、使用异步来优化调用等待链
- 减少js_thread上任务堆积。比如单线程模型上对大任务做拆解、优化调度顺序。
附React Native中Native与JS互操作原理图
侦测无SCU优化的组件和计算
SCU优化(shouldComponentUpdate optimization)是在reactjs/react native中经常强调的一个调优项。我们通过hook组件的componentDidUpdate对先后的props和state做deepdiff,根据结果分为深比较值不等、深比较仅函数不等、浅比较为开发者提供优化建议。在反馈报表中可以清晰感知到哪些组件发生了冗余的render以及优化收益预估。
防止冗余render调用调优策略
对于使用Redux管理数据事件流的项目
- store使用不可变数据结构
- 根据组件需要的最小化依赖按需定制mapStateToProps
- 使用PureComponent或redux connect、qrn-reduxPlugin赋予浅比较规则
- 避免单个render函数/stateless function生成深度和广度较大的JSX Tree
对于使用MobX的项目
MobX的自动化依赖收集机制在数据变更后会定向更新最小粒度的组件,且observer组件的SCU函数已经被替换无需考虑SCU,非observer组件使用PureComponent并遵循性能编程规范即可。
渲染帧率与逻辑帧率
样本采集
React Native的实际渲染执行和视图属性计算声明是彼此异步进行,以往针对原生应用的帧率监控指标在RN应用下会非常“好看”,几乎不可能出现ANR,极少会掉帧。但事实是js域的每次render执行并不是实时生效到实际界面上,js域上的视图数据计算和属性配置成为瓶颈。以安卓为例我们通过VSYNC特性在每两帧间检查是否发生JSInstance.onBatchCompleted视图操作事务提交以及是否期间完成了对BatchedOperation Queue的执行来判定native上每次渲染是否真的生效了来自js域提交的视图操作。
对应的针对渲染帧率与逻辑帧率的自动化分析规则
持续性弱体验侦测
- 渲染帧率或逻辑帧率连续n次连续掉帧
- 过去5s内渲染帧率或逻辑帧率的P80分位数是否>30fps
潜在缺陷侦测:
- 瞬时帧率异常侦测(渲染帧率和逻辑帧率)
执行过程限时超标侦测
通常在设计上我们会对一些模块的初始化、数据处理和计算的关键体验环节设定执行耗时的限制,通过构建AOP样式的decorator,对同步函数和基于Promise/async/await协程的异步函数进行trace并统计是否实际执行耗时不符合设计要求的环节。
@tracePerf({ expectedTimeCost: 200, strict: true })
async function invokeHeavyThingsXXX() {
const result = await hotdog.invoke(...);
await parallel.compute(result);
}
其他周边:CPU关键线程负载、内存用量、应用流量用量监测
关键线程(JS线程、native_module线程、UI线程)的持续性高负载侦测
内存用量:vm heap用量、native heap用量、低内存预警
流量:流量消耗飙升侦测
目前Profiler支持的自动化分析预警规则总结
可视化输出反馈
开发者可以实时观测性能评估过程的各项指标表现,并从中收集反馈信息和优化建议指引,将性能缺陷的挖掘和调优前置到开发和测试阶段。
适用范围、如何接入
适用于所有基于React Native及其衍生方案如QRN的应用
只需配置依赖,SDK化使用
对APM未来的思考
- 开放式架构:将流程、设计约定、基础功能和算法抽离为轻便的APM Core,把具体的sampler和profiler和output作为其上的插件实现进行剥离,各业务线团队和开发者根据自身需要定制、扩展性能维度和分析规则。
- 功能延展:扩展Output模块和应用范围,对性能评级标准化并对接QA流程。
- 自身性能优化:部分js代码移交到native去实现,降低APM本身对js域单线程的负载损耗。