在 CI 中使用 Benchmark 进行回归分析

咱们在 I/O 2019 发布了 Benchmark 库的第一个 alpha 版。以后为了能帮助您在优化代码时能够准确地评估性能,咱们就一直在改进 Benchmark 库。Jetpack Benchmark 是一个运行在 Android 设备上的标准 JUnit 插桩测试 (instrumentation tests),它使用 Benchmark 库提供的一套规则进行测量和报告:html

@get:Rule
val benchmarkRule = BenchmarkRule()

@UiThreadTest
@Test
fun simpleScroll() {
    benchmarkRule.measureRepeated {
        // Scroll RecyclerView by one item
        recyclerView.scrollBy(0, recyclerView.getLastChild().height)
    }
}

△ Github 上的 示例工程android

△ Android Studio 输出、运行多个基准测试的示例

△ Android Studio 输出、运行多个基准测试的示例git

Benchmark 库经过它本身的 JUnit Rule API 处理预热、检测配置问题以及评估代码性能。github

上面介绍的这些在咱们本身的工做环境下用起来很不错,可是不少基准测试数据其实来自于持续集成 (Continuous Integration, CI) 中对于回归模型的检测。那么咱们要如何处理 CI 中的基准数据呢?算法

基准测试 vs 正确性测试

一个工程里就算有数千个正确性测试,也能够轻易经过信息折叠显示在数据面板上。下面就是咱们在 Jetpack 中的测试信息:数据库

这里没有什么特别的内容,可是在减小视觉负荷方面使用了两个常见技巧。首先,这里以包和类的维度折叠了包含数千条测试信息的列表;而后,默认状况下隐藏告终果所有正确的包。就这样,数十个库里接近两万个测试结果,就被囊括到了寥寥几行文字之中。正确性测试的面板很好地控制了所展现的数据规模。缓存

可是基准测试又如何呢?基准测试不会简单地输出经过/不经过,每一个测试的结果都是一个标量,这意味着咱们无法简单地将经过的结果折叠起来。咱们能够看一看数据图表,也许能够对数据的模式有个直观的了解,毕竟一般状况下,基准测试的数量要远少于正确性测试...app

可是您却只能看到一大堆可见噪声。就算测试结果从数千减小到数百个,直接看图表对于数据的分析依然不会有任何帮助。基准测试中保持原有性能结果的数据与测试回归的数据所占据的可视区域相同,因此咱们须要把未出现测试回归的数据过滤掉 (这样测试回归的数据才能凸显出来)。ide

简单的回归检测方法

咱们能够从一些简单的事情开始,尝试回到只有经过和不经过的正确性测试。例如能够把两次运行的结果降低百分比超过某一阈值的状况定义为基准测试的失败结果。不过因为方差的缘由,这种方式并不能成功。函数

△ 视图填充的基准数据容易出现较大方差,可是仍然提供了有用的数据

△ 视图填充的基准数据容易出现较大方差,可是仍然提供了有用的数据

虽然咱们一直尝试在基准测试中产生稳定且一致的结果,可是曲线的变化仍然会很大,这主要取决于工做量的大小和所运行的设备。好比说,相比于其余 CPU 工做量基准测试数据,咱们发现填充视图的测试结果很是不稳定。而将阈值设置为百分之一并不能在每一个测试中得到理想的结果,可是咱们也不但愿把设定阈值的 (或者基线) 的负担施加在基准测试的做者身上,由于这个工做不但繁琐,并且随着分析规模的增长,其扩展性也相对较差。

当一些测试设备在连续几个基准测试中产生异常缓慢的结果时,方差也可能会以低频的大范围波峰的形式出现。虽然咱们能够修复其中一些 (例如,防止因电量不足致使核心被禁用时运行测试) ,可是很难避免全部的方差。

RecyclerView、Ads-identifier 以及 Room 的一次基准测试中出现的全部峰值——咱们不但愿将其做为回归模型报告出来

△ RecyclerView、Ads-identifier 以及 Room 的一次基准测试中出现的全部峰值——咱们不但愿将其做为回归模型报告出来

综上所述,咱们不能仅经过第 N 次和 N - 1 次 Build 结果就定位一个测试回归问题——咱们须要更多上下文信息来辅助决策。

分步拟合,一个可扩展的解决方案

咱们在 Jetpack CI 中进行分步拟合的方法是由 Skia Perf application 提供的。

这个方法是在基准数据中寻找阶跃函数。当咱们检查每一个基准测试的结果序列,能够尝试寻找 "阶跃" 的上升或降低的数据点做为特定的 Build 改变基准测试效果的信号。不过咱们也要多看几个数据点,以确保咱们看到的是多个结果造成的一致的趋势,而不是偶然现象:

△ 上下文能够揭示出性能退化幅度较大的位置可能只是基准化分析结果反复无常的变化而已

△ 上下文能够揭示出性能退化幅度较大的位置可能只是基准化分析结果反复无常的变化而已

那么咱们如何挑选出这样一个阶跃呢?咱们须要查看变化先后的多个结果:

而后,咱们用下面这段代码计算测试回归的权值:

这里操做的原理是,经过检测更改先后的偏差,并对该偏差的平均值的差进行加权,基准的方差越小,咱们就越有信心检测出细微的测试回归。这使得咱们能够在一个方差更高的大型 (对于移动平台来讲) 数据库基准测试的系统中运行纳秒级精度的微型基准测试。

您也能够本身尝试!点击运行按钮,尝试咱们 CI 中处理 WorkManager 基准测试产生的数据的算法。它将输出两个连接,一个指向带有 测试回归 的 build ,另外一个指向 后续相关的修正 (点击 "View Changes",来查看该次代码提交的详细内容) 。这些内容与人们在绘制数据时看到的回归和改进相匹配:

根据咱们对算法的配置,图中的全部次要噪声都将被忽略。当它开始运行时,您能够尝试用下面两个参数控制算法: 

  1. 宽度  (WIDTH) — 要涵盖多少个代码提交的结果
  2. 阈值  (THRESHOLD) — 达到什么程度时会把回归显示在面板上

增长宽度值会下降不一致性,可是也会致使在结果变更较为频繁时难以发现测试回归——咱们当前使用的宽度值是 5。阈值用于总体的敏感性控制——咱们当前用的是 25。下降阈值能够看到捕捉更多的测试回归,可是也可能致使更多的误报。

若是想在您本身的 CI 中进行配置,须要:

  1. 编写一些基准测试
  2. 在真机的 CI 中运行它们, 最好有 持续的性能支持
  3. 从 JSON 中收集输出指标
  4. 当一个结果准备完毕时,检查一下当宽度为两倍时的结果

若是有回归或改进,请发出警报 (电子邮件、问题或任何对您有用的措施) 以检查当前 WIDTH 所涵盖的 Build 的性能。

预提交

那么预提交又是什么呢?若是不但愿在 Build 中出现测试回归,则能够经过预提交来捕捉回归。在提交前运行基准测试多是彻底防止回归的好方法,可是首先要记住: 基准测试就像 Flaky 测试同样,须要像上述算法这样的基础结构来解决不稳定问题。

对于可能中断提交补丁工做流的预提交测试,您须要对所使用的回归检测有更高的可信度。

因为单次运行基准测试并不能给咱们本身带来足够的信心,因此上面的分步拟合算法是必须的。一样,咱们能够经过获取更多数据来增长这方面的信心——只须要不加修改地屡次运行,来检测补丁是否引入了测试回归便可。

对于每次修改代码而后进行的屡次基准测试,都会增长必定的资源消耗,若是您能够接受,那么预提交就可以很好地发挥做用。

全面披露——咱们目前没有在 Jetpack 的预提交中使用基准测试,但若是您愿意尝试,如下是咱们的建议: 

  • 不论有无补丁,都要运行基准测试 5 次以上 (后者一般能够缓存,也能够从提交后的结果中获取);
  • 考虑跳过特别慢的基准测试;
  • 不要阻止基于结果的补丁提交——只需在代码审查期间考虑结果便可。回归有时会做为改进代码库的一部分!
  • 要考虑到之前的结果多是不存在的。预提交没法检测已添加的基准测试。

结论

Jetpack Benchmark 提供了一种从 Android 设备外获取准确性能指标的简便方法。结合上面的逐步拟合算法,您能够解决不稳定的问题,从而能够在性能问题影响到用户前发现它们的测试回归问题——就像咱们在 Jetpack CI 中作的同样。

关于从何处开始的注意事项: 

  • 在基准测试中捕获关键的滚动界面
  • 为与第三方库交互的关键位置和高 CPU 消耗的任务添加性能测试
  • 要像对待测试回归问题同样对待改进——它们值得深究

延伸阅读

若是您想了解更多,请查阅 2019 Android Developer 峰会中咱们的演讲:《在 CI 中使用 Benchmarks

若是想更多了解 Jetpack Benchmark 是如何工做的,能够查看咱们在 Google I/O 的演讲:《使用 Benchmarks 提高应用性能

咱们使用 Skia Perf 应用来跟踪 AndroidX 库的性能,基准测试结果能够在 androidx-perf.skia.org 找到。因为它如今在咱们的 CI 中运行,您能够看到此处描述的逐步拟合算法的 实际来源。若是您想了解更多信息,Joe Gregorio 撰写的另外一篇有关他们更高级的 K-means 聚类检测算法的博文,解释了 Skia 项目开发的特定问题和解决方案,这些问题和解决方案是专门为整合多种配置 (不一样的操做系统和操做系统版本,CPU/GPU 芯片/驱动程序变体,编译器等) 设计的。

相关文章
相关标签/搜索