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

如果你还没有使用过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项目一次构建输出的信息

关键的有如下几点:

  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

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个性质

  1. 具有层级关系

我们知道gradle的生命周期分为evaluateconfigureexecuteevaluate主要是执行脚本,而脚本的执行过程中又可能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记录的粒度会更细

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

从这可以看出,实际上要结合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
  • taskOutcome - SKIP/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耗时的方法
groovy 复制代码
ManagementFactory.getGarbageCollectorMXBeans().sumBy { it.collectionTime.toInt() }

gc耗时过长 有可能是内存给的不够,或者发生了内存泄露

如果一开始构建gc耗时就高可能是前者,这可以通过增加配置内存解决

如果是构建了很多次慢慢变卡,就可能是内存泄露导致的

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

  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文件
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这个文件记录了所有的BuildOperationgradle-to-trace-converter就是对它进行的分析

  1. 使用官方库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,存在兼容风险

参考资料

The Secrets of the Build Scan Plugin and the internals of Gradle by Nelson Osacky, Soundcloud EN - YouTube

相关推荐
SRC_BLUE_1726 分钟前
SQLI LABS | Less-39 GET-Stacked Query Injection-Intiger Based
android·网络安全·adb·less
无尽的大道4 小时前
Android打包流程图
android
镭封5 小时前
android studio 配置过程
android·ide·android studio
夜雨星辰4875 小时前
Android Studio 学习——整体框架和概念
android·学习·android studio
邹阿涛涛涛涛涛涛5 小时前
月之暗面招 Android 开发,大家快来投简历呀
android·人工智能·aigc
IAM四十二6 小时前
Jetpack Compose State 你用对了吗?
android·android jetpack·composer
奶茶喵喵叫6 小时前
Android开发中的隐藏控件技巧
android
Winston Wood8 小时前
Android中Activity启动的模式
android
众乐认证8 小时前
Android Auto 不再用于旧手机
android·google·智能手机·android auto
三杯温开水8 小时前
新的服务器Centos7.6 安卓基础的环境配置(新服务器可直接粘贴使用配置)
android·运维·服务器