Instruments 自 2010 年发布之后,一直不温不火,去年也没有任何更新值得去关注。但在一年的沉淀后,Instruments 团队在今年终于有了个可以令人期待的发布。本文是针对 Session 410:Creating Custom Instruments 的解读。
Instruments 10
Instruments 是一款强大且灵活的性能分析工具,集成在 Xcode 的开发者工具集中。我们能够用不同的 Instrument 来分析测试各种各样的性能问题,比如 Leaks 来查内存泄漏问题,Time Profiler 来分析 App 的页面卡顿问题等等。那么今年苹果对 Instruments 做了哪些更新呢?从官方的 Xcode 10 Release Notes 里我们可以看到有这几点:
- 开发者能够根据不同需求,灵活快速地基于 os_signpost 创建和发布属于自己的 Instrument;
- Instruments 能够自动展示你代码里通过 OSLog signposts 标记的数据;
- System Trace 里为线程分析新增了一个图像模式;
- 由于 Instrument 10 中所有的自定义工具都会基于 os_signpost 来完成,原本基于 dtrace 的自定义 Instrument 将不被继续支持。
其中最重要的一点便是自定义自己的 Instrument 了,虽然以往的版本中也可以根据自己的需要去创建,但步骤麻烦且图形化支持简陋,并且因为数据采集是基于 DTrace 的,导致真机上并不能够使用,苹果自身也并不鼓励大家去做这个自定义。但 Instruments 10 针对这点这次可谓下了苦功——全新基于 os signpost 的架构能够支持所有平台,『标准界面(Standard UI)』 和 『分析核心(Analysis Core)』 使得自定义 Instruments 变得更加灵活而且便利。这个 Session 围绕如何创建一个自定义的组件,分为以下四个大部分展开:
- 为什么要创建自定义的工具
- Instruments 10 的架构体系
- Instruments 自定义工具的初级、中级以及高级应用
- 自定义工具的最佳实践
其中将会涉及到一些关于专家系统(Expert System)和 CLIPS 语言的知识,如果有兴趣的话,可以先了解下。本文相对于 App 端开发者,可能会更适合一些测试人员,尤其是对性能测试方面比较感兴趣的同学。
为什么要创建自定义的工具
我们都知道 Instruments 中已经内置很多方便好用的工具了,这些工具苹果已经通过文档模板创建好并集成在 Instruments 的启动选择界面里。比如上文中提到的 Leaks 和 Time Profiler 相信很多人都有使用过,下图的就是 Instruments 10 的启动界面,与以前版本差别不大。
Instruments 10 的架构体系
架构的演变
介绍 Instrument 10 的架构之前,我们先回顾下以往苹果是怎么维护 Instruments 组件的。
界面介绍
Instruments 自定义工具的初级、中级以及高级应用
初级应用
有了足够的理论知识后,我们可以尝试下简单的自定义工具创建了。
.instrpkg
的 XML 文件,所有的配置都会在这个 XML 文件中完成,其中默认已经帮我生成了包含一些基础信息。接着我们要做的就是按照我们的需要去写这个配置文档了。
1、导入需要使用的 Schema
<!-- MARK: 导入你需要使用的 schema -->
<import-schema>tick</import-schema>
2、完成 Instrument 的『标准界面』和『分析核心』配置
<!-- MARK: 导入你需要使用的 schema -->
<import-schema>tick</import-schema>
<instrument>
<!-- MARK: 这个 Instrument 的基本信息 -->
<id>com.Parsifal.TicksDemo</id>
<title>Ticks</title>
<category>Behavior</category>
<purpose>Instrument drawing ticks every 100ms</purpose>
<icon>Generic</icon>
<!-- MARK: 描述的表数据,将会由 分析核心 最终完成存储和解析提供给 标准界面 模块 -->
<create-table>
<id>tick-table</id>
<!-- 定义了每列的数据 -->
<schema-ref>tick</schema-ref>
</create-table>
<!-- MARK: 图形视图上面展示(可选) -->
<graph>
<title>Ticks</title>
<lane>
<title>Lane</title>
<!-- 这里就是上面你定义的 table id-->
<table-ref>tick-table</table-ref>
<plot>
<value-from>time</value-from>
</plot>
</lane>
</graph>
<!-- MARK: 这里描述你需要展示在详情视图的数据 -->
<list>
<title>Ticks</title>
<!-- 这里就是上面你定义的 table id-->
<table-ref>tick-table</table-ref>
<column>time</column>
</list>
</instrument>
至此我们便已经完成了所有的编码工作了,在编写过程中,我们还会发现苹果为我们准备了很多代码片段,来帮助完成这些配置,并且编译期间还会对代码进行检查,报出的错误信息也很方便我们对其进行调试。编译运行后,在弹出的 Instruments 选择窗口里选择 Blank 就可以在测试界面的 Library 中发现我们自己定义的工具了,直接拖入 Instruments 里就能够像使用其他内置工具一样运行。
中级应用
这一部分我们会介绍一些『标准界面』和『分析中心』里更详细的内容。
标准界面
『标准界面』模块里为我们提供了很多简单又好用的元素,使用这些元素能够让我们创建出非常酷炫又实用的 Instruments 工具。接下来列举一些常用的元素进行介绍,更多的元素还待苹果正式发布 Instruments 10 后大家一起探索。
图形通道面板
- <plot>:为图形视图上面划出一个单独的数据通道,需要提供一个值来定位纵列,如我们 Ticks 例子中的 time;
- <plot-template>:这个和 plot 差不多,区别在于它会自动为每个 instance-by 值创建一个数据通道;
- <histogram>:为给定的时间片段生成柱状图,如 System Trace 组件中使用的那样;
详情面板
- <list>:创建一个列表,常见的各种内置工具都会有的;
- <aggregation>:创建一个总计视图,统计总和和平均数等,使用这个元素的时候,纵列就是各种函数了,如
sum
、average
和count
等,另外这个元素还有个hierarchy
属性,能够为不同的纵列设置外轮廓,非常适合于大量数据的展示; - <calltree>:这个就顾名思义了,调用栈的视图;
- <narrative>:展示一个描述工程类型(engineering type)的视图;
分析核心
这一部分我们首先会重点谈谈『分析核心』是如何收集数据和处理数据的,这一过程主要包含以下三个步骤。然后会介绍一些『分析核心』中的相关概念以助于我们在配置表中使用它们。
1、简化
2、搜索
接着每个 store 将会开始尝试寻找数据的提供者。
3、优化
当我们从各个 store 中获取到数据源后,在『分析核心』中就开始了一项称为『Binding Solution』的工作,第三步就是优化这个工作流。
一些重要的概念:
Binding Solution:Instruments 里是通过 Thread Narrative 实现的,它有以下两个有点
- trace-wide;
- Instruments 会在我们将工具拖入测试界面的时候,开始计算寻找最佳可能的记录方案来最小化在目标上的影响;
Schemas:我们创建表的时候就必须要指定一个 Schema,比如我们第一个 Demo 中的 tick
- 目前 Instruments 里已经定义有超过 100 个 Schema 供我们导入使用了;
- 包含在 Instruments 包中;
- 可以通过编译设置链接其他的 Instruments 包,在编译期会进行类型校验;
- 提供了不同的构建模块;
Modelers:前面提到过,Modeler 可以帮助我们合成不同的数据,关于它的有以下几个常用的元素可在 XML 配置文件中使用。
- <modeler>:创建一个 Modeler,帮助我们做数据类型转换的工作;
- <point-schema>:定义一个可以用来存储点(无时间片段,即某个时间点的数据)的 schema;
- <interval-schema>:定义一个可以用来存储定距数据(有时间片段,一段时间内的数据)的 schema;
PS:Modelers 其实是一个由 CLIPS 编写,非常强大而且高级的小型专家系统(Expert System)。它可以指定自己需要哪些输入信号来告知 Binding Solution 怎么完成剩下数据图形的填充工作。关于这一部分我们将会在“高级应用”里详细说明。
最后,具备定义一个 schema 的能力是很重要的。今年新发布的 OS signpost API 赋予了我们一个很棒的把数据导入到 Instruments 中的方式,而且苹果为我们创建了一些快捷方式来使用它,比如在 XML 配置表中敲下 <os-signpost-interval-schema> 元素,就会自动生成如下代码片段。
<os-signpost-interval-schema>:定义了一个存储定距数据的 schema,而且数据由 os_signpost API 提供。这就意味着我们可以代码的任意地方使用 os_signpost API 来将我们需要被测试的数据直接导入到 Instruments 中。在创建这个元素的时候, Xcode 会自动生成相关的代码片段以帮助我们来完成 Modeler 的创建。 这个 API 和 os_signpost 结合使用起来就如下:
//使用这个 os_signpost 可以再我们代码的任意地方将你的数据传递给 Instruments,但必须记得 begin 和 end 成对使用
os_signpost(.begin, log: parsingLog, name: "Parsing", "Parsing started SIZE:%ld", data.count)
// Decode the JSON we just downloaded
let result = try jsonDecoder.decode(Trail.self, from: data)
os_signpost(.end, log: parsingLog, name: "Parsing", "Parsing finished")
<!-- MARK: 使用这个元素就能创建从 os_signpost 获取数据的 schema -->
<os-signpost-interval-schema>
<id>json-parse</id>
<title>Image Download</title>
<subsystem>"com.apple.trailblazer"</subsystem>
<category>"Networking"</category>
<name>”Parsing"</name>
<start-pattern>
<!-- MARK: 这里根据我们设置好的条件输出信息 -->
<message>"Parsing started SIZE:" ?data-size</message>
</start-pattern>
<column>
<!-- MARK: Engineering Type 对应的助记词 -->
<mnemonic>data-size</mnemonic>
<title>JSON Data Size</title>
<!-- MARK: 填写的是 Engineering Type 里定义的数据类型,比如这边看的是内存相关的,就会有 size-in-bytes -->
<type>size-in-bytes</type>
<!-- MARK: 由 CLIPS 语言编写的表达式作为这个列的值 -->
<expression>?data-size</expression>
</column>
</os-signpost-interval-schema>
在中级这一部分,苹果还为我们演示了一个 os_signpost API 使用示例。该示例中对一个展示图片的列表页做了图片测试,监控了每一个 cell 图片的下载情况。由于涉及到的知识点上面都已经描述过了,具体示例的完成过程这边就不再赘述,相信通过视频大家能看得更直观。其中演示过程中有提到几点比较值得注意的,这里重点抽出来说明下。
高级应用
这一部分将会着重介绍我们怎么去创建和定义 Modelers,并且简单介绍下怎么用 CLIPS 搭建基本的专家系统。
探秘 Modeler 的内部世界
我们通过一个简单的例子来探秘 Modeler 世界。我们模拟这样一种情况,我的代码里有部分危险的操作容易触发程序问题,我们的目标就是在程序出现问题的时候,找到是哪个操作导致的。那么我们可以定义下图中的三个 schema,前两个作为输入项,最后一个是输出项。
将这个流程,我们已一个更加形象直观的时序图来展示,其中虚线代表着 Modeler 自己的时钟:
其中,图上我按照时间顺序,标注除了 4 个值得注意的节点:
(1)这时的 App 出于正常运行状态,没有任何数据传输给 Modeler 的工作内存中,Modeler 的时钟并没有开始走;
(2)此时虚线到达我们的第一个 input schema 触发的节点(开始有危险操作的节点),在这里 Modeler 的工作内存中正式开始接收到数据,Modeler 的时钟从这个点开始计时。
(3)这是第二个 input schema 的触发节点(我们 App 出现问题的节点)。这里值得一提的是,Modeler 是很机智的,它有自己的逻辑,它能区分出在这个时间节点之前的危险操作数据意义不大,而这个节点开始到 app-on-fire 这个节点结束前的数据才是我们所需要的。
(4)到这最后一个节点,所有的输入数据都已经传输完毕了,Modeler 的时钟与这些输入数据没有交集, 它推断出这些输入数据已经不再被需要了,因而把它们从工作内存里移除并且产生最终的输出数据。
可以回顾整个过程,可以总结出两点:
- Modeler 的时钟起始时间总是第一个输入数据触发的时间
- 只有与 Modeler 当前时钟有交集的输入数据才会被 Modeler 保留在工作内存
这样一种机制能够帮助我们更清晰地定位到问题的所在,而不被那些无意义的旧数据干扰。这样一种机制是怎么实现的呢?答案就是我们接下来要说明的 『生产系统(Production System)』。
生产系统
Modeler 中对『工作内存(Working Memory)』的逻辑支持是来自我们定义的 『生产系统』。『生产系统』可以由一系列的 『规则(Rules)』 来生成,它在『工作内存』中为 facts(CLIPS 语言中的专业术语,可以理解为字面意思事实,推理得到的事实) 服务。『规则』由三部分组成—— LHS => RHS
,左边部分 + 操作符(=>)+ 右边部分。左边部分是一个在『工作内存』中激活规则的条件,右边部分则是激活后要执行的行为。右边的行为包含为输出的数据表创建新的一行,也可以是在建模时推断出一个新的『fact』到『工作内存(Working Memory)』里。结合之前上面的时序图,我们可以想到『facts』 来源于两个地方。一个是我们看到的数据输入表,这些是根据『规则』自动推理出的。另一个可以是来自于右边部分的行为,这些是通过 CLIPS 中 assert
命令主动推理的。如果我们打算创建我们自己的『facts』,CLIPS 提供了 fact 模板,模板允许为『fact』提供数据结构和做一些基础类型的检查。接下来就是介绍怎么来定义规则了。
规则和 CLIPS
我们可以使用 CLIPS 语言定义一些规则。先回顾下上面那个例子的时序我们是怎么通过 CLIPS 设置规则实现的:
(defrule MODELER::found-cause//规则名
//LHS,左边部分指定规则激活的条件
(playing-with-matches (start-time ?t1) (who ?object))
(app-on-fire (start-time ?t2))
(test (< ?t1 ?t2))
=>
//RHS,右边部分为推理产生一个 fact
(assert (cause-of-fire (who ?object)))
)
(defrule RECORDER::record-cause//规则名
//LHS,左边部分未设定激活的条件
(app-on-fire (start-time ?start))
(cause-of-fire (who ?object))
(table (table-id ?t) (side append))
(table-attribute (table-id ?t) (has schema started-a-fire))
=>
//RHS,右边部分为产生输出数据
(create-row ?t)
(set-column time ?start)
(set-column who ?object)
)
其中,record-cause
这条规则定义了,如果满足以下三个条件,则这次生产会推理出一个 『fact』并被压入『工作内存(Working Memory)』里。
- 一个对象 t1 这个时间在
playing-with-matches
中产生; app-on-fire
在 t2 这个时间被触发了;- t1 的发生时间早于 t2;
而 record-cause
这条规则定义了,如果满足了以下四个条件:
- App 在一些起始时间“着火”;
- 知道“着火”的原因和相关的对象(规则 1 里可以拿到);
- 有一张绑定了 Modeler 输出侧的数据表;
- 这张数据表关联着我们之前定义的
start-a-fire
schema;
则在输出数据表中创建一行数据,并且设置时间和设置『左边部分』捕获到的中引起“着火”的值。
通过以上两个简单的规则,我们便基本上创建了最早的『专家系统(Expert System)』。使用定义好的两条规则,就可以用来寻找我们 App 内存在的一些问题。或许你也已经注意到,这些规则要么是为 Modeler 预先设置的,要么是为 Recorder。CLIPS 里把他们叫做『模块』,并且支持把规则分组和控制规则的执行顺序。比如说,如果你所有的规则都定义在了记录模块里生产输出数据表,那么你将不会在 Modeler 模块推断的时候写入任何的输出数据。因为在 Modeler 模块 里的规则必须在记录模块里的规则之前执行。
逻辑支持
在前面探秘 Modeler 内部世界的时候,我们提到过逻辑支持。逻辑支持一般与纯推理规则关联在一起。比如说,如果 a 和 b,那么 c。通过我们的『生产系统』中说,就是如果 a 和 b 不存于『工作内存』了,那么 c 就要被自动地被回收。这样我们就可以说 c 是被 a 和 b 在逻辑上支持了。这样的一种能力对于『专家系统』将『工作内存』维持在较低的水平很重要,因为它能很好地控制资源开销。同时,把无效的 facts 从『工作内存』里及时移除也是很重要的。如果 a 和 b 失效了,那么 c 也应该被移除。这样的需求在 CLIPS 里实现会很简单,只要通过 logical
命令就可以,如下面的代码。
(defrule MODELER::found-cause
//通过 logical 命令实现逻辑支持
(logical (playing-with-matches (start-time ?t1) (who ?object))
(app-on-fire (start-time ?t2))
)
(test (< ?t1 ?t2))
=>
(assert (cause-of-fire (who ?object)))
)
最佳实践
前面洋洋洒洒说了那么多,最后这一部分我们主要谈一下在开发自定义 Instruments 工具时有哪些最佳实践。
多写一个 Instrument
这句话不是建议我们去不断练习写自定义的 Instrument 工具,它说的是我们应该把 Instrument 功能做得细粒度化。举个例子,我们已经有了一个自定义的 Instrument 工具,但这个工具的功能并不能满足现在的需求,我们需要在它的基础上去增删一些详情或者图形的数据展示。这样一种场景,我们会有两个方式去做,第一个在原有的基础上继续迭代,第二个是重新再写一个满足我们当前需求的 Instrument。如果选择了第一种方案,这会导致这个 Instrument 组件变得不再纯粹,虽然功能是更多了,但相应的 Instruments 也变得更加复杂。所以苹果会更推荐我们用第二种方案,重新写一个符合我们当前需求的 Instrument。如果我们需要组合使用不同的 Instrument,在 Instrument 组件库中拖拽对应的 Instrument 到我们的文档中即可。如果这类组合使用场景很多,我们也可以用 “File -> Save As Template” 保存为模板以供接下来使用。保存模板的将会展示在我们 Instruments 的启动页面,比如内置的 Leaks、Activity Monitor 和 Time Profiler 等一样。另外这些模板,也能够很方便的在我们的包中复用,使用 <template> 元素即可。
实时模式很困难
实时模式指的是数据的获取、分析、到最终通过 Instruments 的界面能够实时地被展示出来。苹果现在很难完成这种实时交互主要有两个原因。第一个原因是,实时模式的完成需要一些额外的支持,但现在苹果并没有足够充裕的时间去做;第二个更重要的原因便是定距型数据(Interval Data,统计学上的概念,具有间距特征的数据,可作加减计算)。定距变量只有起始和结束这个阶段完成时,才能被加到数据表中和被『分析核心』所获取。在启动测试记录后,Instruments 将会收到一群定距型变量的开区间,当定距变量没完成闭区间时,Modeler 的时钟并不会往前走(Modeler 里的数据是按时间排序的)。这样的机制就会导致了一个问题,如果某个定距变量的开闭区间拉的很长,那么 Modeler 就会一直停滞在那儿等待。但如果这个时候用户点击了停止记录按钮,所有的开区间定距变量就会关闭,一切都会恢复正常,数据将会被填充到 Modeler 里。应该能感觉到,这是一个很不好的用户体验。一旦我们遇到这种情况时,我们有两个选择。第一个选择是配置我们的 Instrument 使它不支持这种实时模式,这个可以通过 <limitations> 元素实现。第二个选择避免数据出现这种长时间的开区间,例如使用 <os-signpost-interval-schema> 替代 <interval-schema>。
使用『最后 5 秒记录模式』
当创建一个用来测试含有大量输入数据的 Instrument 时,使用『最后 5 秒记录模式』则是我们目前最好的选择。这一选项我们可以在 “File -> Recording Options” 里找到。如下图:
总结
Instruments 10 提供了太多创建自定义 Instrument 的可能性了,不过这同样需要我们花点时间来学习掌握新一套的编写方式。对于大多数客户端开发者来说,或许并不会用到上面谈的这部分技能,但对于测试团队来说,这无疑为 iOS App 的性能测试又打开了一扇窗。相信在未来的一年里,圈子内会陆陆续续地有高质量自定义 Instrument 的产出,让我们一起期待。
查看更多 WWDC 18 相关文章请前往 老司机x知识小集xSwiftGG WWDC 18 专题目录