咱们在 I/O 2019 公布了 Benchmark 库的第一个 alpha 版。之后为了能帮忙您在优化代码时能够精确地评估性能,咱们就始终在改良 Benchmark 库。Jetpack Benchmark 是一个运行在 Android 设施上的规范 JUnit 插桩测试 (instrumentation tests),它应用 Benchmark 库提供的一套规定进行测量和报告:
@get:Rule
val benchmarkRule = BenchmarkRule()
@UiThreadTest
@Test
fun simpleScroll() {
benchmarkRule.measureRepeated {
// Scroll RecyclerView by one item
recyclerView.scrollBy(0, recyclerView.getLastChild().height)
}
}
△ Github 上的 示例工程
△ Android Studio 输入、运行多个基准测试的示例
Benchmark 库通过它本人的 JUnit Rule API 解决预热、检测配置问题以及评估代码性能。
下面介绍的这些在咱们本人的工作环境下用起来很不错,然而很多基准测试数据其实来自于继续集成 (Continuous Integration, CI) 中对于回归模型的检测。那么咱们要如何解决 CI 中的基准数据呢?
基准测试 vs 正确性测试
一个工程里就算有数千个正确性测试,也能够轻易通过信息折叠显示在数据面板上。上面就是咱们在 Jetpack 中的测试信息:
这里没有什么特地的内容,然而在缩小视觉负荷方面应用了两个常见技巧。首先,这里以包和类的维度折叠了蕴含数千条测试信息的列表;而后,默认状况下暗藏了后果全副正确的包。就这样,数十个库里靠近两万个测试后果,就被囊括到了寥寥几行文字之中。正确性测试的面板很好地管制了所展现的数据规模。
然而基准测试又如何呢?基准测试不会简略地输入通过 / 不通过,每个测试的后果都是一个标量,这意味着咱们没法简略地将通过的后果折叠起来。咱们能够看一看数据图表,兴许能够对数据的模式有个直观的理解,毕竟通常状况下,基准测试的数量要远少于正确性测试 …
然而您却只能看到 一大堆 可见噪声。就算测试后果从数千缩小到数百个,间接看图表对于数据的剖析仍然不会有任何帮忙。基准测试中放弃原有性能后果的数据与测试回归的数据所占据的可视区域雷同,所以咱们须要把未呈现测试回归的数据过滤掉 (这样测试回归的数据能力凸显进去)。
简略的回归检测办法
咱们能够从一些简略的事件开始,尝试回到只有通过和不通过的正确性测试。例如能够把两次运行的后果降落百分比超过某一阈值的状况定义为基准测试的失败后果。不过因为方差的起因,这种形式并不能胜利。
△ 视图填充的基准数据容易呈现较大方差,然而依然提供了有用的数据
尽管咱们始终尝试在基准测试中产生稳固且统一的后果,然而曲线的变动依然会很大,这次要取决于工作量的大小和所运行的设施。比如说,相比于其余 CPU 工作量基准测试数据,咱们发现填充视图的测试后果十分不稳固。而将阈值设置为百分之一并不能在每个测试中获得理想的后果,然而咱们也不心愿把设定阈值的 (或者基线) 的累赘施加在基准测试的作者身上,因为这个工作岂但繁琐,而且随着剖析规模的减少,其扩展性也绝对较差。
当一些测试设施在间断几个基准测试中产生异样迟缓的后果时,方差也可能会以低频的大范畴波峰的模式呈现。尽管咱们能够修复其中一些 (例如,避免因电量有余导致外围被禁用时运行测试),然而很难防止所有的方差。
△ RecyclerView、Ads-identifier 以及 Room 的一次基准测试中呈现的所有峰值——咱们不心愿将其作为回归模型报告进去
综上所述,咱们不能仅通过第 N 次和 N – 1 次 Build 后果就定位一个测试回归问题——咱们须要更多上下文信息来辅助决策。
分步拟合,一个可扩大的解决方案
咱们在 Jetpack CI 中进行分步拟合的办法是由 Skia Perf application 提供的。
这个办法是在基准数据中寻找阶跃函数。当咱们查看每个基准测试的后果序列,能够尝试寻找 “ 阶跃 ” 的回升或降落的数据点作为特定的 Build 扭转基准测试成果的信号。不过咱们也要多看几个数据点,以确保咱们看到的是多个后果造成的统一的趋势,而不是偶尔景象:
△ 上下文能够揭示出性能进化幅度较大的地位可能只是基准化剖析后果出尔反尔的变动而已
那么咱们如何挑选出这样一个阶跃呢?咱们须要查看变动前后的多个后果:
而后,咱们用上面这段代码计算测试回归的权值:
这里操作的原理是,通过检测更改前后的误差,并对该误差的平均值的差进行加权,基准的方差越小,咱们就越有信念检测出轻微的测试回归。这使得咱们能够在一个方差 更高 的大型 (对于挪动平台来说) 数据库基准测试的零碎中运行纳秒级精度的微型基准测试。
您也能够本人尝试!点击运行按钮,尝试咱们 CI 中解决 WorkManager 基准测试产生的数据的算法。它将输入两个链接,一个指向带有 测试回归 的 build,另一个指向 后续相干的修改 (点击 “View Changes”,来查看该次代码提交的具体内容)。这些内容与人们在绘制数据时看到的回归和改良相匹配:
依据咱们对算法的配置,图中的所有主要噪声都将被疏忽。当它开始运行时,您能够尝试用上面两个参数控制算法:
- 宽度 (WIDTH) — 要涵盖多少个代码提交的后果
- 阈值 (THRESHOLD) — 达到什么水平时会把回归显示在面板上
减少宽度值会升高不一致性,然而也会导致在后果变动较为频繁时难以发现测试回归——咱们以后应用的宽度值是 5。阈值用于整体的敏感性管制——咱们以后用的是 25。升高阈值能够看到捕获更多的测试回归,然而也可能导致更多的误报。
如果想在您本人的 CI 中进行配置,须要:
- 编写一些基准测试
- 在真机的 CI 中运行它们, 最好有 继续的性能反对
- 从 JSON 中收集输入指标
- 当一个后果筹备结束时,检查一下当宽度为两倍时的后果
如果有回归或改良,请收回警报 (电子邮件、问题或任何对您有用的措施) 以查看以后 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 芯片 / 驱动程序变体,编译器等) 设计的。