Skip to content

Commit c4d3933

Browse files
authored
Added JSON support to APK sizing (#101)
* Modified the APK size tooling to produce a JSON file * Enabled linting for APK size tooling * Added tests for ApkSize*Builder classes. * Added Gradle integration tests
1 parent 82288ec commit c4d3933

File tree

11 files changed

+578
-122
lines changed

11 files changed

+578
-122
lines changed

buildSrc/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
plugins {
1616
id 'java-gradle-plugin'
17+
id 'groovy'
1718
}
1819

1920
repositories {
@@ -39,6 +40,7 @@ dependencies {
3940

4041
implementation 'com.android.tools.build:gradle:3.2.1'
4142
testImplementation 'junit:junit:4.12'
43+
testImplementation 'org.json:json:20180813'
4244
testImplementation('org.spockframework:spock-core:1.1-groovy-2.4') {
4345
exclude group: 'org.codehaus.groovy'
4446
}
@@ -65,4 +67,4 @@ test {
6567
showStandardStreams = true
6668
}
6769
enabled = rootProject.properties.get('enablePluginTests', false).toBoolean()
68-
}
70+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
package com.google.firebase.gradle.plugins.measurement
17+
18+
/** A helper class that generates the APK size measurement JSON report. */
19+
class ApkSizeJsonBuilder {
20+
21+
private static final String PULL_REQUEST_TABLE = "PullRequests"
22+
private static final String PULL_REQUEST_COLUMN = "pull_request_id"
23+
private static final String APK_SIZE_TABLE = "ApkSizes"
24+
private static final String SDK_COLUMN = "sdk_id"
25+
private static final String APK_SIZE_COLUMN = "apk_size"
26+
27+
// This comes in as a String and goes out as a String, so we might as well keep it a String
28+
private final String pullRequestNumber
29+
private final List<Tuple2<Integer, Integer>> sdkSizes
30+
31+
ApkSizeJsonBuilder(pullRequestNumber) {
32+
this.pullRequestNumber = pullRequestNumber
33+
this.sdkSizes = []
34+
}
35+
36+
def addApkSize(sdkId, size) {
37+
sdkSizes.add(new Tuple2(sdkId, size))
38+
}
39+
40+
def toJsonString() {
41+
if (sdkSizes.isEmpty()) {
42+
throw new IllegalStateException("No sizes were added")
43+
}
44+
45+
def sizes = sdkSizes.collect {
46+
"[$pullRequestNumber, $it.first, $it.second]"
47+
}.join(", ")
48+
49+
def json = """
50+
{
51+
tables: [
52+
{
53+
table_name: "$PULL_REQUEST_TABLE",
54+
column_names: ["$PULL_REQUEST_COLUMN"],
55+
replace_measurements: [[$pullRequestNumber]],
56+
},
57+
{
58+
table_name: "$APK_SIZE_TABLE",
59+
column_names: ["$PULL_REQUEST_COLUMN", "$SDK_COLUMN", "$APK_SIZE_COLUMN"],
60+
replace_measurements: [$sizes],
61+
},
62+
],
63+
}
64+
"""
65+
66+
return json
67+
}
68+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
package com.google.firebase.gradle.plugins.measurement
17+
18+
/** A helper class that generates the human-readable, APK size measurement table. */
19+
class ApkSizeTableBuilder {
20+
21+
private final List<Tuple> sdkSizes = []
22+
23+
def addApkSize(projectName, buildType, size) {
24+
sdkSizes.add(new Tuple(projectName, buildType, size))
25+
}
26+
27+
def toTableString() {
28+
if (sdkSizes.isEmpty()) {
29+
throw new IllegalStateException("No sizes added")
30+
}
31+
32+
def table = "|------------------ APK Sizes ------------------|\n"
33+
table += "|-- project --|-- build type --|-- size in bytes --|\n"
34+
35+
table += sdkSizes.collect {
36+
sprintf("|%-19s|%-19s|%-21s|", it.get(0), it.get(1), it.get(2))
37+
}.join("\n")
38+
39+
return table
40+
}
41+
}

buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/measurement/GenerateMeasurementsTask.groovy

Lines changed: 41 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,18 @@ import org.gradle.api.tasks.OutputFile
2222
import org.gradle.api.tasks.TaskAction
2323

2424
/**
25-
* Generates size measurements after building the test apps and outputs them as a text-format
26-
* protocol buffer report.
25+
* Generates size measurements after building the test apps.
26+
*
27+
* <p>This task can run in two modes. The first mode, is a dependency for {@link
28+
* UploadMeasurementsTask} and generates a JSON file with measurement information. This file
29+
* references database IDs, and is not considered to be human-friendly. The second mode outputs a
30+
* table to standard out with more useful information. These modes can be toggled by adding the
31+
* {@code pull_request} flag to the task. See the README for more details.
2732
*
2833
* <p>This task requires two properties, an SDK map, as input, and the report, as output. The map is
2934
* used to convert project names and build variants into the SDK identifiers used by the database.
30-
* The report path is where the output should be stored. Additionally, a project property, {@code
31-
* pull_request} is used in the report. Excluding this value will send a human-readable version
32-
* to standard out.
35+
* The report path is where the output should be stored. These properties are not used when the task
36+
* is run in the second, humna-friendly mode. However, they are still required to be specified.
3337
*/
3438
public class GenerateMeasurementsTask extends DefaultTask {
3539

@@ -76,59 +80,58 @@ public class GenerateMeasurementsTask extends DefaultTask {
7680

7781
@TaskAction
7882
def generate() {
79-
// Check if we need to run human-readable or upload mode.
83+
def variants = project.android.applicationVariants
84+
85+
// Check if we need to run human-readable table or JSON upload mode.
8086
if (project.hasProperty("pull_request")) {
8187
def pullRequestNumber = project.properties["pull_request"]
82-
def sdkMap = createSdkMap()
83-
def sizes = calculateSizesForUpload(sdkMap, project.android.applicationVariants)
84-
def report = createReportForUpload(pullRequestNumber, sizes)
85-
86-
reportFile.withWriter {
87-
it.write(report)
88-
}
88+
generateJson(pullRequestNumber, variants)
8989
} else {
90-
def sizes = calculateHumanReadableSizes(project.android.applicationVariants)
91-
printHumanReadableReport(sizes)
90+
generateTable(variants)
9291
}
9392
}
9493

95-
private def calculateHumanReadableSizes(variants) {
96-
def sizes = [:]
97-
def processor = {flavor, build, size ->
98-
sizes[new Tuple2(flavor, build)] = size
94+
private def generateJson(pullRequestNumber, variants) {
95+
def sdkMap = createSdkMap()
96+
def builder = new ApkSizeJsonBuilder(pullRequestNumber)
97+
def variantProcessor = {projectName, buildType, apkSize ->
98+
def name = "$projectName-$buildType"
99+
def sdkId = sdkMap[name]
100+
101+
if (sdkId == null) {
102+
throw new IllegalStateException("$name not included in SDK map")
103+
}
104+
105+
builder.addApkSize(sdkId, apkSize)
99106
}
100107

101-
calculateSizesFor(variants, processor)
102-
return sizes
108+
calculateSizes(variants, variantProcessor)
109+
reportFile.withWriter {
110+
it.write(builder.toJsonString())
111+
}
103112
}
104113

105-
private def calculateSizesForUpload(sdkMap, variants) {
106-
def sizes = [:]
107-
def processor = { flavor, build, size ->
108-
def name = "${flavor}-${build}"
109-
def sdk = sdkMap[name];
110-
111-
if (sdk == null) {
112-
throw new IllegalStateException("$name not included in SDK map")
113-
}
114-
sizes[sdk] = size
114+
private def generateTable(variants) {
115+
def builder = new ApkSizeTableBuilder()
116+
def variantProcessor = {projectName, buildType, apkSize ->
117+
builder.addApkSize(projectName, buildType, apkSize)
115118
}
116119

117-
calculateSizesFor(variants, processor)
118-
return sizes
120+
calculateSizes(variants, variantProcessor)
121+
project.logger.quiet(builder.toTableString())
119122
}
120123

121-
private def calculateSizesFor(variants, processor) {
124+
private def calculateSizes(variants, processor) {
122125
// Each variant should have exactly one APK. If there are multiple APKs, then this file is
123-
// out of sync with our Gradle configuration, and this task fails. If an APK is missing, it
124-
// is silently ignored, and the APKs from the other variants will be used to build the
125-
// report.
126+
// out of sync with our Gradle configuration, and this task fails. If an APK is missing, it
127+
// is silently ignored, and the APKs from the other variants will be used to build the
128+
// report.
126129
variants.each { variant ->
127130
def flavorName = variant.flavorName
128131
def buildType = variant.buildType.name
129132
def apks = variant.outputs.findAll { it.outputFile.name.endsWith(".apk") }
130133
if (apks.size() > 1) {
131-
def msg = "${flavorName}-${buildType} produced more than one APK"
134+
def msg = "${flavorName}-${buildType} produced more than one APK"
132135
throw new IllegalStateException(msg)
133136
}
134137

@@ -140,77 +143,6 @@ public class GenerateMeasurementsTask extends DefaultTask {
140143
}
141144
}
142145

143-
private def printHumanReadableReport(sizes) {
144-
project.logger.quiet("|------------------ APK Sizes ------------------|")
145-
project.logger.quiet("|-- project --|-- build type --|-- size in bytes --|")
146-
147-
sizes.each { key, value ->
148-
def line = sprintf("|%-19s|%-19s|%-21s|", key.first, key.second, value)
149-
project.logger.quiet(line)
150-
}
151-
}
152-
153-
// TODO(allisonbm): Remove hard-coding protocol buffers. This code manually generates the
154-
// text-format protocol buffer report. This eliminates requiring buildSrc to depend on the
155-
// uploader (or simply, the protocol buffer library), but this isn't the most scalable option.
156-
private def createReportForUpload(pullRequestNumber, sizes) {
157-
def sdkId = 0
158-
def apkSize = 0
159-
160-
def pullRequestGroup = """
161-
groups {
162-
table_name: "PullRequests"
163-
column_names: "pull_request_id"
164-
measurements {
165-
values {
166-
int_value: ${pullRequestNumber}
167-
}
168-
}
169-
}
170-
"""
171-
172-
def sizeGroupHeader = """
173-
groups {
174-
table_name: "ApkSizes"
175-
column_names: "sdk_id"
176-
column_names: "pull_request_id"
177-
column_names: "apk_size"
178-
"""
179-
180-
def sizeGroupEntry = """
181-
measurements {
182-
values {
183-
int_value: ${->sdkId}
184-
}
185-
values {
186-
int_value: ${pullRequestNumber}
187-
}
188-
values {
189-
int_value: ${->apkSize}
190-
}
191-
}
192-
"""
193-
194-
def sizeGroupFooter = """
195-
}
196-
"""
197-
198-
199-
def builder = new StringBuilder()
200-
builder.append(pullRequestGroup)
201-
builder.append(sizeGroupHeader)
202-
203-
sizes.each { key, value ->
204-
// sdkId and apkSize are lazily interpolated into sizeGroupEntry.
205-
sdkId = key
206-
apkSize = value
207-
builder.append(sizeGroupEntry)
208-
}
209-
210-
builder.append(sizeGroupFooter)
211-
return builder.toString()
212-
}
213-
214146
private def createSdkMap() {
215147
def map = [:]
216148

buildSrc/src/main/groovy/com/google/firebase/gradle/plugins/measurement/UploadMeasurementsTask.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public class UploadMeasurementsTask extends DefaultTask {
7272
"-jar",
7373
jar,
7474
"--config_path=${configuration}",
75-
"--proto_path=${reportFile}",
75+
"--json_path=${reportFile}",
7676
)
7777
}.rethrowFailure()
7878
}

0 commit comments

Comments
 (0)