-
Notifications
You must be signed in to change notification settings - Fork 617
Migrated new APK size tool to OSS repository #74
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
f86ea3a
Migrated new APK size tool to OSS repository
allisonbm92 f98fcae
Fixed formatting issues
allisonbm92 81a14e8
Added google-services jsons
allisonbm92 9770750
Various changes due to Ashwin's comments
allisonbm92 0be5d8f
Removed google-services files
allisonbm92 85db524
Updated Gradle file to use provided google-services
allisonbm92 60419cd
Added README for APK sizing
allisonbm92 55942f7
Added human readable mode to generator
allisonbm92 614b78e
Updated README and fixed formatting issues
allisonbm92 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
226 changes: 226 additions & 0 deletions
226
buildSrc/src/main/groovy/com/google/firebase/gradle/GenerateMeasurementsTask.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
// Copyright 2018 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
|
||
package com.google.firebase.gradle | ||
|
||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.Task | ||
import org.gradle.api.tasks.InputFile | ||
import org.gradle.api.tasks.OutputFile | ||
import org.gradle.api.tasks.TaskAction | ||
|
||
/** | ||
* Generates size measurements after building the test apps and outputs them as a text-format | ||
* protocol buffer report. | ||
* | ||
* <p>This task requires two properties, an SDK map, as input, and the report, as output. The map is | ||
* used to convert project names and build variants into the SDK identifiers used by the database. | ||
* The report path is where the output should be stored. Additionally, a project property, {@code | ||
* pull_request} is used in the report. Excluding this value will send a human-readable version | ||
* to standard out. | ||
*/ | ||
public class GenerateMeasurementsTask extends DefaultTask { | ||
|
||
/** | ||
* The file storing the SDK map. | ||
* | ||
* <p>This may be any type recognized by Gradle as a file. The format of the file's contents is | ||
* headerless CSV with a colon as a delimiter: projectName-buildVariant:sdkId. The first column | ||
* contains both the project name and build variant separated by an hyphen. The SDK ID is the | ||
* integer identifier used by the SQL database to represent this SDK and build variant pair. | ||
* | ||
* <p>A complete example follows: | ||
* <pre>{@code | ||
* database-debug:1 | ||
* database-release:2 | ||
* firestore-release:7 | ||
* firestore-debug:4 | ||
*}</pre> | ||
*/ | ||
@InputFile | ||
File sdkMapFile | ||
|
||
/** | ||
* The file for storing the report. | ||
* | ||
* <p>This may be any type recognized by Gradle as a file. The contents, if any, will be | ||
* overwritten by the new report. | ||
*/ | ||
@OutputFile | ||
File reportFile | ||
|
||
@Override | ||
Task configure(Closure closure) { | ||
project.android.variantFilter { | ||
if (it.buildType.name != "aggressive") { | ||
it.ignore = true; | ||
} | ||
} | ||
|
||
outputs.upToDateWhen { false } | ||
dependsOn "assemble" | ||
return super.configure(closure) | ||
} | ||
|
||
@TaskAction | ||
def generate() { | ||
// Check if we need to run human-readable or upload mode. | ||
if (project.hasProperty("pull_request")) { | ||
def pullRequestNumber = project.properties["pull_request"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Formatting wierdness There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. My editor seems to be throwing tabs in... :( |
||
def sdkMap = createSdkMap() | ||
def sizes = calculateSizesForUpload(sdkMap, project.android.applicationVariants) | ||
def report = createReportForUpload(pullRequestNumber, sizes) | ||
|
||
reportFile.withWriter { | ||
it.write(report) | ||
} | ||
} else { | ||
def sizes = calculateHumanReadableSizes(project.android.applicationVariants) | ||
printHumanReadableReport(sizes) | ||
} | ||
} | ||
|
||
private def calculateHumanReadableSizes(variants) { | ||
def sizes = [:] | ||
def processor = {flavor, build, size -> | ||
sizes[new Tuple2(flavor, build)] = size | ||
} | ||
ashwinraghav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
calculateSizesFor(variants, processor) | ||
return sizes | ||
} | ||
|
||
private def calculateSizesForUpload(sdkMap, variants) { | ||
def sizes = [:] | ||
def processor = { flavor, build, size -> | ||
def name = "${flavor}-${build}" | ||
def sdk = sdkMap[name]; | ||
|
||
if (sdk == null) { | ||
throw new IllegalStateException("$name not included in SDK map") | ||
} | ||
sizes[sdk] = size | ||
} | ||
|
||
calculateSizesFor(variants, processor) | ||
return sizes | ||
} | ||
|
||
private def calculateSizesFor(variants, processor) { | ||
// Each variant should have exactly one APK. If there are multiple APKs, then this file is | ||
// out of sync with our Gradle configuration, and this task fails. If an APK is missing, it | ||
// is silently ignored, and the APKs from the other variants will be used to build the | ||
// report. | ||
variants.each { variant -> | ||
def flavorName = variant.flavorName | ||
def buildType = variant.buildType.name | ||
def apks = variant.outputs.findAll { it.outputFile.name.endsWith(".apk") } | ||
if (apks.size() > 1) { | ||
def msg = "${flavorName}-${buildType} produced more than one APK" | ||
throw new IllegalStateException(msg) | ||
} | ||
|
||
// This runs at most once, as each variant at this point has zero or one APK. | ||
apks.each { | ||
def size = it.outputFile.size() | ||
processor.call(flavorName, buildType, size) | ||
} | ||
} | ||
} | ||
|
||
private def printHumanReadableReport(sizes) { | ||
project.logger.quiet("|------------------ APK Sizes ------------------|") | ||
project.logger.quiet("|-- project --|-- build type --|-- size in bytes --|") | ||
|
||
sizes.each { key, value -> | ||
def line = sprintf("|%-19s|%-19s|%-21s|", key.first, key.second, value) | ||
project.logger.quiet(line) | ||
} | ||
} | ||
|
||
// TODO(allisonbm): Remove hard-coding protocol buffers. This code manually generates the | ||
// text-format protocol buffer report. This eliminates requiring buildSrc to depend on the | ||
// uploader (or simply, the protocol buffer library), but this isn't the most scalable option. | ||
private def createReportForUpload(pullRequestNumber, sizes) { | ||
def sdkId = 0 | ||
def apkSize = 0 | ||
|
||
def pullRequestGroup = """ | ||
groups { | ||
table_name: "PullRequests" | ||
column_names: "pull_request_id" | ||
measurements { | ||
values { | ||
int_value: ${pullRequestNumber} | ||
} | ||
} | ||
} | ||
""" | ||
|
||
def sizeGroupHeader = """ | ||
groups { | ||
table_name: "ApkSizes" | ||
column_names: "sdk_id" | ||
column_names: "pull_request_id" | ||
column_names: "apk_size" | ||
""" | ||
|
||
def sizeGroupEntry = """ | ||
measurements { | ||
values { | ||
int_value: ${->sdkId} | ||
} | ||
values { | ||
int_value: ${pullRequestNumber} | ||
} | ||
values { | ||
int_value: ${->apkSize} | ||
} | ||
} | ||
""" | ||
|
||
def sizeGroupFooter = """ | ||
} | ||
""" | ||
|
||
|
||
def builder = new StringBuilder() | ||
builder.append(pullRequestGroup) | ||
builder.append(sizeGroupHeader) | ||
|
||
sizes.each { key, value -> | ||
// sdkId and apkSize are lazily interpolated into sizeGroupEntry. | ||
allisonbm92 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sdkId = key | ||
apkSize = value | ||
builder.append(sizeGroupEntry) | ||
} | ||
|
||
builder.append(sizeGroupFooter) | ||
return builder.toString() | ||
} | ||
|
||
private def createSdkMap() { | ||
def map = [:] | ||
|
||
sdkMapFile.eachLine { | ||
def delimiter = it.indexOf(":") | ||
def key = it.substring(0, delimiter).trim() | ||
def value = it.substring(delimiter + 1).trim() | ||
map[key] = Integer.parseInt(value) | ||
} | ||
|
||
return map | ||
} | ||
} |
93 changes: 93 additions & 0 deletions
93
buildSrc/src/main/groovy/com/google/firebase/gradle/UploadMeasurementsTask.groovy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Copyright 2018 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
|
||
package com.google.firebase.gradle | ||
|
||
import java.net.URL | ||
import java.nio.file.Files | ||
import java.nio.file.StandardCopyOption | ||
import org.gradle.api.DefaultTask | ||
import org.gradle.api.Task | ||
import org.gradle.api.tasks.Input | ||
import org.gradle.api.tasks.InputFile | ||
import org.gradle.api.tasks.TaskAction | ||
|
||
/** | ||
* Takes the size information created by {@link GenerateMeasurementTask} and uploads it to the | ||
* database using the uploader tool. | ||
* | ||
* <p>The uploader tool is fetched from the Internet using a URL. This URL, and the path to the | ||
ashwinraghav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* report to upload, must be given as properties to this task. This task also requires a project | ||
* property, {@code database_config} for connecting to the database. The format of this file is | ||
* dictated by the uploader tool. | ||
*/ | ||
public class UploadMeasurementsTask extends DefaultTask { | ||
|
||
/** | ||
* The URL of the uploader tool. | ||
* | ||
* <p>This must be a valid URL as a {@link String}. | ||
*/ | ||
@Input | ||
String uploader | ||
|
||
/** | ||
* The file to upload. | ||
* | ||
* <p>This file must exist prior to executing this task, but it may be created by other tasks | ||
* provided they run first. | ||
*/ | ||
@InputFile | ||
File reportFile | ||
|
||
@TaskAction | ||
def upload() { | ||
if (!project.hasProperty("database_config")) { | ||
throw new IllegalStateException("Cannot upload measurements without database config") | ||
} | ||
|
||
def configuration = project.file(project.properties["database_config"]) | ||
|
||
withTempJar { jar -> | ||
getUploaderUrl().withInputStream { | ||
Files.copy(it, jar, StandardCopyOption.REPLACE_EXISTING) | ||
} | ||
|
||
project.exec { | ||
executable("java") | ||
|
||
args( | ||
"-jar", | ||
jar, | ||
"--config_path=${configuration}", | ||
"--proto_path=${reportFile}", | ||
) | ||
}.rethrowFailure() | ||
} | ||
} | ||
|
||
def getUploaderUrl() { | ||
return new URL(uploader) | ||
} | ||
|
||
private def withTempJar(Closure action) { | ||
def path = Files.createTempFile("uploader", ".jar") | ||
try { | ||
action.call(path) | ||
} finally { | ||
Files.delete(path); | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,4 +6,5 @@ firebase-database-collection | |
firebase-storage | ||
protolite-well-known-types | ||
|
||
tools:apksize | ||
tools:errorprone |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# APK Size Tooling | ||
|
||
## Purpose | ||
|
||
This tooling measures the size of APKs using Firebase. The APKs are simple apps | ||
that exercise only a small faction of the API surface. These numbers help to | ||
show how an app's size might grow if Firebase is included. | ||
|
||
## How to Use | ||
|
||
There are two tasks defined in this subproject: generateMeasurements and | ||
uploadMeasurements. The former gathers the measurements and writes them to a | ||
file in the build directory. The latter is invoked by CI and uploads the report | ||
to an SQL database. | ||
|
||
The generateMeasurements task may be manually run with `./gradlew -q | ||
generateMeasurements`. This will output a human readable report to standard out. | ||
Appending `-Ppull_request=999` will instead generate the report to upload, where | ||
`999` is the pull request number to place in the report. | ||
|
||
The uploadMeasurements task is not intended to be invoked manually. However, it | ||
may be invoked with the above pull request flag and `-Pdatabase_config=path` | ||
where `path` is the path to the config file. The config file must have the | ||
following structure where the values in all-caps are placeholders for the | ||
relevant pieces of configuration: | ||
|
||
``` | ||
host:HOST | ||
database:DATABASE | ||
user:USER | ||
password:PASSWORD | ||
``` | ||
|
||
## Current Support | ||
|
||
All projects in this repository are supported with an aggressive ProGuard | ||
profile. Less aggressive ProGuard profiles will be added at a future date. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.