Gradle 性能测试套件实战场景构建、指标采集与 git bisect 回归定位【免费下载链接】gradleAdaptable, fast automation for all项目地址: https://gitcode.com/gh_mirrors/gr/gradle本指南基于 Gradle 官方仓库中的性能测试套件位于testing/performance系统讲解其整体架构、场景构成、模板化测试构建的生成方式、采集的指标、HTML 报告的生成与查看并重点演示如何使用git bisect精确定位引入性能回归的具体提交。读完本文你将能够看懂 Gradle 自带的性能测试如何工作、如何在本地运行单个性能测试验证改动、以及如何在发现回归后快速锁定 culprit commit 并完成验证。一、性能测试套件总体架构Gradle 的性能测试套件位于仓库的 testing/performance 目录其配套的测试基础设施则位于 testing/internal-performance-testing。一个**性能测试场景performance test scenario**由两个部分组成一个用于测试的构建test build这些测试构建由模板生成模板定义在 build-logic/performance-testing/src/main/groovy/gradlebuild.performance-templates.gradle 插件中调用 Gradle或 Maven的细节包括要执行的任务、JVM 参数、以及使用 Daemon 还是 Tooling API 来调用 Gradle 等。性能测试通过配置各种 fixture 来描述每个场景。fixture 会先将场景运行若干次进行预热warm up然后再运行若干次采集指标capturing metrics。采集到的指标被写入~/.gradle-performance-test-data目录下的数据库中同时一份报告会生成到build/performance-tests/report目录。从源码结构看测试基础设施testing/internal-performance-testing/src/main/groovy/org/gradle/performance已经形成了一个完整的分层体系测试基类AbstractPerformanceTest、AbstractCrossVersionPerformanceTest、AbstractCrossBuildPerformanceTestfixture 层CrossVersionPerformanceTestRunner、GradleInvocationSpec、PerformanceTestJvmOptions、TestProjects等负责把场景描述翻译成实际执行计划结果存储层results包下的CrossVersionResultsStore、PerformanceDatabase、DataReporter等负责把指标持久化到数据库报告渲染层results/report包下的DefaultReportGenerator、IndexPageGenerator、TestPageGenerator、ExecutionGraph等负责把数据库中的历史结果渲染成静态 HTML 页面。二、性能测试构建模板驱动与参数化生成2.1 模板目录构建模板存放在 testing/performance/src/templates 中。每个模板构建都在一定程度上做了参数化例如可以指定要生成多少个 project、多少份 source 文件或 test 文件。仓库中实际存在的模板包括Java / Groovy 相关project-with-source、java-source、with-junit、with-testng、with-verbose-junit、with-verbose-testng、workerApiProjectKotlin DSL 相关kts-empty、kts-settings、kts-project-with-source、kts-many-accessorsNative 相关native-source、native-component、native-monolithic、native-dependents、native-pch-source、native-pch-component、cpp-source、cpp-project其他config-inject、task-creation、gradle-properties、empty、minimal、root-project、generateLotsOfDeprecationWarnings、generateDiverseDeprecationWarnings、archivePerformanceProject等。2.2 模板注册与构建参数gradlebuild.performance-templates.gradle 插件为每个性能测试构建注册一个 task指明使用哪些模板以及构建参数。从该插件的源码可以看到测试项目按类型被组织为几大类Gradle 自身构建gradleBuildCurrent、gradleBuildBaseline使用RemoteProject直接以仓库根目录为被测构建baseline 固定指向某个提交 IDJava / Groovy 构建如largeMonolithicJavaProject、mediumJavaMultiProject、smallJavaMultiProject等通过JavaExecProjectGeneratorTask调用org.gradle.performance.generator.TestProjectGenerator生成Kotlin DSL 构建如ktsManyProjects100 个 project、ktsSmall、kotlinDslManyAccessors单个 project 生成 500 个 source setNative 构建smallNative/mediumNative/bigNative/multiNative/manyProjectsNative、带 PCH预编译头的smallPCHNative/mediumPCHNative/bigPCHNative、以及单体式nativeMonolithic系列C 构建smallCppApp/mediumCppApp/bigCppApp、smallCppMulti/mediumCppMulti/bigCppMultiSwift 构建mediumSwiftMulti、bigSwiftApp通过BuildBuilderGenerator生成Android 构建largeAndroidBuild、nowInAndroidBuild、android100Kts、android500Kts等需要配置对应的-D...ProjectRef属性外部真实项目springBootApp、largeNativeBuild、excludeRuleMergingBuild等同样通过RemoteProject拉取并固定 commit。以与git bisect章节直接相关的mediumNativeMonolithic为例其注册参数为performanceTest.registerTestProject(mediumNativeMonolithic, MonolithicNativeProjectGeneratorTask) { templateArgs [overlapWithOutput: false] projects 10 // 10 个子项目 sourceFiles 200 // 每个项目每种类型生成 200 个源文件 daemonMemory 512m }随后所有MonolithicNativeProjectGeneratorTask还会统一补充参数functionCount: 50每个源文件中的函数数、prebuiltLibraries: 30预编译库数量、includedSourceCount: 50、includedHeaderCount: 10、includedCommonHeaderCount: 10并指定根项目模板为native-monolithic与gradle-properties附加文件为common.gradle、prebuilt.gradle、components.gradle这些文件可在 testing/performance/src/templates/native-monolithic 中查看maxWorkers 12。也就是说mediumNativeMonolithic实际生成的是一个 10 个 project、每个 project 200 个 C/C 源文件、内含大量函数与预编译库依赖的中型 Native 单体构建专门用于测试增量 Native 构建性能。三、跨版本性能测试基类与 Runner 配置testing/internal-performance-testing/README.md 指出性能测试以 Spock 测试实现应继承以下两个基类之一AbstractCrossVersionPerformanceTest使用被测 Gradle 版本运行特定场景并使用若干个基线 Gradle 版本分别运行同一场景将结果与基线对比报告相对基线的性能回归AbstractCrossBuildPerformanceTest仅使用被测 Gradle 版本运行多个不同场景并比较结果。其中被测 Gradle 版本通常是与性能测试套件同一 git 提交构建出的 Gradle 发行版但也可以是任意 Gradle 发行版基线 Gradle 版本通常是一个或多个已发布的 Gradle 版本但同样可以是任意发行版。从 AbstractCrossVersionPerformanceTest.groovy 的源码可以看到该基类在setup()中构建了一个CrossVersionPerformanceTestRunner并注入GradleBuildExperimentRunner以 Gradle Profiler 为后端执行实验、CrossVersionResultsStore结果数据库、ReleasedVersionDistributions已发布版本信息等组件runner.current被设置为UnderDevelopmentGradleDistribution即当前正在开发的发行版。CrossVersionPerformanceTestRunner源码见 CrossVersionPerformanceTestRunner.groovy暴露了以下关键配置字段供测试类在given:块中设置字段含义默认值testId/testClassName测试标识与类名用于写入结果数据库null未设置会抛异常testProject使用的测试构建名称即模板注册名如mediumNativeMonolithic从TestScenarioSelector加载tasksToRun要执行的任务列表如[build][]cleanTasks每次运行前执行的清理任务[]args传给 Gradle 的命令行参数如-Dorg.gradle.paralleltrue、--max-workers12[]gradleOptsJVM 参数[]useDaemon是否使用 Gradle Daemon 调用trueuseToolingApi是否通过 Tooling API 调用falsewarmUpRuns预热运行次数nullruns采集指标运行次数nulltargetVersions指定的基线版本列表如[2.14]、[nightly]、[last][]minimumBaseVersion基线版本的最低要求低于该 base 版本的基线会被过滤掉nullpreviousTestIds历史测试 ID用于结果关联[]run()方法的执行流程见CrossVersionPerformanceTestRunner.groovy第 144–186 行为先运行当前版本再依次运行解析出的基线版本每次运行使用独立的 per-version 工作目录最终把CrossVersionPerformanceResults交给 reporter 写入数据库并在finally中设置结束时间。基线版本的解析由 BaselineVersionResolver.groovy 完成值得注意的细节包括特殊版本名last表示最近一个正式 releasenightly表示最新 nightly 快照RC/snapshot 版本名会被直接信任并加入列表通过系统属性-Dorg.gradle.performance.baselines可以覆盖基线版本列表支持逗号或分号分隔defaults表示回退到targetVersions除非显式指定了last/nightly/RC/snapshot 作为目标否则始终会把最近的正式 release 追加为基线minimumBaseVersion会用GradleVersion.baseVersion做下限过滤若过滤后为空且处于历史通道则跳过该测试。回归断言在 CrossVersionPerformanceResults.groovy 的assertCurrentVersionHasNotRegressed()中实现若启用了回归检查它会检查当前版本是否被某个基线版本显著更快significantlyFasterThan若是则抛出AssertionError。四、采集的指标性能测试在每个采集轮次中记录以下指标总构建执行时间Total build execution time构建配置时间Build configuration time任务执行时间Task execution time构建结束时的堆消耗Heap consumption at the end of the build构建期间的总堆使用量Total heap usage during the build。从源码结构看这些度量由measure包中的类型支撑例如Amount/Duration/DataAmount/MeasuredOperation见 testing/internal-performance-testing/src/main/groovy/org/gradle/performance/measure用于携带带单位的时间和内存数值结果对象CrossVersionPerformanceResults中同时保留了速度统计getSpeedStatsAgainst与内存统计getMemoryStatsAgainst回归断言可针对其中任意一类进行检查。五、报告生成与结果查看performance:report任务会从~/.gradle-performance-test-data数据库的内容生成一份静态 HTML 报告该报告支持将随时间变化的结果可视化即性能曲线图。报告生成逻辑位于 testing/internal-performance-testing/src/main/groovy/org/gradle/performance/results/report 包下其中ExecutionGraph负责渲染执行时间图形IndexPageGenerator生成总览页TestPageGenerator生成每个测试的详情页。针对 master 分支最近一次测试套件运行生成的报告Gradle 会在 CI 上分别产出Linux、macOS、Windows三个平台的性能测试结果压缩包performance-test-results.zip其中包含report/index.html入口页可登录对应的构建产物下载查看本文不再列出外部链接。需要特别说明运行成本一次性运行全部性能测试是资源密集且耗时的通常需要数小时因此更常见的做法是运行单个性能测试。每个 Graph 页面顶部都附有运行单个性能测试的说明单次本地运行的结果不会包含与历史执行的对比但你可以直接查看数字自行确认你的改动没有引入回归。六、用 git bisect 追踪性能回归当你在性能曲线图上看到回归或者出现新的失败测试时由于性能测试的反馈周期较长很难凭直觉定位是哪一次提交触发的回归。docs/performance-bisect.md 提供了一套完整、可复用的git bisect流程分为四步识别测试 → 修改测试 → 执行搜索 → 验证结果。6.1 第一步识别引发回归的测试目前还没有曲线图结果 ↔ 测试类的自动映射因此需要在代码库中搜索。例如曲线图上的native build medium header file change这个场景对应测试类RealWorldNativePluginPerformanceTest当前位于 testing/performance/src/performanceTest/groovy/org/gradle/performance/regression/nativeplatform/RealWorldNativePluginPerformanceTest.groovy。同时还需要知道该测试使用的 sample在本例中是mediumNativeMonolithic。对照当前源码RealWorldNativePluginPerformanceTest中确实存在build with #changeType file change测试其changeType覆盖header、source、build三种变更类型其中 header 变更对应的被修改文件为modules/project1/src/src50_h.h且通过runner.addBuildMutator注入了一个在每轮迭代中交替修改文件/还原文件的BuildMutator——这正是增量构建性能场景的测量方式。6.2 第二步修改测试用于回归搜索为了让搜索聚焦于目标回归并显著加速需要对测试做如下修改只使用你关心的那个版本作为参考基线只执行你关心的那个测试收紧回归限制以获得有意义的显著性结果只搜索内存或执行时间回归取决于你关注哪一类。以下示例来自文档对应示例假设要追踪native build medium header file change的性能回归注意这些代码片段对应的是该文档撰写时的 API当前仓库中的 runner 与结果类已经演进但修改思路完全一致。首先修改RealWorldNativePluginPerformanceTest只保留感兴趣的那一行测试数据Ignore Unroll(Project #testProject measuring incremental build speed) def build real world native project() { ... } Unroll(Project #buildSize native build #changeType) def build with changes(String buildSize, String changeType, AmountDuration maxExecutionTimeRegression, String changedFile, Closure changeClosure) { ... runner.maxExecutionTimeRegression maxExecutionTimeRegression runner.targetVersions [2.14] runner.useDaemon true ... where: // source file change causes a single project, single source set, single file to be recompiled. // header file change causes a single project, two source sets, some files to be recompiled. // recompile all sources causes all projects, all source sets, all files to be recompiled. buildSize | changeType | maxExecutionTimeRegression | changedFile | changeClosure // medium | source file change | millis(300) | modules/project5/src/src100_c.c | this.changeCSource medium | header file change | millis(140) | modules/project1/src/src50_h.h | this.changeHeader // medium | recompile all sources | millis(1500) | common.gradle | this.changeArgs } ...接着为了让测试只检查性能回归速度而忽略内存回归修改CrossVersionPerformanceResults... void assertCurrentVersionHasNotRegressed() { def slower checkBaselineVersion({ it.fasterThan(current) }, { it.getSpeedStatsAgainst(displayName, current) }) // def larger checkBaselineVersion({ it.usesLessMemoryThan(current) }, { it.getMemoryStatsAgainst(displayName, current) }) // if (slower larger) { // throw new AssertionError($slower\n$larger) // } if (slower) { throw new AssertionError(slower) } // if (larger) { // throw new AssertionError(larger) // } } ...最后为了只针对 2.14 做对比而不是默认追加最近 release修改CrossVersionPerformanceTestRunner.toBaselineVersions... static LinkedHashSetString toBaselineVersions(ReleasedVersionDistributions releases, ListString targetVersions, boolean adhocRun) { ... // if (!targetVersions.contains(nightly)) { // // Include the most recent final release if were not testing against a nightly // baselineVersions.add(mostRecentRelease) // } else { // baselineVersions.add(mostRecentSnapshot) // } } baselineVersions } ...6.3 第三步执行搜索执行搜索期间机器上不应有其他活动因此最好使用专用机器例如 CI 基础设施来跑搜索。要检查当前 revision 下测试是否通过可以使用仓库自带的 check-rev.sh 脚本。从脚本源码看它的工作方式如下执行./gradlew clean若存在~/.gradle-bisect-override目录则把其中的文件cp -Rdvp复制到工作目录若存在可执行的~/.gradle-bisect-override-script则调用它传入测试名与测试项目运行单测./gradlew -S -PtimestampedVersion -x :performance:prepareSamples :performance:$TESTPROJECT :performance:cleanPerformanceTest :performance:performanceTest -D:performance:performanceTest.single$TESTNAME把测试结果 XML 复制到~/.gradle-bisect-results/result_${exitcode}_${shortHash}_${timestamp}.xmlgit reset --hard清空工作区改动并以测试退出码作为脚本退出码。注意~/.gradle-bisect-override中的文件会被复制到工作目录脚本会git reset --hard重置任何改动因此你之前修改的所有文件都要放入~/.gradle-bisect-override目录。用法如下对应示例将测试类与两个 fixture 类放入覆盖目录mkdir ~/.gradle-bisect-override # copy test class to override directory and make changes in that directory rsync -aRv testing/performance/src/performanceTest/groovy/org/gradle/performance/regression/nativeplatform/RealWorldNativePluginPerformanceTest.groovy \ testing/internal-performance-testing/src/main/groovy/org/gradle/performance/fixture/{CrossVersionPerformanceResults,CrossVersionPerformanceTestRunner}.groovy \ ~/.gradle-bisect-override # check revision ./check-rev.sh RealWorldNativePluginPerformanceTest mediumNativeMonolithic说明文档原始示例中写的是./check_rev.sh下划线仓库中实际脚本文件名是check-rev.sh连字符见 testing/performance/docs/check-rev.sh执行时请以实际文件名为准。确认脚本工作正常后即可交给git bisect全自动执行git bisect start HEAD REL_2.14 -- # HEADbad REL_2.14good git bisect run check-rev.sh RealWorldNativePluginPerformanceTest mediumNativeMonolithic搜索耗时取决于提交数量。每步之后测试结果都会被复制到~/.gradle-bisect-results。要快速总览 bisect 的当前状态可以直接对该目录 grepmymachine:~$ grep -A 1 ^Speed ~/.gradle-bisect-results/*.xml /home/vmadmin/.gradle-bisect-results/result_0_cd420bfd_2016-06-17-13:15:11.xml:Speed Results for test project mediumNativeMonolithic with tasks build: were slower than 2.14. /home/vmadmin/.gradle-bisect-results/result_0_cd420bfd_2016-06-17-13:15:11.xml-Difference: 3.8 ms slower (3.8 ms), 0.39%, max regression: 140 ms -- /home/vmadmin/.gradle-bisect-results/result_1_00f795e2_2016-06-17-13:10:45.xml:Speed Results for test project mediumNativeMonolithic with tasks build: were slower than 2.14. /home/vmadmin/.gradle-bisect-results/result_1_00f795e2_2016-06-17-13:10:45.xml-Difference: 170.4 ms slower (170.4 ms), 17.21%, max regression: 140 ms -- /home/vmadmin/.gradle-bisect-results/result_1_29731dc5_2016-06-17-13:06:17.xml:Speed Results for test project mediumNativeMonolithic with tasks build: were slower than 2.14. /home/vmadmin/.gradle-bisect-results/result_1_29731dc5_2016-06-17-13:06:17.xml-Difference: 155.4 ms slower (155.4 ms), 15.62%, max regression: 140 ms文件名中的result_0/result_1分别对应退出码 0 和 1即通过/失败Difference行同时给出了绝对差值、百分比与max regression阈值方便快速判断差距是否显著。6.4 第四步验证结果git bisect找到候选提交后强烈建议做两项验证在HEAD上 revert 该提交确认 revert 后回归消失——这能证明该提交确实是引入回归的根因检查测试失败是否由性能之外的原因引起查看~/.gradle-bisect-results中的测试结果尤其是非Speed相关行如编译错误、环境问题等排除 flaky 或环境干扰导致的误判。七、仓库关键路径速查性能测试套件主体testing/performance测试基础设施说明testing/internal-performance-testing/README.md模板生成插件build-logic/performance-testing/src/main/groovy/gradlebuild.performance-templates.gradle构建模板目录testing/performance/src/templates跨版本测试基类AbstractCrossVersionPerformanceTest.groovyRunner 与调用规格CrossVersionPerformanceTestRunner.groovy、GradleInvocationSpec.groovy基线版本解析BaselineVersionResolver.groovy回归断言CrossVersionPerformanceResults.groovy报告生成results/report回归定位手册docs/performance-bisect.md 与辅助脚本 check-rev.sh真实 Native 性能测试示例RealWorldNativePluginPerformanceTest.groovy【免费下载链接】gradleAdaptable, fast automation for all项目地址: https://gitcode.com/gh_mirrors/gr/gradle创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考