blob: d6226be4a5dd2d1cc82176203280da0073ffe931 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
import argparse
import hashlib
import os
import shutil
import sys
import time
import zipfile
from datetime import datetime
import adb
import apk_masseur
import as_utils
import compiledump
import gradle
import jdk
import thread_utils
from thread_utils import print_thread
import update_prebuilds_in_android
import utils
GOLEM_BUILD_TARGETS = [utils.GRADLE_TASK_R8LIB]
SHRINKERS = ['r8', 'r8-full', 'r8-nolib', 'r8-nolib-full']
class AttrDict(dict):
def __getattr__(self, name):
return self.get(name, None)
# To generate the files for a new app, navigate to the app source folder and
# run:
# ./gradlew clean :app:assembleRelease -Dcom.android.tools.r8.dumpinputtodirectory=<path>
# and store the dump and the apk.
# If the app has instrumented tests, adding `testBuildType "release"` and
# running:
# ./gradlew assembleAndroidTest -Dcom.android.tools.r8.dumpinputtodirectory=<path>
# will also generate dumps and apk for tests.
class App(object):
def __init__(self, fields):
defaults = {
'id': None,
'name': None,
'collections': [],
'dump_app': None,
'apk_app': None,
'dump_test': None,
'apk_test': None,
'skip': False,
'url': None, # url is not used but nice to have for updating apps
'revision': None,
'folder': None,
'skip_recompilation': False,
'compiler_properties': [],
'internal': False,
'golem_duration': None,
}
# This below does not work in python3
defaults.update(fields.items())
self.__dict__ = defaults
class AppCollection(object):
def __init__(self, fields):
defaults = {'name': None}
# This below does not work in python3
defaults.update(fields.items())
self.__dict__ = defaults
APPS = [
App({
'id':
'com.numix.calculator',
'name':
'Calculator',
'dump_app':
'dump_app.zip',
'apk_app':
'app-release.apk',
# Compiling tests fail: Library class android.content.res.XmlResourceParser
# implements program class org.xmlpull.v1.XmlPullParser. Nothing to really
# do about that.
'id_test':
'com.numix.calculator.test',
'dump_test':
'dump_test.zip',
'apk_test':
'app-release-androidTest.apk',
'url':
'https://github.com/numixproject/android-suite/tree/master/Calculator',
'revision':
'f58e1b53f7278c9b675d5855842c6d8a44cccb1f',
'folder':
'android-suite-calculator',
}),
App({
'id': 'dev.dworks.apps.anexplorer.pro',
'name': 'AnExplorer',
'dump_app': 'dump_app.zip',
'apk_app': 'AnExplorer-googleMobileProRelease-4.0.3.apk',
'url': 'https://github.com/christofferqa/AnExplorer',
'revision': '365927477b8eab4052a1882d5e358057ae3dee4d',
'folder': 'anexplorer',
}),
App({
'id': 'de.danoeh.antennapod',
'name': 'AntennaPod',
'dump_app': 'dump_app.zip',
'apk_app': 'app-free-release.apk',
# TODO(b/172452102): Tests and monkey do not work
'id_test': 'de.danoeh.antennapod.test',
'dump_test': 'dump_test.zip',
'apk_test': 'app-free-release-androidTest.apk',
'url': 'https://github.com/christofferqa/AntennaPod.git',
'revision': '77e94f4783a16abe9cc5b78dc2d2b2b1867d8c06',
'folder': 'antennapod',
}),
App({
'id': 'com.example.applymapping',
'name': 'applymapping',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
'id_test': 'com.example.applymapping.test',
'dump_test': 'dump_test.zip',
'apk_test': 'app-release-androidTest.apk',
'url': 'https://github.com/mkj-gram/applymapping',
'revision': 'e3ae14b8c16fa4718e5dea8f7ad00937701b3c48',
'folder': 'applymapping',
}),
App({
'id':
'com.chanapps.four.activity',
'name':
'chanu',
'dump_app':
'dump_app.zip',
'apk_app':
'app-release.apk',
'url':
'https://github.com/mkj-gram/chanu.git',
'revision':
'6e53458f167b6d78398da60c20fd0da01a232617',
'folder':
'chanu',
# The app depends on a class file that has access flags interface but
# not abstract
'compiler_properties': [
'-Dcom.android.tools.r8.allowInvalidCfAccessFlags=true'
]
}),
App({
'id': 'com.example.myapplication',
'name': 'empty-activity',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
'url': 'https://github.com/christofferqa/empty_android_activity.git',
'revision': '2d297ec3373dadb03cbae916b9feba4792563156',
'folder': 'empty-activity',
}),
App({
'id':
'com.example.emptycomposeactivity',
'name':
'empty-compose-activity',
'dump_app':
'dump_app.zip',
'apk_app':
'app-release.apk',
'url':
'https://github.com/christofferqa/empty_android_compose_activity.git',
'revision':
'3c8111b8b7d6e9184049a07e2b96702d7b33d03e',
'folder':
'empty-compose-activity',
}),
# TODO(b/172539375): Monkey runner fails on recompilation.
App({
'id': 'com.google.firebase.example.fireeats',
'name': 'FriendlyEats',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release-unsigned.apk',
'url': 'https://github.com/firebase/friendlyeats-android',
'revision': '7c6dd016fc31ea5ecb948d5166b8479efc3775cc',
'folder': 'friendlyeats',
}),
App({
'id': 'com.google.samples.apps.sunflower',
'name': 'Sunflower',
'dump_app': 'dump_app.zip',
'apk_app': 'app-debug.apk',
# TODO(b/172549283): Compiling tests fails
'id_test': 'com.google.samples.apps.sunflower.test',
'dump_test': 'dump_test.zip',
'apk_test': 'app-debug-androidTest.apk',
'url': 'https://github.com/android/sunflower',
'revision': '0c4c88fdad2a74791199dffd1a6559559b1dbd4a',
'folder': 'sunflower',
}),
# TODO(b/172565385): Monkey runner fails on recompilation
App({
'id': 'com.google.samples.apps.iosched',
'name': 'iosched',
'dump_app': 'dump_app.zip',
'apk_app': 'mobile-release.apk',
'url': 'https://github.com/christofferqa/iosched.git',
'revision': '581cbbe2253711775dbccb753cdb53e7e506cb02',
'folder': 'iosched',
}),
App({
'id': 'fr.neamar.kiss',
'name': 'KISS',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
# TODO(b/172569220): Running tests fails due to missing keep rules
'id_test': 'fr.neamar.kiss.test',
'dump_test': 'dump_test.zip',
'apk_test': 'app-release-androidTest.apk',
'url': 'https://github.com/Neamar/KISS',
'revision': '8ccffaadaf0d0b8fc4418ed2b4281a0935d3d971',
'folder': 'kiss',
}),
# TODO(b/172577344): Monkey runner not working.
App({
'id': 'io.github.hidroh.materialistic',
'name': 'materialistic',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
'url': 'https://github.com/christofferqa/materialistic.git',
'revision': '2b2b2ee25ce9e672d5aab1dc90a354af1522b1d9',
'folder': 'materialistic',
}),
App({
'id': 'com.avjindersinghsekhon.minimaltodo',
'name': 'MinimalTodo',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
'url': 'https://github.com/christofferqa/Minimal-Todo',
'revision': '9d8c73746762cd376b718858ec1e8783ca07ba7c',
'folder': 'minimal-todo',
}),
App({
'id': 'net.nurik.roman.muzei',
'name': 'muzei',
'dump_app': 'dump_app.zip',
'apk_app': 'muzei-release.apk',
'url': 'https://github.com/romannurik/muzei',
'revision': '9eac6e98aebeaf0ae40bdcd85f16dd2886551138',
'folder': 'muzei',
}),
# TODO(b/172806281): Monkey runner does not work.
App({
'id': 'org.schabi.newpipe',
'name': 'NewPipe',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release-unsigned.apk',
'url': 'https://github.com/TeamNewPipe/NewPipe',
'revision': 'f4435f90313281beece70c544032f784418d85fa',
'folder': 'newpipe',
}),
# TODO(b/172806808): Monkey runner does not work.
App({
'id': 'io.rover.app.debug',
'name': 'Rover',
'dump_app': 'dump_app.zip',
'apk_app': 'example-app-release-unsigned.apk',
'url': 'https://github.com/RoverPlatform/rover-android',
'revision': '94342117097770ea3ca2c6df6ab496a1a55c3ce7',
'folder': 'rover-android',
}),
# TODO(b/172808159): Monkey runner does not work
App({
'id': 'com.google.android.apps.santatracker',
'name': 'SantaTracker',
'dump_app': 'dump_app.zip',
'apk_app': 'santa-tracker-release.apk',
'url': 'https://github.com/christofferqa/santa-tracker-android',
'revision': '8dee74be7d9ee33c69465a07088c53087d24a6dd',
'folder': 'santa-tracker',
}),
App({
'id': 'org.thoughtcrime.securesms',
'name': 'Signal',
'dump_app': 'dump_app.zip',
'apk_app': 'Signal-Android-play-prod-universal-release-4.76.2.apk',
# TODO(b/172812839): Instrumentation test fails.
'id_test': 'org.thoughtcrime.securesms.test',
'dump_test': 'dump_test.zip',
'apk_test': 'Signal-Android-play-prod-release-androidTest.apk',
'url': 'https://github.com/signalapp/Signal-Android',
'revision': '91ca19f294362ccee2c2b43c247eba228e2b30a1',
'folder': 'signal-android',
'golem_duration': 300
}),
# TODO(b/172815827): Monkey runner does not work
App({
'id': 'com.simplemobiletools.calendar.pro',
'name': 'Simple-Calendar',
'dump_app': 'dump_app.zip',
'apk_app': 'calendar-release.apk',
'url': 'https://github.com/SimpleMobileTools/Simple-Calendar',
'revision': '906209874d0a091c7fce5a57972472f272d6b068',
'folder': 'simple-calendar',
}),
# TODO(b/172815534): Monkey runner does not work
App({
'id': 'com.simplemobiletools.camera.pro',
'name': 'Simple-Camera',
'dump_app': 'dump_app.zip',
'apk_app': 'camera-release.apk',
'url': 'https://github.com/SimpleMobileTools/Simple-Camera',
'revision': 'ebf9820c51e960912b3238287e30a131244fdee6',
'folder': 'simple-camera',
}),
App({
'id': 'com.simplemobiletools.filemanager.pro',
'name': 'Simple-File-Manager',
'dump_app': 'dump_app.zip',
'apk_app': 'file-manager-release.apk',
'url': 'https://github.com/SimpleMobileTools/Simple-File-Manager',
'revision': '2b7fa68ea251222cc40cf6d62ad1de260a6f54d9',
'folder': 'simple-file-manager',
}),
App({
'id': 'com.simplemobiletools.gallery.pro',
'name': 'Simple-Gallery',
'dump_app': 'dump_app.zip',
'apk_app': 'gallery-326-foss-release.apk',
'url': 'https://github.com/SimpleMobileTools/Simple-Gallery',
'revision': '564e56b20d33b28d0018c8087ec705beeb60785e',
'folder': 'simple-gallery',
}),
App({
'id': 'com.example.sqldelight.hockey',
'name': 'SQLDelight',
'dump_app': 'dump_app.zip',
'apk_app': 'android-release.apk',
'url': 'https://github.com/christofferqa/sqldelight',
'revision': '2e67a1126b6df05e4119d1e3a432fde51d76cdc8',
'folder': 'sqldelight',
}),
# TODO(b/172824096): Monkey runner does not work.
App({
'id': 'eu.kanade.tachiyomi',
'name': 'Tachiyomi',
'dump_app': 'dump_app.zip',
'apk_app': 'app-dev-release.apk',
'url': 'https://github.com/inorichi/tachiyomi',
'revision': '8aa6486bf76ab9a61a5494bee284b1a5e9180bf3',
'folder': 'tachiyomi',
}),
# TODO(b/172862042): Monkey runner does not work.
App({
'id': 'app.tivi',
'name': 'Tivi',
'dump_app': 'dump_app.zip',
'apk_app': 'app-release.apk',
'url': 'https://github.com/chrisbanes/tivi',
'revision': '5c6d9ed338885c59b1fc64050d92d056417bb4de',
'folder': 'tivi',
'golem_duration': 300
}),
App({
'id': 'com.keylesspalace.tusky',
'name': 'Tusky',
'dump_app': 'dump_app.zip',
'apk_app': 'app-blue-release.apk',
'url': 'https://github.com/tuskyapp/Tusky',
'revision': '814a9b8f9bacf8d26f712b06a0313a3534a2be95',
'folder': 'tusky',
}),
App({
'id': 'org.wikipedia',
'name': 'Wikipedia',
'dump_app': 'dump_app.zip',
'apk_app': 'app-prod-release.apk',
'url': 'https://github.com/wikimedia/apps-android-wikipedia',
'revision': '0fa7cad843c66313be8e25790ef084cf1a1fa67e',
'folder': 'wikipedia',
})
]
APP_COLLECTIONS = []
def remove_print_lines(file):
with open(file) as f:
lines = f.readlines()
with open(file, 'w') as f:
for line in lines:
if '-printconfiguration' not in line:
f.write(line)
def download_sha(app_sha, internal, quiet=False):
if internal:
utils.DownloadFromX20(app_sha)
else:
utils.DownloadFromGoogleCloudStorage(app_sha, quiet=quiet)
def is_logging_enabled_for(app, options):
if options.no_logging:
return False
if options.app_logging_filter and app.name not in options.app_logging_filter:
return False
return True
def is_minified_r8(shrinker):
return '-nolib' not in shrinker
def is_full_r8(shrinker):
return '-full' in shrinker
def version_is_built_jar(version):
return version != 'main' and version != 'source'
def compute_size_of_dex_files_in_package(path):
dex_size = 0
z = zipfile.ZipFile(path, 'r')
for filename in z.namelist():
if filename.endswith('.dex'):
dex_size += z.getinfo(filename).file_size
return dex_size
def dump_for_app(app_dir, app):
return os.path.join(app_dir, app.dump_app)
def dump_test_for_app(app_dir, app):
return os.path.join(app_dir, app.dump_test)
def get_r8_jar(options, temp_dir, shrinker):
if (options.version == 'source'):
return None
jar = os.path.abspath(
os.path.join(temp_dir, '..',
'r8lib.jar' if is_minified_r8(shrinker) else 'r8.jar'))
return jar
def get_results_for_app(app, options, temp_dir, worker_id):
app_folder = app.folder if app.folder else app.name + "_" + app.revision
# Golem extraction will extract to the basename under the benchmarks dir.
app_location = os.path.basename(app_folder) if options.golem else app_folder
opensource_basedir = (os.path.join('benchmarks', app.name)
if options.golem else utils.OPENSOURCE_DUMPS_DIR)
app_dir = (os.path.join(utils.INTERNAL_DUMPS_DIR, app_location) if
app.internal else os.path.join(opensource_basedir, app_location))
if not os.path.exists(app_dir) and not options.golem:
# Download the app from google storage.
download_sha(app_dir + ".tar.gz.sha1", app.internal)
# Ensure that the dumps are in place
assert os.path.isfile(dump_for_app(app_dir, app)), "Could not find dump " \
"for app " + app.name
result = {}
result['status'] = 'success'
result_per_shrinker = build_app_with_shrinkers(app,
options,
temp_dir,
app_dir,
worker_id=worker_id)
for shrinker, shrinker_result in result_per_shrinker.items():
result[shrinker] = shrinker_result
return result
def build_app_with_shrinkers(app, options, temp_dir, app_dir, worker_id):
result_per_shrinker = {}
for shrinker in options.shrinker:
results = []
build_app_and_run_with_shrinker(app,
options,
temp_dir,
app_dir,
shrinker,
results,
worker_id=worker_id)
result_per_shrinker[shrinker] = results
if len(options.apps) > 1:
print_thread('', worker_id)
log_results_for_app(app,
result_per_shrinker,
options,
worker_id=worker_id)
print_thread('', worker_id)
return result_per_shrinker
def is_last_build(index, compilation_steps):
return index == compilation_steps - 1
def build_app_and_run_with_shrinker(app, options, temp_dir, app_dir, shrinker,
results, worker_id):
print_thread(
'[{}] Building {} with {}'.format(datetime.now().strftime("%H:%M:%S"),
app.name, shrinker), worker_id)
print_thread(
'To compile locally: '
'tools/run_on_app_dump.py --shrinker {} --r8-compilation-steps {} '
'--app {} --minify {} --optimize {} --shrink {}'.format(
shrinker, options.r8_compilation_steps, app.name, options.minify,
options.optimize, options.shrink), worker_id)
print_thread(
'HINT: use --shrinker r8-nolib --no-build if you have a local R8.jar',
worker_id)
recomp_jar = None
status = 'success'
if options.r8_compilation_steps < 1:
return
compilation_steps = 1 if app.skip_recompilation else options.r8_compilation_steps
for compilation_step in range(0, compilation_steps):
if status != 'success':
break
print_thread(
'Compiling {} of {}'.format(compilation_step + 1,
compilation_steps), worker_id)
result = {}
try:
start = time.time()
(app_jar, mapping, new_recomp_jar) = \
build_app_with_shrinker(
app, options, temp_dir, app_dir, shrinker, compilation_step,
compilation_steps, recomp_jar, worker_id=worker_id)
end = time.time()
dex_size = compute_size_of_dex_files_in_package(app_jar)
result['build_status'] = 'success'
result['recompilation_status'] = 'success'
result['output_jar'] = app_jar
result['output_mapping'] = mapping
result['dex_size'] = dex_size
result['duration'] = int((end - start) * 1000) # Wall time
if (new_recomp_jar is None and
not is_last_build(compilation_step, compilation_steps)):
result['recompilation_status'] = 'failed'
warn('Failed to build {} with {}'.format(app.name, shrinker))
recomp_jar = new_recomp_jar
except Exception as e:
warn('Failed to build {} with {}'.format(app.name, shrinker))
if e:
print_thread('Error: ' + str(e), worker_id)
result['build_status'] = 'failed'
status = 'failed'
original_app_apk = os.path.join(app_dir, app.apk_app)
app_apk_destination = os.path.join(
temp_dir, "{}_{}.apk".format(app.id, compilation_step))
if result.get('build_status') == 'success' and options.monkey:
# Make a copy of the given APK, move the newly generated dex files into the
# copied APK, and then sign the APK.
apk_masseur.masseur(original_app_apk,
dex=app_jar,
resources='META-INF/services/*',
out=app_apk_destination,
quiet=options.quiet,
logging=is_logging_enabled_for(app, options),
keystore=options.keystore)
result['monkey_status'] = 'success' if adb.run_monkey(
app.id, options.emulator_id, app_apk_destination,
options.monkey_events, options.quiet,
is_logging_enabled_for(app, options)) else 'failed'
if (result.get('build_status') == 'success' and options.run_tests and
app.dump_test):
if not os.path.isfile(app_apk_destination):
apk_masseur.masseur(original_app_apk,
dex=app_jar,
resources='META-INF/services/*',
out=app_apk_destination,
quiet=options.quiet,
logging=is_logging_enabled_for(
app, options),
keystore=options.keystore)
# Compile the tests with the mapping file.
test_jar = build_test_with_shrinker(app, options, temp_dir, app_dir,
shrinker, compilation_step,
result['output_mapping'])
if not test_jar:
result['instrumentation_test_status'] = 'compilation_failed'
else:
original_test_apk = os.path.join(app_dir, app.apk_test)
test_apk_destination = os.path.join(
temp_dir, "{}_{}.test.apk".format(app.id_test,
compilation_step))
apk_masseur.masseur(original_test_apk,
dex=test_jar,
resources='META-INF/services/*',
out=test_apk_destination,
quiet=options.quiet,
logging=is_logging_enabled_for(
app, options),
keystore=options.keystore)
result[
'instrumentation_test_status'] = 'success' if adb.run_instrumented(
app.id, app.id_test, options.emulator_id,
app_apk_destination,
test_apk_destination, options.quiet,
is_logging_enabled_for(app, options)) else 'failed'
results.append(result)
if result.get('recompilation_status') != 'success':
break
def get_jdk_home(options, app):
if options.golem:
return os.path.join('benchmarks', app.name, 'linux')
return None
def build_app_with_shrinker(app, options, temp_dir, app_dir, shrinker,
compilation_step_index, compilation_steps,
prev_recomp_jar, worker_id):
def config_files_consumer(files):
for file in files:
compiledump.clean_config(file, options)
remove_print_lines(file)
properties = app.compiler_properties
if options.dump_input_to_directory:
properties.append(
'-Dcom.android.tools.r8.dumpinputtodirectory=%s'
% options.dump_input_to_directory)
args = AttrDict({
'dump': dump_for_app(app_dir, app),
'r8_jar': get_r8_jar(options, temp_dir, shrinker),
'r8_flags': options.r8_flags,
'disable_assertions': options.disable_assertions,
'version': options.version,
'compiler': 'r8full' if is_full_r8(shrinker) else 'r8',
'debug_agent': options.debug_agent,
'program_jar': prev_recomp_jar,
'nolib': not is_minified_r8(shrinker),
'config_files_consumer': config_files_consumer,
'properties': properties,
'disable_desugared_lib': False,
'print_times': options.print_times,
'java_opts': [],
})
app_jar = os.path.join(
temp_dir, '{}_{}_{}_dex_out.jar'.format(app.name, shrinker,
compilation_step_index))
app_mapping = os.path.join(
temp_dir, '{}_{}_{}_dex_out.jar.map'.format(app.name, shrinker,
compilation_step_index))
recomp_jar = None
jdkhome = get_jdk_home(options, app)
with utils.TempDir() as compile_temp_dir:
compile_result = compiledump.run1(compile_temp_dir,
args, [],
jdkhome,
worker_id=worker_id)
out_jar = os.path.join(compile_temp_dir, "out.jar")
out_mapping = os.path.join(compile_temp_dir, "out.jar.map")
if compile_result != 0 or not os.path.isfile(out_jar):
assert False, 'Compilation of {} failed'.format(
dump_for_app(app_dir, app))
shutil.move(out_jar, app_jar)
shutil.move(out_mapping, app_mapping)
if compilation_step_index < compilation_steps - 1:
args['classfile'] = True
args['min_api'] = "10000"
args['disable_desugared_lib'] = True
compile_result = compiledump.run1(compile_temp_dir, args, [],
jdkhome)
if compile_result == 0:
recomp_jar = os.path.join(
temp_dir,
'{}_{}_{}_cf_out.jar'.format(app.name, shrinker,
compilation_step_index))
shutil.move(out_jar, recomp_jar)
return (app_jar, app_mapping, recomp_jar)
def build_test_with_shrinker(app, options, temp_dir, app_dir, shrinker,
compilation_step_index, mapping):
def rewrite_files(files):
add_applymapping = True
for file in files:
compiledump.clean_config(file, options)
remove_print_lines(file)
with open(file) as f:
lines = f.readlines()
with open(file, 'w') as f:
for line in lines:
if '-applymapping' not in line:
f.write(line + '\n')
if add_applymapping:
f.write("-applymapping " + mapping + '\n')
add_applymapping = False
args = AttrDict({
'dump': dump_test_for_app(app_dir, app),
'r8_jar': get_r8_jar(options, temp_dir, shrinker),
'disable_assertions': options.disable_assertions,
'version': options.version,
'compiler': 'r8full' if is_full_r8(shrinker) else 'r8',
'debug_agent': options.debug_agent,
'nolib': not is_minified_r8(shrinker),
# The config file will have an -applymapping reference to an old map.
# Update it to point to mapping file build in the compilation of the app.
'config_files_consumer': rewrite_files,
})
test_jar = os.path.join(
temp_dir, '{}_{}_{}_test_out.jar'.format(app.name, shrinker,
compilation_step_index))
with utils.TempDir() as compile_temp_dir:
jdkhome = get_jdk_home(options, app)
compile_result = compiledump.run1(compile_temp_dir, args, [], jdkhome)
out_jar = os.path.join(compile_temp_dir, "out.jar")
if compile_result != 0 or not os.path.isfile(out_jar):
return None
shutil.move(out_jar, test_jar)
return test_jar
def log_results_for_apps(result_per_shrinker_per_app, options):
print('')
app_errors = 0
for (app, result_per_shrinker) in result_per_shrinker_per_app:
app_errors += (1 if log_results_for_app(app, result_per_shrinker,
options) else 0)
return app_errors
def log_results_for_app(app, result_per_shrinker, options, worker_id=None):
if options.print_dexsegments:
log_segments_for_app(app,
result_per_shrinker,
options,
worker_id=worker_id)
return False
else:
return log_comparison_results_for_app(app,
result_per_shrinker,
options,
worker_id=worker_id)
def log_segments_for_app(app, result_per_shrinker, options, worker_id):
for shrinker in SHRINKERS:
if shrinker not in result_per_shrinker:
continue
for result in result_per_shrinker.get(shrinker):
benchmark_name = '{}-{}'.format(options.print_dexsegments, app.name)
utils.print_dexsegments(benchmark_name, [result.get('output_jar')],
worker_id=worker_id)
duration = result.get('duration')
print_thread(
'%s-Total(RunTimeRaw): %s ms' % (benchmark_name, duration),
worker_id)
print_thread(
'%s-Total(CodeSize): %s' %
(benchmark_name, result.get('dex_size')), worker_id)
def percentage_diff_as_string(before, after):
if after < before:
return '-' + str(round((1.0 - after / before) * 100)) + '%'
else:
return '+' + str(round((after - before) / before * 100)) + '%'
def log_comparison_results_for_app(app, result_per_shrinker, options,
worker_id):
print_thread(app.name + ':', worker_id)
app_error = False
if result_per_shrinker.get('status', 'success') != 'success':
error_message = result_per_shrinker.get('error_message')
print_thread(' skipped ({})'.format(error_message), worker_id)
return
proguard_result = result_per_shrinker.get('pg', {})
proguard_dex_size = float(proguard_result.get('dex_size', -1))
for shrinker in SHRINKERS:
if shrinker not in result_per_shrinker:
continue
compilation_index = 1
for result in result_per_shrinker.get(shrinker):
build_status = result.get('build_status')
if build_status != 'success' and build_status is not None:
app_error = True
warn(' {}-#{}: {}'.format(shrinker, compilation_index,
build_status))
continue
if options.golem:
print_thread(
'%s(RunTimeRaw): %s ms' %
(app.name, result.get('duration')), worker_id)
print_thread(
'%s(CodeSize): %s' % (app.name, result.get('dex_size')),
worker_id)
continue
print_thread(' {}-#{}:'.format(shrinker, compilation_index),
worker_id)
dex_size = result.get('dex_size')
msg = ' dex size: {}'.format(dex_size)
if options.print_runtimeraw:
print_thread(
' run time raw: {} ms'.format(result.get('duration')),
worker_id)
if dex_size != proguard_dex_size and proguard_dex_size >= 0:
msg = '{} ({}, {})'.format(
msg, dex_size - proguard_dex_size,
percentage_diff_as_string(proguard_dex_size, dex_size))
success(msg) if dex_size < proguard_dex_size else warn(msg)
else:
print_thread(msg, worker_id)
if options.monkey:
monkey_status = result.get('monkey_status')
if monkey_status != 'success':
app_error = True
warn(' monkey: {}'.format(monkey_status))
else:
success(' monkey: {}'.format(monkey_status))
if options.run_tests and 'instrumentation_test_status' in result:
test_status = result.get('instrumentation_test_status')
if test_status != 'success':
warn(' instrumentation_tests: {}'.format(test_status))
else:
success(' instrumentation_tests: {}'.format(test_status))
recompilation_status = result.get('recompilation_status', '')
if recompilation_status == 'failed':
app_error = True
warn(' recompilation {}-#{}: failed'.format(
shrinker, compilation_index))
continue
compilation_index += 1
return app_error
def parse_options(argv):
result = argparse.ArgumentParser(description='Run/compile dump artifacts.')
result.add_argument('--app',
help='What app to run on',
choices=[app.name for app in APPS],
action='append')
result.add_argument(
'--app-collection',
'--app_collection',
help='What app collection to run',
choices=[collection.name for collection in APP_COLLECTIONS],
action='append')
result.add_argument('--app-logging-filter',
'--app_logging_filter',
help='The apps for which to turn on logging',
action='append')
result.add_argument('--bot',
help='Running on bot, use third_party dependency.',
default=False,
action='store_true')
result.add_argument('--generate-golem-config',
'--generate_golem_config',
help='Generate a new config for golem.',
default=False,
action='store_true')
result.add_argument('--debug-agent',
help='Enable Java debug agent and suspend compilation '
'(default disabled)',
default=False,
action='store_true')
result.add_argument(
'--disable-assertions',
'--disable_assertions',
'-da',
help='Disable Java assertions when running the compiler '
'(default enabled)',
default=False,
action='store_true')
result.add_argument(
'--dump-input-to-directory',
'--dump_input_to_directory',
help='Dump all compilations to directory',
default=None)
result.add_argument('--emulator-id',
'--emulator_id',
help='Id of the emulator to use',
default='emulator-5554')
result.add_argument('--golem',
help='Running on golem, do not download',
default=False,
action='store_true')
result.add_argument('--hash', help='The commit of R8 to use')
result.add_argument(
'--internal',
help='Run internal apps if set, otherwise run opensource',
default=False,
action='store_true')
result.add_argument('--keystore',
help='Path to app.keystore',
default=os.path.join(utils.TOOLS_DIR, 'debug.keystore'))
result.add_argument('--keystore-password',
'--keystore_password',
help='Password for app.keystore',
default='android')
result.add_argument('--minify',
help='Force enable/disable minification' +
' (defaults to app proguard config)',
choices=['default', 'force-enable', 'force-disable'],
default='default')
result.add_argument('--monkey',
help='Whether to install and run app(s) with monkey',
default=False,
action='store_true')
result.add_argument('--monkey-events',
'--monkey_events',
help='Number of events that the monkey should trigger',
default=250,
type=int)
result.add_argument('--no-build',
'--no_build',
help='Run without building first (only when using ToT)',
default=False,
action='store_true')
result.add_argument('--no-logging',
'--no_logging',
help='Disable logging except for errors',
default=False,
action='store_true')
result.add_argument('--optimize',
help='Force enable/disable optimizations' +
' (defaults to app proguard config)',
choices=['default', 'force-enable', 'force-disable'],
default='default')
result.add_argument('--print-times',
help='Print timing information from r8',
default=False,
action='store_true')
result.add_argument('--print-dexsegments',
metavar='BENCHMARKNAME',
help='Print the sizes of individual dex segments as ' +
'\'<BENCHMARKNAME>-<APP>-<segment>(CodeSize): '
'<bytes>\'')
result.add_argument('--print-runtimeraw',
metavar='BENCHMARKNAME',
help='Print the line \'<BENCHMARKNAME>(RunTimeRaw):' +
' <elapsed> ms\' at the end where <elapsed> is' +
' the elapsed time in milliseconds.')
result.add_argument('--quiet',
help='Disable verbose logging',
default=False,
action='store_true')
result.add_argument('--r8-compilation-steps',
'--r8_compilation_steps',
help='Number of times R8 should be run on each app',
default=2,
type=int)
result.add_argument('--r8-flags',
'--r8_flags',
help='Additional option(s) for the compiler.')
result.add_argument('--run-tests',
'--run_tests',
help='Whether to run instrumentation tests',
default=False,
action='store_true')
result.add_argument('--shrink',
help='Force enable/disable shrinking' +
' (defaults to app proguard config)',
choices=['default', 'force-enable', 'force-disable'],
default='default')
result.add_argument('--sign-apks',
'--sign_apks',
help='Whether the APKs should be signed',
default=False,
action='store_true')
result.add_argument('--shrinker',
help='The shrinkers to use (by default, all are run)',
action='append')
result.add_argument('--temp',
help='A directory to use for temporaries and outputs.',
default=None)
result.add_argument('--version',
default='main',
help='The version of R8 to use (e.g., 1.4.51)')
result.add_argument('--workers',
help='Number of workers to use',
default=1,
type=int)
(options, args) = result.parse_known_args(argv)
if options.app or options.app_collection:
if not options.app:
options.app = []
if not options.app_collection:
options.app_collection = []
options.apps = [
app for app in APPS if app.name in options.app or any(
collection in options.app_collection
for collection in app.collections)
]
del options.app
del options.app_collection
else:
options.apps = [app for app in APPS if app.internal == options.internal]
if options.app_logging_filter:
for app_name in options.app_logging_filter:
assert any(app.name == app_name for app in options.apps)
if options.shrinker:
for shrinker in options.shrinker:
assert shrinker in SHRINKERS, ('Shrinker must be one of %s' %
', '.join(SHRINKERS))
else:
options.shrinker = [shrinker for shrinker in SHRINKERS]
if options.hash or version_is_built_jar(options.version):
# No need to build R8 if a specific version should be used.
options.no_build = True
if 'r8-nolib' in options.shrinker:
warn('Skipping shrinker r8-nolib because a specific version ' +
'of r8 was specified')
options.shrinker.remove('r8-nolib')
if 'r8-nolib-full' in options.shrinker:
warn('Skipping shrinker r8-nolib-full because a specific version ' +
'of r8 was specified')
options.shrinker.remove('r8-nolib-full')
return (options, args)
def print_indented(s, indent):
print(' ' * indent + s)
def get_sha256(gz_file):
with open(gz_file, 'rb') as f:
bytes = f.read() # read entire file as bytes
return hashlib.sha256(bytes).hexdigest()
def get_sha_from_file(sha_file):
with open(sha_file, 'r') as f:
return f.readlines()[0]
def print_golem_config(options):
print('// AUTOGENERATED FILE from tools/run_on_app_dump.py in R8 repo')
print('part of r8_config;')
print('')
print('final Suite dumpsSuite = Suite("OpenSourceAppDumps");')
print('')
print('createOpenSourceAppBenchmarks() {')
print_indented('final cpus = ["Lenovo M90"];', 2)
print_indented('final targetsCompat = ["R8"];', 2)
print_indented('final targetsFull = ["R8-full-minify-optimize-shrink"];', 2)
# Avoid calculating this for every app
jdk_gz = jdk.GetJdkHome() + '.tar.gz'
add_golem_resource(2, jdk_gz, 'openjdk')
for app in options.apps:
if app.folder and not app.internal:
indentation = 2
print_indented('{', indentation)
indentation = 4
print_indented('final name = "%s";' % app.name, indentation)
print_indented('final benchmark =', indentation)
print_indented(
'StandardBenchmark(name, [Metric.RunTimeRaw, Metric.CodeSize]);',
indentation + 4)
if app.golem_duration != None:
print_indented(
'final timeout = const Duration(seconds: %s);' %
app.golem_duration, indentation)
print_indented(
'ExecutionManagement.addTimeoutConstraint'
'(timeout, benchmark: benchmark);', indentation)
app_gz = os.path.join(utils.OPENSOURCE_DUMPS_DIR,
app.folder + '.tar.gz')
name = 'appResource'
add_golem_resource(indentation, app_gz, name)
print_golem_config_target('Compat', 'r8', app, indentation)
print_golem_config_target('Full',
'r8-full',
app,
indentation,
minify='force-enable',
optimize='force-enable',
shrink='force-enable')
print_indented('dumpsSuite.addBenchmark(name);', indentation)
indentation = 2
print_indented('}', indentation)
print('}')
def print_golem_config_target(target,
shrinker,
app,
indentation,
minify='default',
optimize='default',
shrink='default'):
options = "options" + target
print_indented(
'final %s = benchmark.addTargets(noImplementation, targets%s);' %
(options, target), indentation)
print_indented('%s.cpus = cpus;' % options, indentation)
print_indented('%s.isScript = true;' % options, indentation)
print_indented('%s.fromRevision = 9700;' % options, indentation)
print_indented('%s.mainFile = "tools/run_on_app_dump.py "' % options,
indentation)
print_indented(
'"--golem --disable-assertions --quiet --shrinker %s --app %s "' %
(shrinker, app.name), indentation + 4)
print_indented(
'"--minify %s --optimize %s --shrink %s";' % (minify, optimize, shrink),
indentation + 4)
print_indented('%s.resources.add(appResource);' % options, indentation)
print_indented('%s.resources.add(openjdk);' % options, indentation)
def add_golem_resource(indentation, gz, name, sha256=None):
sha = gz + '.sha1'
if not sha256:
# Golem uses a sha256 of the file in the cache, and you need to specify that.
download_sha(sha, False, quiet=True)
sha256 = get_sha256(gz)
sha = get_sha_from_file(sha)
print_indented('final %s = BenchmarkResource("",' % name, indentation)
print_indented('type: BenchmarkResourceType.storage,', indentation + 4)
print_indented('uri: "gs://r8-deps/%s",' % sha, indentation + 4)
# Make dart formatter happy.
if indentation > 2:
print_indented('hash:', indentation + 4)
print_indented('"%s",' % sha256, indentation + 8)
else:
print_indented('hash: "%s",' % sha256, indentation + 4)
print_indented('extract: "gz");', indentation + 4)
def main(argv):
(options, args) = parse_options(argv)
if options.bot:
options.no_logging = True
options.shrinker = ['r8', 'r8-full']
print(options.shrinker)
if options.golem:
options.disable_assertions = True
options.no_build = True
options.r8_compilation_steps = 1
options.quiet = True
options.no_logging = True
if options.generate_golem_config:
print_golem_config(options)
return 0
with utils.TempDir() as temp_dir:
if options.temp:
temp_dir = options.temp
os.makedirs(temp_dir, exist_ok=True)
if options.hash:
# Download r8-<hash>.jar from
# https://storage.googleapis.com/r8-releases/raw/.
target = 'r8-{}.jar'.format(options.hash)
update_prebuilds_in_android.download_hash(
temp_dir, 'com/android/tools/r8/' + options.hash, target)
as_utils.MoveFile(os.path.join(temp_dir, target),
os.path.join(temp_dir, 'r8lib.jar'),
quiet=options.quiet)
elif version_is_built_jar(options.version):
# Download r8-<version>.jar from
# https://storage.googleapis.com/r8-releases/raw/.
target = 'r8-{}.jar'.format(options.version)
update_prebuilds_in_android.download_version(
temp_dir, 'com/android/tools/r8/' + options.version, target)
as_utils.MoveFile(os.path.join(temp_dir, target),
os.path.join(temp_dir, 'r8lib.jar'),
quiet=options.quiet)
elif options.version == 'main':
if not options.no_build:
gradle.RunGradle([
utils.GRADLE_TASK_R8,
'-Pno_internal'
])
build_r8lib = False
for shrinker in options.shrinker:
if is_minified_r8(shrinker):
build_r8lib = True
if build_r8lib:
gradle.RunGradle([utils.GRADLE_TASK_R8LIB, '-Pno_internal'])
# Make a copy of r8.jar and r8lib.jar such that they stay the same for
# the entire execution of this script.
if 'r8-nolib' in options.shrinker or 'r8-nolib-full' in options.shrinker:
assert os.path.isfile(
utils.R8_JAR), 'Cannot build without r8.jar'
shutil.copyfile(utils.R8_JAR, os.path.join(temp_dir, 'r8.jar'))
if 'r8' in options.shrinker or 'r8-full' in options.shrinker:
assert os.path.isfile(
utils.R8LIB_JAR), 'Cannot build without r8lib.jar'
shutil.copyfile(utils.R8LIB_JAR,
os.path.join(temp_dir, 'r8lib.jar'))
jobs = []
result_per_shrinker_per_app = []
for app in options.apps:
if app.skip:
continue
result = {}
result_per_shrinker_per_app.append((app, result))
jobs.append(create_job(app, options, result, temp_dir))
thread_utils.run_in_parallel(jobs,
number_of_workers=options.workers,
stop_on_first_failure=False)
errors = log_results_for_apps(result_per_shrinker_per_app, options)
if errors > 0:
dest = 'gs://r8-test-results/r8-libs/' + str(int(time.time()))
utils.upload_file_to_cloud_storage(
os.path.join(temp_dir, 'r8lib.jar'), dest)
print('R8lib saved to %s' % dest)
return errors
def create_job(app, options, result, temp_dir):
return lambda worker_id: run_job(app, options, result, temp_dir, worker_id)
def run_job(app, options, result, temp_dir, worker_id):
job_temp_dir = os.path.join(temp_dir, str(worker_id or 0))
os.makedirs(job_temp_dir, exist_ok=True)
result.update(get_results_for_app(app, options, job_temp_dir, worker_id))
return 0
def success(message):
CGREEN = '\033[32m'
CEND = '\033[0m'
print(CGREEN + message + CEND)
def warn(message):
CRED = '\033[91m'
CEND = '\033[0m'
print(CRED + message + CEND)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))