gradle中如何统计每个task的执行时间 ?

一. 背景

假设我们有这样一个需求:
我们想要知道哪些task是耗时较多的, 这样就可以针对这些task进行优化, 以此来节省构建时间!

构建, 对于一个开发者来说, 是一个痛苦的等待过程, 相信开发者都深有体会.
当然对于安卓开发者来说, 已经有不少非常优秀的加速构建过程的工具了, 如: Instant Run, Freeline 等.
但是对于其他使用gradle作为构建工具的项目, 可能会缺少这样的工具, 因此会有自行优化构建过程的必要. 至于如后优化, 就留给读者发挥自己的智慧了. 下面主要讲讲如何收集构建过程的各个步骤所花费的时间.

二. 解决思路

1. 收集各个task所花费的时间

  • 此过程主要用到两个gradle的关键接口以及一个方法:
    • org.gradle.api.execution.TaskExecutionListener
      此接口定义了每个task执行前后的回调: beforeExecute()afterExecute()
    • org.gradle.BuildListener
      此接口主要定义了构建开始和构建完成的回调 (当然还有一些其他的回: 调配置完成, 所有项目加载完成等): buildStarted()buildFinished()
    • org.gradle.api.Project对象的gradle属性的addListener()方法
      1. 每个项目(父项目和子项目)都有自己的配置, 一般是用项目根目录下的build.gradle脚本来进行配置. 每个项目都会创建一个org.gradle.api.Project对象来代表该项目. Project对象中的gradle属性代表的是build.gradle脚本. gradle属性的类型是org.gradle.api.invocation.Gradle.
      2. 值得一提的是org.gradle.api.invocation.Gradle类的addListener()是一个比较特殊的方法, 它的参数是Object类型, 此方法的原型如下:
             /**
             * Adds the given listener to this build. The listener may implement any of the given listener interfaces:
             *
             * <ul>
             * <li>{@link org.gradle.BuildListener}
             * <li>{@link org.gradle.api.execution.TaskExecutionGraphListener}
             * <li>{@link org.gradle.api.ProjectEvaluationListener}
             * <li>{@link org.gradle.api.execution.TaskExecutionListener}
             * <li>{@link org.gradle.api.execution.TaskActionListener}
             * <li>{@link org.gradle.api.logging.StandardOutputListener}
             * <li>{@link org.gradle.api.tasks.testing.TestListener}
             * <li>{@link org.gradle.api.tasks.testing.TestOutputListener}
             * <li>{@link org.gradle.api.artifacts.DependencyResolutionListener}
             * </ul>
             *
             * @param listener The listener to add. Does nothing if this listener has already been added.
             */
            void addListener(Object listener);
      
      可以看到, 其可以添加的类型非常多. 上面我们使用的就是其中的两个.

2. 上传上一步收集的数据 (用于统计分析)

三. 具体实践

1. 在父项目的build.gradle文件中将http-builder-ng库加入到classpath中 (脚本文件中用的的类库必须添加到classpath中), 如下:
buildscript {
   repositories {
       google()
       jcenter()
   }
   dependencies {
       classpath 'com.android.tools.build:gradle:3.0.1'
       //添加http-builder-ng的依赖
       classpath 'io.github.http-builder-ng:http-builder-ng-core:1.0.3'
   }
}

如果用http-builder, classpath处改成:
classpath "org.codehaus.groovy.modules.http-builder:http-builder:0.7.2"

2. 在父项目的build.gradle文件顶部导入http-builder-ng相关的类 (上传数据时用到)

如下:
import groovyx.net.http.HttpBuilder
如果是http-builder, 导入语句如下:

import groovyx.net.http.HTTPBuilder
import static groovyx.net.http.ContentType.*
3. 在父项目的build.gradle内容底部, 自定义监听CollectTaskTimeListener, 如下:
class CollectTaskTimeListener implements TaskExecutionListener, BuildListener {
    private Clock clock                 //用于记录每个task执行所花的时间
    private Clock start = new Clock()   //用于记录所有task执行所花的时间
    private def timings = new HashMap<String, Long>() //存储所有task和其所发时间的对应关系
    private def final MIN_COST = 5      //展示统计数据的下限 (小于此值时不输出统计数据)

    //每个task执行之前调用
    @Override
    void beforeExecute(Task task) {
        clock = new Clock()
    }

    //每个task执行后调用
    @Override
    void afterExecute(Task task, TaskState state) {
        long ms = clock.timeInMs
        timings.put(task.path, ms)
        task.project.logger.warn "${task.path} took ${ms}ms"
    }

    //build结束时调用 (所有task结束时调用)
    @Override
    void buildFinished(BuildResult result) {
        //输出统计数据
        outputHeader("Task timings(no sort): ")
        outputProfile(timings.iterator())
        //输出排序后的统计数据
        outputHeader("Task timings(sorted): ")
        outputProfile(sortProfileData(timings).iterator())
        println("\n")
        uploadReport()
    }

    void outputHeader(String headerMessage) {
        println("\n======================================================")
        println(headerMessage)
    }

    //输出收集的数据
    void outputProfile(Iterator<Map.Entry<String, Long>> it) {
        for (entry in it) {
            if (entry.value >= MIN_COST) {
                printf("%-50s  %-15s\n", entry.key, entry.value + "ms")
            }
        }
    }

    //对task所花费的时间进行排序
    List<Map<String, Long>> sortProfileData(Map<String, Long> profileData) {
        List<Map.Entry<String, Long>> data = new ArrayList<>()
        for (timing in profileData) data.add(timing)
        Collections.sort(data, new Comparator<Map.Entry<String, Long>>() {
            @Override
            int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
                if (o1.value > o2.value) return 1
                else if (o1.value < o2.value) return -1
                return 0
            }
        })
        return data
    }

    //将收集的数据上传到服务器做分析
    //org.codehaus.groovy.modules.http-builder:http-builder:0.7.2
    /*
    void uploadReport() {
        def http = new HTTPBuilder('http://10.249.23.63:8080', "application/json")
        try {
            http.post(path: '/time', body: ['timings': timings, 'user.name': System.getProperty("user.name"), "total_time": start.timeInMs],
                    requestContentType: JSON) { resp ->
                println "POST Success: ${resp.statusLine}"
            }
        } catch (Exception e) {
            e.printStackTrace()
        }
    }*/

    //将收集的数据上传到服务器做分析
    //io.github.http-builder-ng:http-builder-ng-core:1.0.3
    void uploadReport() {
        HttpBuilder.configure {
            request.uri = "http://10.249.23.72:8080"
        }.postAsync {
            request.uri.path = '/time'
            request.body = ['timings': timings, 'user.name': System.getProperty("user.name"), "total_time": start.timeInMs]
            request.contentType = 'application/json'
            response.success { formServer, body -> //body => groovy.json.internal.LazyMap  (服务端相应类型Content-Type为application/json)
                println "POST Success: ${formServer.statusCode}, ${formServer.message}, ${body.getClass()}; code=${body.get('code')}, message=${body.get('message')}"
            }
            response.failure { formServer, errorMessage -> //errorMessage => byte[]
                println "POST Failure: ${formServer.statusCode}, ${formServer.message}, errorMessage=${new String(errorMessage)}"
            }
        }
    }

    @Override
    void buildStarted(Gradle gradle) {}

    @Override
    void settingsEvaluated(Settings settings) {}

    @Override
    void projectsLoaded(Gradle gradle) {}

    @Override
    void projectsEvaluated(Gradle gradle) {}
}
4. 在上一步定义的CollectTaskTimeListener的最后面, 将自定义的监听添加到gradle对象中, 如下:
//添加自定义的监听
gradle.addListener(new CollectTaskTimeListener())
完整build.gradle文件内容如下:
import groovyx.net.http.HttpBuilder

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath 'io.github.http-builder-ng:http-builder-ng-core:1.0.3'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

class CollectTaskTimeListener implements TaskExecutionListener, BuildListener {
    private Clock clock                 //用于记录每个task执行所花的时间
    private Clock start = new Clock()   //用于记录所有task执行所花的时间
    private def timings = new HashMap<String, Long>() //存储所有task和其所发时间的对应关系
    private def final MIN_COST = 5      //展示统计数据的下限 (小于此值时不输出统计数据)

    //每个task执行之前调用
    @Override
    void beforeExecute(Task task) {
        clock = new Clock()
    }

    //每个task执行后调用
    @Override
    void afterExecute(Task task, TaskState state) {
        long ms = clock.timeInMs
        timings.put(task.path, ms)
        //输出当前task的执行时间
        task.project.logger.warn "${task.path} took ${ms}ms"
    }

    //build结束时调用 (所有task结束时调用)
    @Override
    void buildFinished(BuildResult result) {
        //输出统计数据
        outputHeader("Task timings(no sort): ")
        outputProfile(timings.iterator())
        //输出排序后的统计数据
        outputHeader("Task timings(sorted): ")
        outputProfile(sortProfileData(timings).iterator())
        println("\n")
        uploadReport()
    }

    void outputHeader(String headerMessage) {
        println("\n======================================================")
        println(headerMessage)
    }

    //输出收集的数据
    void outputProfile(Iterator<Map.Entry<String, Long>> it) {
        for (entry in it) {
            if (entry.value >= MIN_COST) {
                printf("%-50s  %-15s\n", entry.key, entry.value + "ms")
            }
        }
    }

    //对task所花费的时间进行排序
    List<Map<String, Long>> sortProfileData(Map<String, Long> profileData) {
        List<Map.Entry<String, Long>> data = new ArrayList<>()
        for (timing in profileData) data.add(timing)
        Collections.sort(data, new Comparator<Map.Entry<String, Long>>() {
            @Override
            int compare(Map.Entry<String, Long> o1, Map.Entry<String, Long> o2) {
                if (o1.value > o2.value) return 1
                else if (o1.value < o2.value) return -1
                return 0
            }
        })
        return data
    }

    //将收集的数据上传到服务器做分析 (http-builder数据上传代码)
    /*
    void uploadReport() {
        def http = new HTTPBuilder('http://10.249.23.63:8080', "application/json")
        try {
            http.post(path: '/time', body: ['timings': timings, 'user.name': System.getProperty("user.name"), "total_time": start.timeInMs],
                    requestContentType: JSON) { resp ->
                println "POST Success: ${resp.statusLine}"
            }
        } catch (Exception e) {
            e.printStackTrace()
        }
    }*/

    //将收集的数据上传到服务器做分析 (http-builder-ng)
    void uploadReport() {
        HttpBuilder.configure {
            request.uri = "http://10.249.23.72:8080"
        }.postAsync {
            request.uri.path = '/time'
            request.body = ['timings': timings, 'user.name': System.getProperty("user.name"), "total_time": start.timeInMs]
            request.contentType = 'application/json'
            response.success { formServer, body -> //body => groovy.json.internal.LazyMap  (服务端相应类型Content-Type为application/json)
                println "POST Success: ${formServer.statusCode}, ${formServer.message}, ${body.getClass()}; code=${body.get('code')}, message=${body.get('message')}"
            }
            response.failure { formServer, errorMessage -> //errorMessage => byte[]
                println "POST Failure: ${formServer.statusCode}, ${formServer.message}, errorMessage=${new String(errorMessage)}"
            }
        }
    }

    @Override
    void buildStarted(Gradle gradle) {}

    @Override
    void settingsEvaluated(Settings settings) {}

    @Override
    void projectsLoaded(Gradle gradle) {}

    @Override
    void projectsEvaluated(Gradle gradle) {}
}

//添加自定义的监听
gradle.addListener(new CollectTaskTimeListener())

References:

Gradle:
https://docs.gradle.org/4.3.1/userguide/userguide.html
https://docs.gradle.org/4.3.1/dsl/
https://docs.gradle.org/4.3.1/javadoc/
http-builder-ng:
https://http-builder-ng.github.io/http-builder-ng/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355

推荐阅读更多精彩内容