假如你还没有运用过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一样收集gradle构建信息

build scan便是根据对这些信息的剖析完成的

能搜集到哪些信息

首要咱们来看看build scan能搜集到哪些信息

下图是build scan搜集到的androidx项目一次构建输出的信息

如何像build scan一样收集gradle构建信息

要害的有如下几点:

  1. Project: 参加编译的project层级结构
  2. Switches: 一些开关的状况
  3. BuildCache: build cache local、remote装备
  4. Scripts: 运用的script,区别groovy和kts,按project维度区别
  5. Plugins: 运用的plugin,plugin的id和类名,按project维度区别
  6. Build dependencies: 脚本用到的依靠,按project维度区别
  7. Dependencies: 项目运用到的依靠,按project维度区别,包括依靠的来历库房
  8. 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个性质

  1. 具有层级联系

咱们知道gradle的生命周期分为evaluateconfigureexecuteevaluate首要是履行脚本,而脚本的履行进程中又或许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记载的粒度会更细

  1. 一个完好的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);  
}

从这能够看出,实际上要结合startedprogressfinished的完好参数信息才能分分出一个BuildOperation的状况,下面咱们展开看看

BuildOperation参数剖析

BuildOperationDescriptor

BuildOperationmetadata,一些重要的参数

  • id – BuildOperation的标识符,用于和其他BuildOperation区别
  • parentId – BuildOperation层级联系的体现,子任务的parentId和父任务的id相关
  • displayName – 操作的称号,辅佐了解用的
  • details – 除了基础信息外还有,每个操作自身还有自己额定的参数

OperationStartEvent

只有一个startTime

OperationFinishEvent

result – 每个操作自身履行的成果对象,例如task graph calculate就能够拿到task plan

startfinish能够分分出履行耗时,比方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
  • taskOutcomeSKIP/NO-SOURCE/UP-TO-DATE/FROM-CACHE/EXECUTE这些
  • cacheable – 是否支撑缓存
  • actionable – 区别action tasklifecycle 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开关状况

其他信息搜集

  1. 获取gc耗时的办法
ManagementFactory.getGarbageCollectorMXBeans().sumBy { it.collectionTime.toInt() }

gc耗时过长有或许是内存给的不够,或许发生了内存走漏
假如一开始构建gc耗时就高或许是前者,这能够经过增加装备内存解决
假如是构建了许屡次慢慢变卡,就或许是内存走漏导致的

  1. 体系信息
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型号频率等信息

  1. 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 serviceconfiguration 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来处理

  1. 先生成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这个文件记载了一切的BuildOperationgradle-to-trace-converter便是对它进行的剖析

  1. 运用官方库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文件一样

如何像build scan一样收集gradle构建信息

长处:

  • 官方供给的办法
  • 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