iOS调试和性能优化技巧

WWDC2019-Session-417专门讨论如何利用Metrics来进行性能监控和优化,对于我们开发者显然是一个重大利好。Metrics涵盖了整个App开发的完整流程:

  • XCTest Metrics (开发和测试阶段)
  • MetricsKit (内测阶段和线上阶段)
  • Xcode Metrics Organizer (线上阶段)

对于性能监控和优化这个主题上今年推出的MetricKit聚焦于两个主题:

  • Battery
  • Performance

设备耗电情况体现:

  • CPU Processing(CPU运算)
  • Location(定位)
  • Display(显示/屏幕亮度)
  • Networking(网络服务)
  • Accessories(外围设备如蓝牙)
  • Multimedia(多媒体)
  • Camera(相机)

性能指标体现:

  • Hangs(主线程卡顿)
  • Disk(设备IO)
  • Application Launch(应用启动耗时)
  • Memory(内存表现)
  • Custom Intervals(其他)

在使用Xcode -> Debug- > Simulate Metrics Payloads的功能后将会触发一份设备Metric数据到- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads监听方法里面。

在学习到利用MXSignpostMetric标记业务流程时候发现我们完全可以通过埋点的方式关注到每个业务流的性能情况。它提供的粒度已经相对较为细致了,更多具体的优化细节应该结合业务利用Xcode+Insturment去分析。

关于MetricKit和Metrics的话题到这里就暂时告一段落,接下来主要是讲一讲在学习到利用埋点标记业务流程后带来的一些调试思路启发。

基本场景概况

性能优化应该是要结合到具体业务具体场景下去讨论才是有意义的。接下来让我们从两个方面(耗电量性能表现)来分析一个相机滤镜渲染业务的瓶颈/优化点

  • 1920x1080后置摄像头分辨率
  • 依次叠加饱和度/高斯模糊/亮度滤镜到这个链路上

首先我们切到Energy Impact模块中观察耗电量的基本情况。发现其中GPU单元的耗电量影响占93.9%,而CPU单元仅占6.1%。可以预计在这个场景中设备发热和性能损耗将主要是由GPU单元运算引起的,所以之后的性能分析重心应该放在调用GPU相关的代码上。

接下来我们再看一下FPS模块中的GPU单元运行的基本情况,我们已知iOS设备的屏幕刷新率是60fps即理论上GPU运算单元损耗要控制在1 / 60 = 16.75 ms才能够达到这个帧率,而下图中FrameTime中GPU运算花去了22.9ms即意味着这个场景上最多只能达到43fps左右的帧率。

显然这个相机滤镜链业务里面,滤镜脚本所实现的性能表现差强人意。那么要如何确认具体是哪一个滤镜导致的性能瓶颈问题呢?

分离业务边界

Logging这个主题中Debugging CPU Performance部分发现我们可以通过OSSignPost给业务埋点标记出业务的运行流程。它的基本用法也十分的简单:

1
2
3
4
5
6
7
8
9
# OC版
os_log_t log = os_log_create("Subsystem", "Category");// 全局变量

os_signpost_id_t postId = os_signpost_id_generate(log);
os_signpost_interval_begin(log, postId, "Event", "MoreInfoTag");

dosomething();

os_signpost_interval_end(log, postId, "Event", "MoreInfoTag");

在这个示例中,我所实现的业务流:相机画面输出->饱和度滤镜->高斯模糊滤镜->亮度滤镜->提交视图渲染,接下去通过在三个滤镜渲染前后埋点标记出渲染业务CPU部分的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
# Swift版
let filterChainProcessBlock: MetalImageFilterBlock = {(beforeProcess, textureReousrece, filter) in
struct StaticVar { static var postId = OSSignpostID(log: .metalImage) }
if beforeProcess {
StaticVar.postId = OSSignpostID(log: .metalImage)
os_signpost(.begin, log: .metalImage, name: "Filter", signpostID: StaticVar.postId, "b_%s", NSStringFromClass(filter.classForCoder))
return;
}
os_signpost(.end, log: .metalImage, name: "Filter", signpostID: StaticVar.postId, "e_%s", NSStringFromClass(filter.classForCoder))
}
saturationFilter.chainProcessHandle = filterChainProcessBlock
gaussianFilter.chainProcessHandle = filterChainProcessBlock
luminanceFilter.chainProcessHandle = filterChainProcessBlock

接下来结合Instrument+Xcode来分析这段业务代码里面的性能表现情况。

具体问题具体分析

在Apple平台上做性能调试是一件相对简单的事情,这里将结合Xcode+Instrument进行调试分析。

  • CPU调试分析:Instrument的Time Profileros_signpost两个模板。
  • GPU调试分析:Xcode的Frame Capture以及Instrument的Metal System Trace模板。

CPU调试分析

整体上可以看到这个场景中实际损耗的CPU并不严重,90s的运行时间里面平均只损耗了12%左右的性能。

而各个部分的损耗占比:

  • 滤镜渲染流程约84%
  • 渲染到视图流程约9.6%
  • 纹理缓存回收与获取调度约5.5%

接下来我们放大到一个滤镜渲染流程业务标记区间,可以看到大部分运行时间都集中在MetalImageGaussianFilter这段业务逻辑上,也就是接下去若有需要优化CPU性能应该深入研究这段业务的代码是否有优化空间。

GPU调试分析

利用Instrument的Metal System Trace模块可以观察到这个业务场景中滤镜的工作流程,显然和我们之前的推测是一致的。MetalImageGaussianFilter这个滤镜的Fragment脚本实现上是十分消耗性能的。

在Xcode的Frame Capture界面可以在更细致的粒度上调试Shader,更多具体的调试细节这里就不介绍了(Apple官方文档关于这一块的描述实际挺全面的)。

这里实现的是一个高斯模糊的效果,在这个Fragment脚本中我所用到的方式是在水平和垂直方向先后进行了高斯模糊算法的运算,而关于模糊效果的算法优化可以参考快速高斯模糊Shader实现

参考

iOS耗电量和性能优化的全新框架
iOS 最全面的功耗分析之——Power Log
Improving Your App’s Performance
WWDC-417
Developing a Great Profiling Experience
Instruments官方文档