ApkSizePlugin.kt (5211B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import org.gradle.api.DefaultTask 6 import org.gradle.api.Plugin 7 import org.gradle.api.Project 8 import org.gradle.api.tasks.Input 9 import org.gradle.api.tasks.TaskAction 10 import org.json.JSONArray 11 import org.json.JSONException 12 import org.json.JSONObject 13 import java.io.File 14 import java.nio.file.Files 15 import java.nio.file.Paths 16 17 class ApkSizePlugin : Plugin<Project> { 18 override fun apply(project: Project) = Unit 19 } 20 21 /** 22 * Gradle task for determining the size of APKs and logging them in a perfherder compatible format. 23 */ 24 open class ApkSizeTask : DefaultTask() { 25 /** 26 * Name of the build variant getting built. 27 */ 28 @Input 29 var variantName: String? = null 30 31 /** 32 * List of APKs that get build for the build variant. 33 */ 34 @Input 35 var apks: List<String>? = null 36 37 private val buildDir = project.layout.buildDirectory.get().asFile 38 39 @TaskAction 40 fun logApkSize() { 41 val apkSizes = determineApkSizes() 42 if (apkSizes.isEmpty()) { 43 println("Couldn't determine APK sizes for perfherder") 44 return 45 } 46 47 val json = buildPerfherderJson(apkSizes) ?: return 48 val isAutomation = System.getenv("MOZ_AUTOMATION") == "1" 49 val uploadPath = System.getenv("MOZ_PERFHERDER_UPLOAD") 50 if (isAutomation && uploadPath != null) { 51 println("PERFHERDER_DATA: $json") 52 val outputFile = File(uploadPath) 53 outputFile.parentFile?.mkdirs() 54 outputFile.writeText(json.toString()) 55 } 56 } 57 58 private fun determineApkSizes(): Map<String, Long> { 59 val variantOutputPath = variantName?.removePrefix("fenix")?.lowercase() 60 val basePath = listOf( 61 "$buildDir", "outputs", "apk", "fenix", variantOutputPath 62 ).joinToString(File.separator) 63 64 return requireNotNull(apks).associateWith { apk -> 65 val rawPath = "$basePath${File.separator}$apk" 66 67 try { 68 val path = Paths.get(rawPath) 69 Files.size(path) 70 } catch (t: Throwable) { 71 println("Could not determine size of $apk ($rawPath)") 72 t.printStackTrace() 73 0 74 } 75 }.filter { (_, size) -> size > 0 } 76 } 77 78 /** 79 * Returns perfherder compatible JSON for tracking the file size of APKs. 80 * 81 * ``` 82 * { 83 * "framework": { 84 * "name": "build_metrics" 85 * }, 86 * "suites": [ 87 * { 88 * "name": "apk-size-[debug,nightly,beta,release]", 89 * "lowerIsBetter": true, 90 * "subtests": [ 91 * { "name": "app-arm64-v8a-debug.apk", "value": 98855735 }, 92 * { "name": "app-armeabi-v7a-debug.apk", "value": 92300031 }, 93 * { "name": "app-x86-debug.apk", "value": 103410909 }, 94 * { "name": "app-x86_64-debug.apk", "value": 102465675 } 95 * ], 96 * "value":98855735, 97 * "shouldAlert":false 98 * } 99 * ] 100 * } 101 * ``` 102 */ 103 private fun buildPerfherderJson(apkSize: Map<String, Long>): JSONObject? { 104 return try { 105 val data = JSONObject() 106 107 val framework = JSONObject() 108 framework.put("name", "build_metrics") 109 data.put("framework", framework) 110 111 val suites = JSONArray() 112 113 val suite = JSONObject() 114 suite.put("name", "apk-size-$variantName") 115 suite.put("value", getSummarySize(apkSize)) 116 suite.put("lowerIsBetter", true) 117 suite.put("alertChangeType", "absolute") 118 suite.put("alertThreshold", 1024 * 1024) 119 120 // Debug variants do not have alerts 121 if (variantName?.contains("debug", ignoreCase = true) == true) { 122 suite.put("shouldAlert", false) 123 } 124 125 val subtests = JSONArray() 126 apkSize.forEach { (apk, size) -> 127 val subtest = JSONObject() 128 subtest.put("name", apk) 129 subtest.put("value", size) 130 subtests.put(subtest) 131 } 132 suite.put("subtests", subtests) 133 134 suites.put(suite) 135 136 data.put("suites", suites) 137 138 data 139 } catch (e: JSONException) { 140 println("Couldn't generate perfherder JSON") 141 e.printStackTrace() 142 null 143 } 144 } 145 } 146 147 /** 148 * Returns a summarized size for the APKs. This is the main value that is getting tracked. The size 149 * of the individual APKs will be reported as "subtests". 150 */ 151 private fun getSummarySize(apkSize: Map<String, Long>): Long { 152 val arm64size = apkSize.keys.find { it.contains("arm64") }?.let { apk -> apkSize[apk] } 153 if (arm64size != null) { 154 // If available we will report the size of the arm64 APK as the summary. This is the most 155 // important and most installed APK. 156 return arm64size 157 } 158 159 // If there's no arm64 APK then we calculate a simple average. 160 return apkSize.values.sum() / apkSize.size 161 }