Skip to content

Commit 8dcac74

Browse files
committed
Migrated new APK size tool to OSS repository
1 parent 29ba4eb commit 8dcac74

File tree

32 files changed

+1082
-0
lines changed

32 files changed

+1082
-0
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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
17+
18+
import org.gradle.api.DefaultTask
19+
import org.gradle.api.Task
20+
import org.gradle.api.tasks.InputFile
21+
import org.gradle.api.tasks.OutputFile
22+
import org.gradle.api.tasks.TaskAction
23+
24+
/**
25+
* Generates size measurements after building the test apps and outputs them as a text-format
26+
* protocol buffer report.
27+
*
28+
* <p>This task requires two properties, an SDK map, as input, and the report, as output. The map is
29+
* 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 also required. This value will be placed in the report.
32+
*/
33+
public class GenerateMeasurementsTask extends DefaultTask {
34+
35+
/**
36+
* The file storing the SDK map.
37+
*
38+
* <p>This may be any type recognized by Gradle as a file. The format of the file's contents is
39+
* headerless CSVwith a colon as a delimiter: projectName_buildVariant:sdkId. The first column
40+
* contains both the project name and build variant separated by an hyphen, {@code
41+
* firestore-aggressive}, for example.
42+
*/
43+
@InputFile
44+
File sdkMap
45+
46+
/**
47+
* The file for storing the report.
48+
*
49+
* <p>This may be any type recognized by Gradle as a file. The contents, if any, will be
50+
* overwritten by the new report.
51+
*/
52+
@OutputFile
53+
File report
54+
55+
@Override
56+
Task configure(Closure closure) {
57+
project.android.variantFilter {
58+
if (it.buildType.name != "aggressive") {
59+
it.ignore = true;
60+
}
61+
}
62+
63+
outputs.upToDateWhen { false }
64+
dependsOn "assemble"
65+
return super.configure(closure)
66+
}
67+
68+
@TaskAction
69+
def generate() {
70+
def sdks = createSdkMap()
71+
def sizes = calculateSizes(sdks, project.android.applicationVariants)
72+
def report = createReport(sizes)
73+
74+
project.file(this.report).withWriter {
75+
it.write(report)
76+
}
77+
}
78+
79+
private def calculateSizes(sdks, variants) {
80+
def sizes = [:]
81+
82+
variants.each { variant ->
83+
def name = "${variant.flavorName}-${variant.buildType.name}"
84+
def apks = variant.outputs.findAll { it.outputFile.name.endsWith(".apk") }
85+
if (apks.size() > 1) {
86+
throw new IllegalStateException("${name} produced more than one APK")
87+
}
88+
89+
apks.each {
90+
def size = it.outputFile.size();
91+
def sdk = sdks[name];
92+
93+
if (sdk == null) {
94+
throw new IllegalStateException("$name not included in SDK map")
95+
}
96+
sizes[sdk] = size
97+
}
98+
}
99+
100+
return sizes
101+
}
102+
103+
// TODO(allisonbm): Remove hard-coding protocol buffers. This code manually generates the
104+
// text-format protocol buffer report. This eliminates requiring buildSrc to depend on the
105+
// uploader (or simply, the protocol buffer library), but this isn't the most scalable option.
106+
private def createReport(sizes) {
107+
def pullRequestNumber = project.properties["pull_request"]
108+
if (pullRequestNumber == null) {
109+
throw new IllegalStateException("`-Ppull_request` not defined")
110+
}
111+
112+
def sdkId = 0
113+
def apkSize = 0
114+
115+
def pullRequestGroup = """
116+
groups {
117+
table_name: "PullRequests"
118+
column_names: "pull_request_id"
119+
measurements {
120+
values {
121+
int_value: ${pullRequestNumber}
122+
}
123+
}
124+
}
125+
"""
126+
127+
def sizeGroupHeader = """
128+
groups {
129+
table_name: "ApkSizes"
130+
column_names: "sdk_id"
131+
column_names: "pull_request_id"
132+
column_names: "apk_size"
133+
"""
134+
135+
def sizeGroupEntry = """
136+
measurements {
137+
values {
138+
int_value: ${->sdkId}
139+
}
140+
values {
141+
int_value: ${pullRequestNumber}
142+
}
143+
values {
144+
int_value: ${->apkSize}
145+
}
146+
}
147+
"""
148+
149+
def sizeGroupFooter = """
150+
}
151+
"""
152+
153+
154+
def builder = new StringBuilder()
155+
builder.append(pullRequestGroup)
156+
builder.append(sizeGroupHeader)
157+
158+
sizes.each { key, value ->
159+
// sdkId and apkSize are lazily interpolated into sizeGroupEntry.
160+
sdkId = key
161+
apkSize = value
162+
builder.append(sizeGroupEntry)
163+
}
164+
165+
builder.append(sizeGroupFooter)
166+
return builder.toString()
167+
}
168+
169+
private def createSdkMap() {
170+
def map = [:]
171+
def path = project.file(sdkMap)
172+
173+
path.eachLine {
174+
def delimiter = it.indexOf(":")
175+
def key = it.substring(0, delimiter).trim()
176+
def value = it.substring(delimiter + 1).trim()
177+
map[key] = Integer.parseInt(value)
178+
}
179+
180+
return map
181+
}
182+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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
17+
18+
import java.net.URL
19+
import java.nio.file.Files
20+
import java.nio.file.StandardCopyOption
21+
import org.gradle.api.DefaultTask
22+
import org.gradle.api.Task
23+
import org.gradle.api.tasks.Input
24+
import org.gradle.api.tasks.InputFile
25+
import org.gradle.api.tasks.TaskAction
26+
27+
/**
28+
* Takes the size information created by {@link GenerateMeasurementTask} and uploads it to the
29+
* database using the uploader tool.
30+
*
31+
* <p>The uploader tool is fetched from the Internet using a URL. This URL, and the path to the
32+
* report to upload, must be given as properties to this task. This task also requires a project
33+
* property, {@code database_config} for connecting to the database. The format of this file is
34+
* dictated by the uploader tool.
35+
*/
36+
public class UploadMeasurementsTask extends DefaultTask {
37+
38+
/**
39+
* The URL of the uploader tool.
40+
*
41+
* <p>This must be a valid URL as a {@link String}.
42+
*/
43+
@Input
44+
String uploader
45+
46+
/**
47+
* The file to upload.
48+
*
49+
* <p>This file must exist prior to executing this task, but it may be created by other tasks
50+
* provided they run first.
51+
*/
52+
@InputFile
53+
File report
54+
55+
@TaskAction
56+
def upload() {
57+
if (!project.hasProperty("database_config")) {
58+
throw new IllegalStateException("Cannot upload measurements without database config")
59+
}
60+
61+
def configuration = project.file(project.properties["database_config"])
62+
63+
withTempJar { jar ->
64+
getUploaderUrl().withInputStream {
65+
Files.copy(it, jar, StandardCopyOption.REPLACE_EXISTING)
66+
}
67+
68+
project.exec {
69+
executable("java")
70+
71+
args(
72+
"-jar",
73+
jar,
74+
"--config_path=${configuration}",
75+
"--proto_path=${report}",
76+
)
77+
}.rethrowFailure()
78+
}
79+
}
80+
81+
def getUploaderUrl() {
82+
return new URL(uploader)
83+
}
84+
85+
private def withTempJar(Closure action) {
86+
def path = Files.createTempFile("uploader", ".jar")
87+
try {
88+
action.call(path)
89+
} finally {
90+
Files.delete(path);
91+
}
92+
}
93+
}

subprojects.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ firebase-database-collection
66
firebase-storage
77
protolite-well-known-types
88

9+
tools:apksize
910
tools:errorprone

tools/apksize/apksize.gradle

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
import com.google.firebase.gradle.GenerateMeasurementsTask
17+
import com.google.firebase.gradle.UploadMeasurementsTask
18+
19+
// Linting needs to be disabled as none of these apps are real or intended to conform to
20+
// linting standards.
21+
tasks.whenTaskAdded { task ->
22+
if (task.name.equals("lint")) {
23+
task.enabled = false
24+
}
25+
}
26+
27+
apply plugin: "com.android.application"
28+
android {
29+
flavorDimensions "apkSize"
30+
}
31+
32+
apply from: "default.gradle"
33+
apply from: "src/database/database.gradle"
34+
apply from: "src/storage/storage.gradle"
35+
apply from: "src/firestore/firestore.gradle"
36+
apply from: "src/functions/functions.gradle"
37+
38+
/**
39+
* This task builds all supported variants (only aggressive as of writing) and writes the
40+
* APK sizes to a text-format protocol buffer file.
41+
*
42+
* @param -Ppull_request the pull request number to be included in the report
43+
*/
44+
task generateMeasurements(type: GenerateMeasurementsTask) {
45+
sdkMap = file("sdks.csv")
46+
report = file("$buildDir/size-report.textpb")
47+
}
48+
49+
/**
50+
* This task uploads the report produced by the generate measurements task to a SQL database.
51+
*
52+
* @param -Pdatabase_config the file with the database configuration
53+
* @param -Ppull_request the pull request number to be included in the report
54+
*/
55+
task uploadMeasurements(type: UploadMeasurementsTask) {
56+
dependsOn generateMeasurements
57+
58+
report = file("$buildDir/size-report.textpb")
59+
uploader = "https://storage.googleapis.com/firebase-engprod-metrics/upload_tool.jar"
60+
}
61+
62+
apply plugin: "com.google.gms.google-services"

tools/apksize/debug.keystore

2.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)