如果你还没有使用过build scan 功能,推荐尝试一下,它用精美的UI展示了gradle构建过程中的详细信息。比如参与构建的project的层级关系,所使用到的插件,项目维度的依赖,task的执行耗时等等等等
但是它并没有将原始数据给到我们,我们无法基于此来做一些定制化的需求,例如耗时阈值的监控
那么下面我们就来探究一下build scan是如何做到能收集如此详细信息的,看看我们是否也能够仿照它来将这些信息记录到自己的平台上
实操
我们从一个简单的例子看起,在settings.gradle中加入如下代码
groovy
import org.gradle.api.internal.GradleInternal
import org.gradle.internal.operations.trace.BuildOperationTrace
import groovy.json.JsonOutput
import org.gradle.internal.operations.*
def gradleInternal = (gradle as GradleInternal)
def manager = gradleInternal.services.get(BuildOperationListenerManager)
manager.addListener(new BuildOperationListener() {
@Override
void started(BuildOperationDescriptor buildOperationDescriptor, OperationStartEvent operationStartEvent) {
}
@Override
void progress(OperationIdentifier operationIdentifier, OperationProgressEvent operationProgressEvent) {
}
@Override
void finished(BuildOperationDescriptor buildOperationDescriptor, OperationFinishEvent operationFinishEvent) {
def startTime = operationFinishEvent?.startTime
def endTime = operationFinishEvent?.endTime
def details = BuildOperationTrace.toSerializableModel(buildOperationDescriptor.getDetails())
def result = BuildOperationTrace.toSerializableModel(operationFinishEvent?.result)
println "finished: \n" +
" id: ${buildOperationDescriptor.id}\n" +
" cost: ${endTime - startTime}ms \n" +
" detail: ${JsonOutput.toJson(details)}\n" +
" result: ${JsonOutput.toJson(result)}"
}
})
执行sync或者gradle命令,你将在控制台输出中看到大量信息
build scan就是基于对这些信息的分析完成的
能收集到哪些信息
首先我们来看看build scan能搜集到哪些信息
下图是build scan采集到的androidx项目一次构建输出的信息
关键的有如下几点:
Project
: 参与编译的project层级结构Switches
: 一些开关的状态BuildCache
: build cache local、remote配置Scripts
: 应用的script,区分groovy和kts,按project维度区分Plugins
: 应用的plugin,plugin的id和类名,按project维度区分Build dependencies
: 脚本用到的依赖,按project维度区分Dependencies
: 项目使用到的依赖,按project维度区分,包含依赖的来源仓库TaskExecution
:- task的执行信息
- task名称
- task执行结果
- 执行耗时(snapshot inputs耗时, build cache耗时)
- 是否支持缓存(如不支持列出具体原因)
- 是否增量
- 执行原因(如有执行)
- snapshot inputs所有hash信息
BuildOperation
build scan 能够收集到这些信息,主要是因为gradle本身对BuildOperation有完善的记录在,我们首先需要对gradle自身的信息收集机制有一定的了解
每次完整的构建过程可以看作是一次session,每个session都会初始化一个对应的BuildOperationListenerManager
,它记录的就是BuildOperation
java
@ServiceScope(Scope.Global.class)
public interface BuildOperationListenerManager {
void addListener(BuildOperationListener listener);
void removeListener(BuildOperationListener listener);
BuildOperationListener getBroadcaster();
}
而我们只需要通过往这里面添加自己的监听就可以采集到这些信息
BuildOperation
,顾名思义就是构建过程中的操作行为,完整的一次构建中会产生大量的操作行为,gradle会将这些行为全部记录下来,这里的信息很多,比如script的加载,apply plugin,register task,task执行等等等等
BuildOperation
有2个性质
- 具有层级关系
我们知道gradle的生命周期分为evaluate
、configure
和execute
,evaluate
主要是执行脚本,而脚本的执行过程中又可能apply plugin,像这样子就形成了层级关系,举个例子,在执行脚本的过程中,可能会register task
,或者在加载plugin的时候会去register task
,那register task
这个操作就可以存在于apply script
或者apply plugin
下面,对于分析哪些plugin引入了哪些task有帮助,如下
arduino
RootBuild
Evalute
Apply Setting Script
Apply Build Script
Apply Java Plugin
Register Compile Task
Apply Publish Plugin
Register Publish Task
Register Custom Task
Configure
Resolve Task Graph
Execution
Execute Compile Task
Execute Publish Task
这样子的层级关系,当然实际情况比这复杂得多,BuildOperation
记录的粒度会更细
- 一个完整的
BuildOperation
由start、progress(可以缺失)、finish3部分组成
从BuildOperationListenerManager
提供的接口中我们能看到,listener需要实现BuildOperationListener
才能收到BuildOperation
的事件,来看看这个BuildOperationListener
java
@EventScope(Global.class)
public interface BuildOperationListener {
void started(BuildOperationDescriptor buildOperation, OperationStartEvent startEvent);
void progress(OperationIdentifier operationIdentifier, OperationProgressEvent progressEvent);
void finished(BuildOperationDescriptor buildOperation, OperationFinishEvent finishEvent);
}
从这可以看出,实际上要结合started
、progress
和finished
的完整参数信息才能分析出一个BuildOperation
的情况,下面我们展开看看
BuildOperation参数分析
BuildOperationDescriptor
BuildOperation
的metadata,一些重要的参数
id
- BuildOperation的标识符,用于和其他BuildOperation区分parentId
- BuildOperation层级关系的体现,子任务的parentId和父任务的id关联displayName
- 操作的名称,辅助理解用的details
- 除了基础信息外还有,每个操作本身还有自己额外的参数
OperationStartEvent
只有一个startTime
OperationFinishEvent
result
- 每个操作本身执行的结果对象,例如task graph calculate
就可以拿到task plan
从start
和finish
可以分析出执行耗时,比如task执行的耗时,解析依赖的耗时,下载依赖的耗时,脚本执行的耗时等等
而区分不同BuildOperation
的,目前只能通过details
或者result
的类型去区分
一般这2个都是放在一起的,以apply plugin这个操作为例
detail的类型是ApplyPluginBuildOperationType.Details
result的类型是ApplyPluginBuildOperationType.Result
下面来看一些主要的BuildOperation
类型
一些关键的BuildOperation类型
ApplyPluginBuildOperationType
执行apply plugin
时会发送这个BuildOperation
事件,能搜集到的主要信息有
- plugin id - java plugin完整的id是
org.gradle.java
,org.gradle开头的都是官方插件,可以省略前面的部分 - plugin class - java plugin完整的class是
org.gradle.api.plugins.JavaPlugin
- target type - 可以用来区分settings.gradle和build.gradle
- build path - 可以用来识别是那个project的,多module构建有用
ApplyScriptPluginBuildOperationType
apply script
操作,能获取到的信息有
- getUri/getFile - 判断是file还是uri
- target type 同apply plugin
- build path - 路径
ExecuteTaskBuildOperationType
task相关信息,也是最重要的一个,主要有
- task path - 例如
':compileJava'
,':libA:processResource'
- task class - 例如默认task为
org.gradle.api.DefaultTask
- taskOutcome -
SKIP/NO-SOURCE/UP-TO-DATE/FROM-CACHE/EXECUTE
这些 - cacheable - 是否支持缓存
- actionable - 区分
action task
和lifecycle task
- incremental - 是否支持增量
- skipReasonMessage - 如果SKIP了,SKIP的原因
- executionReason - 执行原因,例如
compileJava
是支持缓存的,如果inputs有变动,这里会输出是哪些文件改动导致的重新执行 - originExecutionTime - 例如如果上一次是execute了的,这次是from-cache的,
originExecutionTime
记录的就是上次执行的时间,对增量构建节省的耗时有一定的参考价值 - snapshot inputs hash - 以
compileJava
举例,就是所有源码文件、classpath文件的hash值,这里可能会非常多
SnapshotTaskInputsBuildOperationType
执行task时进行的snapshot操作,这里可以拿到fingerprints和build cache key等信息,用来判断缓存命中率有一定帮助
RealizeTaskBuildOperationType
可以用来区分task是否eager创建,create task方式是eager,register方式是lazy
NotifyProjectAfterEvaluatedBuildOperationType NotifyProjectBeforeEvaluatedBuildOperationType
用来获取project.before/after
的耗时
ExecuteListenerBuildOperationType
可以分析插件或脚本hook gradle生命周期的耗时
LoadProjectsBuildOperationType
project完整信息,可以用来分析参与构建的project及其层级关系
ResolveConfigurationDependenciesBuildOperationType
依赖相关信息,无论是脚本依赖还是项目依赖都是这个,有提供方法区分
还可以获取到依赖的来源仓库,依赖冲突的裁决过程等
BuildOptionBuildOperationProgressEventsEmitter
这是一个progress事件,主要分析configuration cache
开关状态
其他信息收集
- 获取gc耗时的方法
groovy
ManagementFactory.getGarbageCollectorMXBeans().sumBy { it.collectionTime.toInt() }
gc耗时过长 有可能是内存给的不够,或者发生了内存泄露
如果一开始构建gc耗时就高可能是前者,这可以通过增加配置内存解决
如果是构建了很多次慢慢变卡,就可能是内存泄露导致的
- 系统信息
ini
def osName = getSystemProperty("os.name")
def osVersion = getSystemProperty("os.version")
def javaVersion = getSystemProperty("java.version")
def javaVmVersion = getSystemProperty("java.vm.version")
def runtimeMemory = Runtime.getRuntime().maxMemory()
def gradleVersion = gradle.gradleVersion
操作系统名称版本,java版本等等信息。此外还可以尝试去获取CPU型号频率等信息
- CustomValues
build scan提供了方法可以添加一些tag,这些tag对于分析也很有帮助,例如 git的分支、commit id,CI的机器信息等
CI run: 5183423829
CI workflow: AndroidX Presubmits
Git branch: androidx-main
Git commit id: c219c9f36e7b4d5d2f56c280f0ba4422e547e039
Git commit id short: c219c9f3
Git repository: github.com/androidx/an...
采集方案
Build Service
Kotlin、agp都使用了这种方式
Android Studio的Build Analyzer就是通过agp使用这个方式来实现的
Troubleshoot build performance with Build Analyzer | Android Studio | Android Developers
Kotlin的Build Reports也是使用这个方式统计kotlin编译过程的信息
Compilation and caches in the Kotlin Gradle plugin | Kotlin Documentation
优点:
- 官方提供的api实现,维护有保障
- agp、kotlin通过这种方式实现,有参考价值
- configuration cache兼容
缺点:
- 拿到的数据不完整,注册监听的时机在script执行阶段,前面的信息会有丢失,这部分信息丢失倒影响不大
build service 官方文档Shared Build Services
build service 是configuration cache 出现后,对于脚本、plugin内一些副作用无法实现的一个替代方案
当使用configuration cache 时,脚本、plugin的执行可能会被跳过,这会导致其中注册的对于构建流程的监听就无效了
build service 可以弥补这一部分功能的缺失,它会由configuration cache进行恢复
configuration cache 的部分可以参考官方文档Configuration cache
Service Injection 的部分可以参考官方文档Developing Custom Gradle Types
Build Scan方式
优点:
- configuration cache兼容
- 数据采集完整
- 可以添加tag等额外信息
缺点:
- gradle internal api,存在兼容适配风险
BuildOperationTrace
trace的采集是我在研究gradle源码时碰巧发现的,目前没有找到官方文档
BuildOperationTrace
注释中有使用方法BuildOperationTrace
trace采集的数据可以通过gradle官方的库gradle-to-trace-converter来处理
- 先生成trace文件
shell
./gradlew build -Dorg.gradle.internal.operations.trace=/your/project/path/trace
会生成3个文件
trace-log.txt
trace-tree.json
trace-tree.txt
trace-tree.json
这个文件记录了所有的BuildOperation
,gradle-to-trace-converter就是对它进行的分析
- 使用官方库gradle-to-trace-converter进行分析
shell
./gradlew :app:run --args='/your/project/path/trace-tree.json -o all'
指定all
会生成3个文件
trace-tree-chrome.proto
trace-tree-timeline.csv
trace-tree-transform-summary.csv
值得一提的是这个proto文件,Chrome里打开perfetto的地址,将trace-tree-chrome.proto
拖进去就行,展示效果如下,和Android分析trace文件一样
优点:
- 官方提供的方式
- configuration cache兼容
缺点:
- 不能添加tag,tag可以记录git版本、commit id等信息,用在CICD流程中可以用其他方式部分弥补这些不足
- 数据信息相比build scan方式略有不足,不过总体影响不大
Build Trace Plugin
既然知道了build scan的原理,我们是否可以自己写一个类似功能的插件呢,当然可以
GitHub - neas-neas/gradle_trace_plugin
这是我参照build scan写的一个插件,在settings.gradle 中使用
目前没有上传到gradle官方仓库,所以还只能使用老方式apply,如下
groovy
buildscript{
repositories {
mavenCentral()
}
dependencies{
classpath 'io.github.neas-neas:gradle-trace-plugin:0.0.2'
}
}
apply plugin: 'build.trace'
目前为了简单,数据的收集和分析直接都放一起了
它就会自动收集构建信息,输出2个文件
buildOpTrace.json - 原始json数据
buildOpTrace-analyzer.txt - 分析后的数据,输出如下(内容过长有精简)
yaml
// 参与构建的project层级关系
Project
:(dir: root)
// 一些开关配置状态
Switches
Configuration Cache: Off
File System Watch: On
Build Cache: On
// build cache信息
BuildCache
Local cache:
Type: directory
Push: enabled
Configuration
Location: /Users/username/.gradle/caches/build-cache-1
RemoveUnusedEntriesAfter: 7 days
Remote cache: disabled
// plugin信息
Plugins
path ':'
targetType: gradle
id: no_id pluginClass: JetGradlePlugin
targetType: settings
id: build.trace pluginClass: com.neas.trace.BuildTracePlugin
targetType: project
id: org.gradle.help-tasks pluginClass: org.gradle.api.plugins.HelpTasksPlugin
id: org.gradle.build-init pluginClass: org.gradle.buildinit.plugins.BuildInitPlugin
id: org.gradle.wrapper pluginClass: org.gradle.buildinit.plugins.WrapperPlugin
id: org.gradle.java pluginClass: org.gradle.api.plugins.JavaPlugin
// build脚本使用到的依赖
Build dependencies
path: ':'
components:
io.github.neas-neas:gradle-trace-plugin:0.0.2(MavenRepo)
com.google.code.gson:gson:2.10(MavenRepo)
unspecified:unspecified:unspecified
configurationName: classpath
components:
javax.inject:javax.inject:1(MavenRepo)
commons-io:commons-io:2.5(MavenRepo)
commons-codec:commons-codec:1.9(MavenRepo)
// 项目使用到的依赖
Dependencies
path: ':'
configurationName: compileClasspath
components:
org.jetbrains.kotlin:kotlin-gradle-plugins-bom:1.9.0-RC(MavenRepo)
com.android.tools.build:apkzlib:8.0.0(Google)
org.jetbrains.kotlin:kotlin-project-model:1.9.0-RC(MavenRepo)
com.android.tools.build:gradle:8.0.0(Google)
org.jetbrains.kotlin:kotlin-util-klib:1.9.0-RC(MavenRepo)
org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.9.0-RC(MavenRepo)
...
// task执行信息
TaskExecution
task ':prepareKotlinBuildScriptModel' UP-TO-DATE
taskClass: org.gradle.api.DefaultTask
duration: 0.001s
snapshot inputs duration: 0.0s
cacheable: false, reason: Cacheability was not determined
actionable: false
incremental: false
优点:
- 数据信息完整
- 相比与build scan能获取到原始数据,对于集成到监控系统比较方便
缺点:
- 还没有添加tag功能
- configuration cache不兼容,后续优化
- 使用了internal api,存在兼容风险