Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add build time tracking to Mixpanel #303

Merged
merged 4 commits into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Experimental architecture app with example usage intended to be a showcase, test
- Tests are run on Firebase Test Lab. [See PR](https://github.com/jraska/github-client/pull/233)
- Release publishing by [Triple-T/google-play-publisher plugin](https://github.com/Triple-T/gradle-play-publisher)
- Enforced ownership of remote configuration and analytics events - [Details on PR](https://github.com/jraska/github-client/pull/230). More on why these need to be explicitly owned on [this article](https://proandroiddev.com/remote-feature-flags-do-not-always-come-for-free-a372f1768a70).
- Build time tracking with reporting to Mixpanel - see [this PR](https://github.com/jraska/github-client/pull/303).

1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
id "com.jraska.module.graph.assertion" version "1.4.0"
id "com.github.triplet.play" version "2.8.0"
id "com.jraska.github.client.firebase"
id 'com.jraska.gradle.buildtime'
}

apply plugin: 'com.android.application'
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ android.enableJetifier=true
kapt.incremental.apt=true
kapt.use.worker.api=true
org.gradle.caching=true
org.gradle.configureondemand=true

6 changes: 5 additions & 1 deletion firebasePlugin/build.gradle → plugins/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repositories {
dependencies {
implementation gradleApi()
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72"

implementation 'com.mixpanel:mixpanel-java:1.4.4'
}

compileKotlin {
Expand All @@ -37,5 +37,9 @@ gradlePlugin {
id = 'com.jraska.github.client.firebase'
implementationClass = 'com.jraska.github.client.firebase.FirebaseTestLabPlugin'
}
buildTime {
id = 'com.jraska.gradle.buildtime'
implementationClass = 'com.jraska.gradle.buildtime.BuildTimePlugin'
}
}
}
30 changes: 30 additions & 0 deletions plugins/src/main/java/com/jraska/gradle/buildtime/BuildData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.jraska.gradle.buildtime

data class BuildData(
val action: String,
val buildTime: Long,
val tasks: List<String>,
val failed: Boolean,
val failure: Throwable?,
val daemonsRunning: Int,
val thisDaemonBuilds: Int,
val hostname: String,
val gradleVersion: String,
val operatingSystem: String,
val environment: Environment,
val parameters: Map<String, Any>,
val taskStatistics: TaskStatistics
)

enum class Environment {
IDE,
CI,
CMD
}

data class TaskStatistics(
val total: Int,
val upToDate: Int,
val fromCache: Int,
val executed: Int
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.jraska.gradle.buildtime

import org.codehaus.groovy.runtime.ProcessGroovyMethods
import org.gradle.BuildResult
import org.gradle.api.internal.tasks.execution.statistics.TaskExecutionStatistics
import org.gradle.api.invocation.Gradle
import org.gradle.internal.buildevents.BuildStartedTime
import org.gradle.internal.time.Clock
import org.gradle.invocation.DefaultGradle
import org.gradle.launcher.daemon.server.scaninfo.DaemonScanInfo
import java.util.Locale

object BuildDataFactory {
fun buildData(result: BuildResult, statistics: TaskExecutionStatistics): BuildData {
val gradle = result.gradle as DefaultGradle
val services = gradle.services

val startTime = services[BuildStartedTime::class.java].startTime
val totalTime = services[Clock::class.java].currentTime - startTime

val daemonInfo = services[DaemonScanInfo::class.java]
val startParameter = gradle.startParameter

return BuildData(
action = result.action,
buildTime = totalTime,
failed = result.failure != null,
failure = result.failure,
daemonsRunning = daemonInfo.numberOfRunningDaemons,
thisDaemonBuilds = daemonInfo.numberOfBuilds,
hostname = hostname(),
tasks = startParameter.taskNames,
environment = gradle.environment(),
gradleVersion = gradle.gradleVersion,
operatingSystem = System.getProperty("os.name").toLowerCase(Locale.getDefault()),
parameters = mutableMapOf(
"isConfigureOnDemand" to startParameter.isConfigureOnDemand,
"isWatchFileSystem" to startParameter.isWatchFileSystem,
"isConfigurationCache" to startParameter.isConfigurationCache,
"isBuildCacheEnabled" to startParameter.isBuildCacheEnabled,
"maxWorkers" to startParameter.maxWorkerCount
).apply { putAll(startParameter.systemPropertiesArgs) },
taskStatistics = TaskStatistics(
statistics.totalTaskCount,
statistics.upToDateTaskCount,
statistics.fromCacheTaskCount,
statistics.executedTasksCount
)
)
}

private fun hostname(): String {
val process = Runtime.getRuntime().exec("hostname")
process.waitFor()
return ProcessGroovyMethods.getText(process).trim()
}

private fun Gradle.environment(): Environment {
return if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
Environment.IDE
} else if (System.getenv("CI") != null) {
Environment.CI
} else {
Environment.CMD
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.jraska.gradle.buildtime

interface BuildReporter {
fun report(buildData: BuildData)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jraska.gradle.buildtime

import org.gradle.BuildListener
import org.gradle.BuildResult
import org.gradle.api.initialization.Settings
import org.gradle.api.internal.tasks.execution.statistics.TaskExecutionStatisticsEventAdapter
import org.gradle.api.invocation.Gradle
import org.gradle.internal.event.ListenerManager
import org.gradle.invocation.DefaultGradle

internal class BuildTimeListener(
private val buildDataFactory: BuildDataFactory,
private val buildReporter: BuildReporter
) : BuildListener {
private val taskExecutionStatisticsEventAdapter = TaskExecutionStatisticsEventAdapter()

override fun buildStarted(gradle: Gradle) = Unit
override fun settingsEvaluated(gradle: Settings) = Unit
override fun projectsLoaded(gradle: Gradle) = Unit
override fun projectsEvaluated(gradle: Gradle) {
val listenerManager = (gradle as DefaultGradle).services[ListenerManager::class.java]
listenerManager.addListener(taskExecutionStatisticsEventAdapter)
}

override fun buildFinished(result: BuildResult) {
val buildData = buildDataFactory.buildData(result, taskExecutionStatisticsEventAdapter.statistics)
buildReporter.report(buildData)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.jraska.gradle.buildtime

import com.jraska.gradle.buildtime.report.ConsoleReporter
import com.jraska.gradle.buildtime.report.MixpanelReporter
import com.mixpanel.mixpanelapi.MixpanelAPI
import org.gradle.api.Plugin
import org.gradle.api.Project
import java.sql.DriverManager.println

class BuildTimePlugin : Plugin<Project> {
override fun apply(project: Project) {
val buildTimeListener = BuildTimeListener(BuildDataFactory, reporter())
project.gradle.addBuildListener(buildTimeListener)
}

private fun reporter(): BuildReporter {
val mixpanelToken: String? = System.getenv("GITHUB_CLIENT_MIXPANEL_API_KEY")
if (mixpanelToken == null) {
println("'GITHUB_CLIENT_MIXPANEL_API_KEY' not set, data will be reported to console only")
return ConsoleReporter()
} else {
return MixpanelReporter(mixpanelToken, MixpanelAPI())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.jraska.gradle.buildtime.report

import com.jraska.gradle.buildtime.BuildData
import com.jraska.gradle.buildtime.BuildReporter

class ConsoleReporter : BuildReporter {
override fun report(buildData: BuildData) {
println(buildData)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.jraska.gradle.buildtime.report

import com.jraska.gradle.buildtime.BuildData
import com.jraska.gradle.buildtime.BuildReporter
import com.mixpanel.mixpanelapi.ClientDelivery
import com.mixpanel.mixpanelapi.MessageBuilder
import com.mixpanel.mixpanelapi.MixpanelAPI
import org.json.JSONObject
import java.util.concurrent.TimeUnit

class MixpanelReporter(
private val apiKey: String,
private val api: MixpanelAPI
) : BuildReporter {
override fun report(buildData: BuildData) {
val start = nowMillis()

reportInternal(buildData)

val reportingOverhead = nowMillis() - start
println("$STOPWATCH_ICON Build time '${buildData.buildTime} ms' reported to Mixpanel in $reportingOverhead ms.$STOPWATCH_ICON")
}

private fun reportInternal(buildData: BuildData) {
val delivery = ClientDelivery()

val properties = convertBuildData(buildData)
val mixpanelEvent = MessageBuilder(apiKey)
.event(SINGLE_NAME_FOR_ONE_USER, "Android Build", JSONObject(properties))

delivery.addMessage(mixpanelEvent)

api.deliver(delivery)
}

private fun nowMillis() = TimeUnit.NANOSECONDS.toMillis(System.nanoTime())

private fun convertBuildData(buildData: BuildData): Map<String, Any?> {
return mutableMapOf<String, Any?>(
"action" to buildData.action,
"buildTime" to buildData.buildTime,
"tasks" to buildData.tasks.joinToString(),
"failed" to buildData.failed,
"failure" to buildData.failure,
"daemonsRunning" to buildData.daemonsRunning,
"thisDaemonBuilds" to buildData.thisDaemonBuilds,
"hostname" to buildData.hostname,
"gradleVersion" to buildData.gradleVersion,
"OS" to buildData.operatingSystem,
"environment" to buildData.environment,
"tasksTotal" to buildData.taskStatistics.total,
"tasksUpToDate" to buildData.taskStatistics.upToDate,
"tasksFromCache" to buildData.taskStatistics.fromCache,
"tasksExecuted" to buildData.taskStatistics.executed
).apply { putAll(buildData.parameters) }
}

companion object {
private val SINGLE_NAME_FOR_ONE_USER = "Build Time Plugin"
private val STOPWATCH_ICON = "\u23F1"
}
}
2 changes: 1 addition & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
includeBuild("firebasePlugin")
includeBuild("plugins")

include ':app',
':app-partial-users',
Expand Down