| #!/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', | |
| }), | |
| # 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_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 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. | |
| utils.ensure_download(app_dir, 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 | |
| add_golem_resource(2, jdk.GetJdk21Home(), '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_dir = os.path.join(utils.OPENSOURCE_DUMPS_DIR, app.folder) | |
| name = 'appResource' | |
| add_golem_resource(indentation, app_dir, 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, path, name, sha256=None): | |
| gz = path + '.tar.gz' | |
| sha = gz + '.sha1' | |
| if not sha256: | |
| # Golem uses a sha256 of the file in the cache, and you need to specify that. | |
| utils.ensure_google_download(path, 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.run_gradle([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.run_gradle( | |
| [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) | |
| return log_results_for_apps(result_per_shrinker_per_app, options) | |
| 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:])) |