/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import com.android.build.api.artifact.SingleArtifact import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.BuiltArtifact import com.android.build.api.variant.BuiltArtifactsLoader import com.android.build.gradle.AppPlugin import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.Internal import org.gradle.api.tasks.TaskAction import org.gradle.kotlin.dsl.withType import org.json.JSONArray import org.json.JSONObject import java.io.File import java.nio.file.Files /** * This plugin generates a [ApkSizeTask] for each variant of the target * project dependant on the APK output artifacts. */ class ApkSizePlugin : Plugin { override fun apply(project: Project) { project.plugins.withType().configureEach { val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) androidComponents.onVariants { variant -> val taskName = "apkSize${variant.name.replaceFirstChar { it.uppercase() }}" val task = project.tasks.register(taskName, ApkSizeTask::class.java) task.configure { artifactsDirectory.set(variant.artifacts.get(SingleArtifact.APK)) artifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader()) } } } } } /** * Gradle task for determining the size of APKs and logging them in a perfherder compatible format. */ abstract class ApkSizeTask : DefaultTask() { /** * Directory containing APKs that get built for the build variant. */ @get:InputDirectory abstract val artifactsDirectory: DirectoryProperty /** * The apk directory has a metadata file indicating relevant files * and this loader processes that to give us the all the splits the * form the abstract artifact. */ @get:Internal abstract val artifactsLoader: Property @TaskAction fun logApkSize() { val builtArtifacts = artifactsLoader.get().load(artifactsDirectory.get()) ?: throw RuntimeException("Cannot load APK metadata") val variantName = builtArtifacts.variantName.replaceFirstChar { it.uppercase() } val apkSizes = determineApkSizes(builtArtifacts.elements) val json = buildPerfherderJson(variantName, apkSizes) val isAutomation = System.getenv("MOZ_AUTOMATION") == "1" val uploadPath = System.getenv("MOZ_PERFHERDER_UPLOAD") if (isAutomation && uploadPath != null) { println("PERFHERDER_DATA: $json") val outputFile = File(uploadPath) outputFile.parentFile?.mkdirs() outputFile.writeText(json.toString()) } } private fun determineApkSizes(artifacts: Collection): Map { return artifacts.associate { val file = File(it.outputFile) file.name to Files.size(file.toPath()) } } /** * Returns perfherder compatible JSON for tracking the file size of APKs. * * ``` * { * "framework": { * "name": "build_metrics" * }, * "suites": [ * { * "name": "apk-size-[debug,nightly,beta,release]", * "lowerIsBetter": true, * "subtests": [ * { "name": "app-arm64-v8a-debug.apk", "value": 98855735 }, * { "name": "app-armeabi-v7a-debug.apk", "value": 92300031 }, * { "name": "app-x86-debug.apk", "value": 103410909 }, * { "name": "app-x86_64-debug.apk", "value": 102465675 } * ], * "value":98855735, * "shouldAlert":false * } * ] * } * ``` */ private fun buildPerfherderJson(variantName: String, apkSize: Map): JSONObject { val data = JSONObject() val framework = JSONObject() framework.put("name", "build_metrics") data.put("framework", framework) val suites = JSONArray() val suite = JSONObject() suite.put("name", "apk-size-$variantName") suite.put("value", getSummarySize(apkSize)) suite.put("lowerIsBetter", true) suite.put("alertChangeType", "absolute") suite.put("alertThreshold", 1024 * 1024) if (variantName.contains("debug", ignoreCase = true)) { suite.put("shouldAlert", false) } val subtests = JSONArray() apkSize.forEach { (apk, size) -> val subtest = JSONObject() subtest.put("name", apk) subtest.put("value", size) subtests.put(subtest) } suite.put("subtests", subtests) suites.put(suite) data.put("suites", suites) return data } } /** * Returns a summarized size for the APKs. This is the main value that is getting tracked. The size * of the individual APKs will be reported as "subtests". */ private fun getSummarySize(apkSize: Map): Long { val arm64size = apkSize.keys.find { it.contains("arm64") }?.let { apk -> apkSize[apk] } if (arm64size != null) { // If available we will report the size of the arm64 APK as the summary. This is the most // important and most installed APK. return arm64size } // If there's no arm64 APK then we calculate a simple average. return apkSize.values.sum() / apkSize.size }