谷歌 - 在 -CI- 中使用 -Benchmark- 进行回归分析
△?Android Studio 输出、运行多个基准测试的示例 Benchmark 库通过它自己的?JUnit Rule API?处理预热、检测配置问题以及评估代码性能。上面介绍的这些在我们自己的工作环境下用起来很不错,但是很多基准测试数据其实来自于持续集成 (Continuous Integration, CI) 中对于回归模型的检测。那么我们要如何处理 CI 中的基准数据呢?
JUnit Rule API
https://developer.android.google.cn/reference/kotlin/androidx/benchmark/junit4/BenchmarkRule.html
基准测试 vs 正确性测试
一个工程里就算有数千个正确性测试,也可以轻易通过信息折叠显示在数据面板上。下面就是我们在 Jetpack 中的测试信息:
这里没有什么特别的内容,但是在减少视觉负荷方面使用了两个常见技巧。首先,这里以包和类的维度折叠了包含数千条测试信息的列表;然后,默认情况下隐藏了结果全部正确的包。就这样,数十个库里接近两万个测试结果,就被囊括到了寥寥几行文字之中。正确性测试的面板很好地控制了所展示的数据规模。
但是基准测试又如何呢?基准测试不会简单地输出通过/不通过,每个测试的结果都是一个标量,这意味着我们没法简单地将通过的结果折叠起来。我们可以看一看数据图表,也许可以对数据的模式有个直观的了解,毕竟通常情况下,基准测试的数量要远少于正确性测试...
![](https://upload-images.jianshu.io/upload_i
mages/23319472-2a8520eca7a591f6?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
但是您却只能看到一大堆可见噪声。就算测试结果从数千减少到数百个,直接看图表对于数据的分析依然不会有任何帮助。基准测试中保持原有性能结果的数据与测试回归的数据所占据的可视区域相同,所以我们需要把未出现测试回归的数据过滤掉 (这样测试回归的数据才能凸显出来)。
简单的回归检测方法
我们可以从一些简单的事情开始,尝试回到只有通过和不通过的正确性测试。例如可以把两次运行的结果下降百分比超过某一阈值的情况定义为基准测试的失败结果。不过由于方差的原因,这种方式并不能成功。
△?视图填充的基准数据容易出现较大方差,但是仍然提供了有用的数据
虽然我们一直尝试在基准测试中产生稳定且一致的结果,但是曲线的变化仍然会很大,这主要取决于工作量的大小和所运行的设备。比如说,相比于其他 CPU 工作量基准测试数据,我们发现填充视图的测试结果非常不稳定。而将阈值设置为百分之一并不能在每个测试中获得理想的结果,但是我们也不希望把设定阈值的 (或者基线) 的负担施加在基准测试的作者身上,因为这个工作不但繁琐,而且随着分析规模的增加,其扩展性也相对较差。
当一些测试设备在连续几个基准测试中产生异常缓慢的结果时,方差也可能会以低频的大范围波峰的形式出现。虽然我们可以修复其中一些 (例如,防止因电量不足导致核心被禁用时运行测试) ,但是很难避免所有的方差。
△?RecyclerView、Ads-identifier 以及 Room 的一次基准测试中出现的所有峰值——我们不希望将其作为回归模型报告出来
综上所述,我们不能仅通过第 N 次和 N - 1 次 Build 结果就定位一个测试回归问题——我们需要更多上下文信息来辅助决策。
分步拟合,一个可扩展的解决方案
我们在 Jetpack CI 中进行分步拟合的方法是由?Skia Perf application?提供的。
Skia Perf applicationhttps://skia.org/dev/testing/skiaperf
这个方法是在基准数据中寻找阶跃函数。当我们检查每个基准测试的结果序列,可以尝试寻找 "阶跃" 的上升或下降的数据点作为特定的 Build 改变基准测试效果的信号。不过我们也要多看几个数据点,以确保我们看到的是多个结果形成的一致的趋势,而不是偶然现象:
△?上下文可以揭示出性能退化幅度较大的位置可能只是基准化分析结果反复无常的变化而已
那么我们如何挑选出这样一个阶跃呢?我们需要查看变化前后的多个结果:
然后,我们用下面这段代码计算测试回归的权值:
这里操作的原理是,通过检测更改前后的误差,并对该误差的平均值的差进行加权,基准的方差越小,我们就越有信心检测出细微的测试回归。这使得我们可以在一个方差更高的大型 (对于移动平台来说) 数据库基准测试的系统中运行纳秒级精度的微型基准测试。您也可以自己尝试!点击运行按钮,尝试我们 CI 中处理 WorkManager 基准测试产生的数据的算法。它将输出两个链接,一个指向带有测试回归的 build ,另一个指向后续相关的修正?(点击 "View Changes",来查看该次代码提交的详细内容) 。这些内容与人们在绘制数据时看到的回归和改进相匹配:
测试回归
https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?head=5783944&tail=5783944
后续相关的修正
https://ci.android.com/builds/branches/aosp-androidx-master-dev/grid?head=5787972&tail=5787972
根据我们对算法的配置,图中的所有次要噪声都将被忽略。当它开始运行时,您可以尝试用下面两个参数控制算法:?
宽度?(WIDTH) — 要涵盖多少个代码提交的结果
阈值?(THRESHOLD) — 达到什么程度时会把回归显示在面板上
增加宽度值会降低不一致性,但是也会导致在结果变动较为频繁时难以发现测试回归——我们当前使用的宽度值是 5。阈值用于整体的敏感性控制——我们当前用的是 25。降低阈值可以看到捕捉更多的测试回归,但是也可能导致更多的误报。
如果想在您自己的 CI 中进行配置,需要:
编写一些基准测试
在真机的 CI 中运行它们, 最好有持续的性能支持
从 JSON 中收集输出指标
当一个结果准备完毕时,检查一下当宽度为两倍时的结果
编写一些基准测试 https://developer.android.google.cn/studio/profile/benchmark
持续的性能支持 https://developer.android.google.cn/studio/profile/benchmark#sustained-perf
从 JSON 中收集输出指标 https://developer.android.google.cn/studio/profile/run-benchmarks-in-ci
如果有回归或改进,请发出警报 (电子邮件、问题或任何对您有用的措施) 以检查当前 WIDTH 所涵盖的 Build 的性能。
预提交
那么预提交又是什么呢?如果不希望在 Build 中出现测试回归,则可以通过预提交来捕捉回归。在提交前运行基准测试可能是完全防止回归的好方法,但是首先要记住:?基准测试就像 Flaky 测试一样,需要像上述算法这样的基础结构来解决不稳定问题。
对于可能中断提交补丁工作流的预提交测试,您需要对所使用的回归检测有更高的可信度。
由于单次运行基准测试并不能给我们自己带来足够的信心,所以上面的分步拟合算法是必须的。同样,我们可以通过获取更多数据来增加这方面的信心——只需要不加修改地多次运行,来检测补丁是否引入了测试回归即可。
评论