假如你还没有运用过build scan功用,引荐测验一下,它用精巧的UI展示了gradle构建进程中的详细信息。比方参加构建的project的层级联系,所运用到的插件,项目维度的依靠,task的履行耗时等等等等
可是它并没有将原始数据给到咱们,咱们无法根据此来做一些定制化的需求,例如耗时阈值的监控
那么下面咱们就来探究一下build scan是如何做到能搜集如此详细信息的,看看咱们是否也能够仿照它来将这些信息记载到自己的平台上
实操
咱们从一个简略的比方看起,在settings.gradle中参加如下代码
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
@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有协助,如下
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
@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耗时的办法
ManagementFactory.getGarbageCollectorMXBeans().sumBy { it.collectionTime.toInt() }
gc耗时过长有或许是内存给的不够,或许发生了内存走漏
假如一开始构建gc耗时就高或许是前者,这能够经过增加装备内存解决
假如是构建了许屡次慢慢变卡,就或许是内存走漏导致的
- 体系信息
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文件
./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进行剖析
./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,如下
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 – 剖析后的数据,输出如下(内容过长有精简)
// 参加构建的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,存在兼容危险
参阅资料
The Secrets of the Build Scan Plugin and the internals of Gradle by Nelson Osacky, Soundcloud EN – YouTube