| #!/usr/bin/env python3 | |
| # Copyright (c) 2022, 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 os | |
| import sys | |
| import time | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| import adb | |
| import profile_utils | |
| class Device: | |
| def __init__(self, device_id, device_pin): | |
| self.device_id = device_id | |
| self.device_pin = device_pin | |
| def extend_startup_descriptors(startup_descriptors, iteration, device, options): | |
| (logcat, profile, profile_classes_and_methods) = \ | |
| generate_startup_profile(device, options) | |
| if options.logcat: | |
| write_tmp_logcat(logcat, iteration, options) | |
| current_startup_descriptors = get_r8_startup_descriptors_from_logcat( | |
| logcat, options) | |
| else: | |
| write_tmp_profile(profile, iteration, options) | |
| write_tmp_profile_classes_and_methods(profile_classes_and_methods, | |
| iteration, options) | |
| current_startup_descriptors = \ | |
| profile_utils.transform_art_profile_to_r8_startup_list( | |
| profile_classes_and_methods, options.generalize_synthetics) | |
| write_tmp_startup_descriptors(current_startup_descriptors, iteration, | |
| options) | |
| new_startup_descriptors = add_r8_startup_descriptors( | |
| startup_descriptors, current_startup_descriptors) | |
| number_of_new_startup_descriptors = \ | |
| len(new_startup_descriptors) - len(startup_descriptors) | |
| if options.out is not None: | |
| print('Found %i new startup descriptors in iteration %i' % | |
| (number_of_new_startup_descriptors, iteration + 1)) | |
| return new_startup_descriptors | |
| def generate_startup_profile(device, options): | |
| logcat = None | |
| profile = None | |
| profile_classes_and_methods = None | |
| if options.use_existing_profile: | |
| # Verify presence of profile. | |
| adb.check_app_has_profile_data(options.app_id, device.device_id) | |
| profile = adb.get_profile_data(options.app_id, device.device_id) | |
| profile_classes_and_methods = \ | |
| profile_utils.parse_art_profile( | |
| adb.get_classes_and_methods_from_app_profile( | |
| options.app_id, device.device_id)) | |
| else: | |
| # Unlock device. | |
| tear_down_options = adb.prepare_for_interaction_with_device( | |
| device.device_id, device.device_pin) | |
| logcat_process = None | |
| if options.logcat: | |
| # Clear logcat and start capturing logcat. | |
| adb.clear_logcat(device.device_id) | |
| logcat_process = adb.start_logcat( | |
| device.device_id, | |
| format='tag', | |
| filter='R8:I ActivityTaskManager:I *:S') | |
| else: | |
| # Clear existing profile data. | |
| adb.clear_profile_data(options.app_id, device.device_id) | |
| # Launch activity to generate startup profile on device. | |
| adb.launch_activity(options.app_id, options.main_activity, | |
| device.device_id) | |
| # Wait for activity startup. | |
| time.sleep(options.startup_duration) | |
| if options.logcat: | |
| # Get startup descriptors from logcat. | |
| logcat = adb.stop_logcat(logcat_process) | |
| else: | |
| # Capture startup profile. | |
| adb.capture_app_profile_data(options.app_id, device.device_id) | |
| profile = adb.get_profile_data(options.app_id, device.device_id) | |
| profile_classes_and_methods = \ | |
| profile_utils.parse_art_profile( | |
| adb.get_classes_and_methods_from_app_profile( | |
| options.app_id, device.device_id)) | |
| # Shutdown app. | |
| adb.stop_app(options.app_id, device.device_id) | |
| adb.teardown_after_interaction_with_device(tear_down_options, | |
| device.device_id) | |
| return (logcat, profile, profile_classes_and_methods) | |
| def get_r8_startup_descriptors_from_logcat(logcat, options): | |
| post_startup = False | |
| startup_descriptors = {} | |
| for line in logcat: | |
| line_elements = parse_logcat_line(line) | |
| if line_elements is None: | |
| continue | |
| (priority, tag, message) = line_elements | |
| if tag == 'ActivityTaskManager': | |
| if message.startswith('START') \ | |
| or message.startswith('Activity pause timeout for') \ | |
| or message.startswith('Activity top resumed state loss timeout for') \ | |
| or message.startswith('Force removing') \ | |
| or message.startswith( | |
| 'Launch timeout has expired, giving up wake lock!'): | |
| continue | |
| elif message.startswith('Displayed %s/' % options.app_id): | |
| print('Entering post startup: %s' % message) | |
| post_startup = True | |
| continue | |
| elif tag == 'R8': | |
| if is_startup_descriptor(message): | |
| startup_descriptors[message] = { | |
| 'conditional_startup': False, | |
| 'hot': False, | |
| 'post_startup': post_startup, | |
| 'startup': True | |
| } | |
| continue | |
| # Reaching here means we didn't expect this line. | |
| report_unrecognized_logcat_line(line) | |
| return startup_descriptors | |
| def is_startup_descriptor(string): | |
| # The descriptor should start with the holder (possibly prefixed with 'S'). | |
| if not any(string.startswith('%sL' % flags) for flags in ['', 'S']): | |
| return False | |
| # The descriptor should end with ';', a primitive type, or void. | |
| if not string.endswith(';') \ | |
| and not any(string.endswith(c) for c in get_primitive_descriptors()) \ | |
| and not string.endswith('V'): | |
| return False | |
| return True | |
| def get_primitive_descriptors(): | |
| return ['Z', 'B', 'S', 'C', 'I', 'F', 'J', 'D'] | |
| def parse_logcat_line(line): | |
| if line == '--------- beginning of kernel': | |
| return None | |
| if line == '--------- beginning of main': | |
| return None | |
| if line == '--------- beginning of system': | |
| return None | |
| priority = None | |
| tag = None | |
| try: | |
| priority_end = line.index('/') | |
| priority = line[0:priority_end] | |
| line = line[priority_end + 1:] | |
| except ValueError: | |
| return report_unrecognized_logcat_line(line) | |
| try: | |
| tag_end = line.index(':') | |
| tag = line[0:tag_end].strip() | |
| line = line[tag_end + 1:] | |
| except ValueError: | |
| return report_unrecognized_logcat_line(line) | |
| message = line.strip() | |
| return (priority, tag, message) | |
| def report_unrecognized_logcat_line(line): | |
| print('Unrecognized line in logcat: %s' % line) | |
| def add_r8_startup_descriptors(old_startup_descriptors, | |
| startup_descriptors_to_add): | |
| new_startup_descriptors = {} | |
| if len(old_startup_descriptors) == 0: | |
| for startup_descriptor, flags in startup_descriptors_to_add.items(): | |
| new_startup_descriptors[startup_descriptor] = flags.copy() | |
| else: | |
| # Merge the new startup descriptors with the old descriptors in a way so | |
| # that new startup descriptors are added next to the startup descriptors | |
| # they are close to in the newly generated list of startup descriptors. | |
| startup_descriptors_to_add_after_key = {} | |
| startup_descriptors_to_add_in_the_end = {} | |
| closest_seen_startup_descriptor = None | |
| for startup_descriptor, flags in startup_descriptors_to_add.items(): | |
| if startup_descriptor in old_startup_descriptors: | |
| closest_seen_startup_descriptor = startup_descriptor | |
| else: | |
| if closest_seen_startup_descriptor is None: | |
| # Insert this new startup descriptor in the end of the result. | |
| startup_descriptors_to_add_in_the_end[ | |
| startup_descriptor] = flags | |
| else: | |
| # Record that this should be inserted after | |
| # closest_seen_startup_descriptor. | |
| pending_startup_descriptors = \ | |
| startup_descriptors_to_add_after_key.setdefault( | |
| closest_seen_startup_descriptor, {}) | |
| pending_startup_descriptors[startup_descriptor] = flags | |
| for startup_descriptor, flags in old_startup_descriptors.items(): | |
| # Merge flags if this also exists in startup_descriptors_to_add. | |
| if startup_descriptor in startup_descriptors_to_add: | |
| merged_flags = flags.copy() | |
| other_flags = startup_descriptors_to_add[startup_descriptor] | |
| assert not other_flags['conditional_startup'] | |
| merged_flags['hot'] = \ | |
| merged_flags['hot'] or other_flags['hot'] | |
| merged_flags['startup'] = \ | |
| merged_flags['startup'] or other_flags['startup'] | |
| merged_flags['post_startup'] = \ | |
| merged_flags['post_startup'] or other_flags['post_startup'] | |
| new_startup_descriptors[startup_descriptor] = merged_flags | |
| else: | |
| new_startup_descriptors[startup_descriptor] = flags.copy() | |
| # Flush startup descriptors that followed this item in the new trace. | |
| if startup_descriptor in startup_descriptors_to_add_after_key: | |
| pending_startup_descriptors = \ | |
| startup_descriptors_to_add_after_key[startup_descriptor] | |
| for pending_startup_descriptor, pending_flags \ | |
| in pending_startup_descriptors.items(): | |
| new_startup_descriptors[pending_startup_descriptor] = \ | |
| pending_flags.copy() | |
| # Insert remaining new startup descriptors in the end. | |
| for startup_descriptor, flags \ | |
| in startup_descriptors_to_add_in_the_end.items(): | |
| assert startup_descriptor not in new_startup_descriptors | |
| new_startup_descriptors[startup_descriptor] = flags.copy() | |
| return new_startup_descriptors | |
| def write_tmp_binary_artifact(artifact, iteration, options, name): | |
| if not options.tmp_dir: | |
| return | |
| out_dir = os.path.join(options.tmp_dir, str(iteration)) | |
| os.makedirs(out_dir, exist_ok=True) | |
| path = os.path.join(out_dir, name) | |
| with open(path, 'wb') as f: | |
| f.write(artifact) | |
| def write_tmp_textual_artifact(artifact, | |
| iteration, | |
| options, | |
| name, | |
| item_to_string=None): | |
| if not options.tmp_dir: | |
| return | |
| out_dir = os.path.join(options.tmp_dir, str(iteration)) | |
| os.makedirs(out_dir, exist_ok=True) | |
| path = os.path.join(out_dir, name) | |
| with open(path, 'w') as f: | |
| for item in artifact: | |
| f.write(item if item_to_string is None else item_to_string(item)) | |
| f.write('\n') | |
| def write_tmp_logcat(logcat, iteration, options): | |
| write_tmp_textual_artifact(logcat, iteration, options, 'logcat.txt') | |
| def write_tmp_profile(profile, iteration, options): | |
| write_tmp_binary_artifact(profile, iteration, options, 'primary.prof') | |
| def write_tmp_profile_classes_and_methods(profile_classes_and_methods, | |
| iteration, options): | |
| def item_to_string(item): | |
| (descriptor, flags) = item | |
| return '%s%s%s%s' % ('H' if flags.get('hot') else '', | |
| 'S' if flags.get('startup') else '', 'P' | |
| if flags.get('post_startup') else '', descriptor) | |
| write_tmp_textual_artifact(profile_classes_and_methods.items(), iteration, | |
| options, 'profile.txt', item_to_string) | |
| def write_tmp_startup_descriptors(startup_descriptors, iteration, options): | |
| lines = [ | |
| startup_descriptor_to_string(startup_descriptor, flags) | |
| for startup_descriptor, flags in startup_descriptors.items() | |
| ] | |
| write_tmp_textual_artifact(lines, iteration, options, | |
| 'startup-descriptors.txt') | |
| def startup_descriptor_to_string(startup_descriptor, flags): | |
| result = '' | |
| if flags['hot']: | |
| result += 'H' | |
| if flags['startup']: | |
| result += 'S' | |
| if flags['post_startup']: | |
| result += 'P' | |
| result += startup_descriptor | |
| return result | |
| def should_include_startup_descriptor(descriptor, flags, options): | |
| if flags.get('conditional_startup') \ | |
| and not options.include_conditional_startup: | |
| return False | |
| if flags.get('post_startup') \ | |
| and not flags.get('startup') \ | |
| and not options.include_post_startup: | |
| return False | |
| return True | |
| def parse_options(argv): | |
| result = argparse.ArgumentParser( | |
| description='Generate a perfetto trace file.') | |
| result.add_argument('--apk', help='Path to the .apk') | |
| result.add_argument('--apks', help='Path to the .apks') | |
| result.add_argument('--app-id', | |
| help='The application ID of interest', | |
| required=True) | |
| result.add_argument('--bundle', help='Path to the .aab') | |
| result.add_argument('--device-id', | |
| help='Device id (e.g., emulator-5554).', | |
| action='append') | |
| result.add_argument('--device-pin', | |
| help='Device pin code (e.g., 1234)', | |
| action='append') | |
| result.add_argument( | |
| '--generalize-synthetics', | |
| help='Whether synthetics should be abstracted into their ' | |
| 'synthetic contexts', | |
| action='store_true', | |
| default=False) | |
| result.add_argument('--grant-post-notification-permission', | |
| help='Grants the android.permission.POST_NOTIFICATIONS ' | |
| 'permission before launching the app', | |
| default=False, | |
| action='store_true') | |
| result.add_argument('--logcat', action='store_true', default=False) | |
| result.add_argument( | |
| '--include-conditional-startup', | |
| help='Include conditional startup classes and methods in ' | |
| 'the R8 startup descriptors', | |
| action='store_true', | |
| default=False) | |
| result.add_argument( | |
| '--include-post-startup', | |
| help='Include post startup classes and methods in the R8 ' | |
| 'startup descriptors', | |
| action='store_true', | |
| default=False) | |
| result.add_argument('--iterations', | |
| help='Number of profiles to generate', | |
| default=1, | |
| type=int) | |
| result.add_argument('--main-activity', help='Main activity class name') | |
| result.add_argument( | |
| '--out', | |
| help='File where to store startup descriptors (defaults ' | |
| 'to stdout)') | |
| result.add_argument('--startup-duration', | |
| help='Duration in seconds before shutting down app', | |
| default=15, | |
| type=int) | |
| result.add_argument('--tmp-dir', | |
| help='Directory where to store intermediate artifacts' | |
| ' (by default these are not emitted)') | |
| result.add_argument('--until-stable', | |
| help='Repeat profile generation until no new startup ' | |
| 'descriptors are found', | |
| action='store_true', | |
| default=False) | |
| result.add_argument( | |
| '--until-stable-iterations', | |
| help='Number of times that profile generation must must ' | |
| 'not find new startup descriptors before exiting', | |
| default=1, | |
| type=int) | |
| result.add_argument('--use-existing-profile', | |
| help='Do not launch app to generate startup profile', | |
| action='store_true', | |
| default=False) | |
| options, args = result.parse_known_args(argv) | |
| # Read the device pins. | |
| device_pins = options.device_pin or [] | |
| del options.device_pin | |
| # Convert the device ids and pins into a list of devices. | |
| options.devices = [] | |
| if options.device_id is None: | |
| # Assume a single device is attached. | |
| options.devices.append( | |
| Device(None, device_pins[0] if len(device_pins) > 0 else None)) | |
| else: | |
| for i in range(len(options.device_id)): | |
| device_id = options.device_id[i] | |
| device_pin = device_pins[i] if i < len(device_pins) else None | |
| options.devices.append(Device(device_id, device_pin)) | |
| del options.device_id | |
| paths = [ | |
| path for path in [options.apk, options.apks, options.bundle] | |
| if path is not None | |
| ] | |
| assert len(paths) <= 1, 'Expected at most one .apk, .apks, or .aab file.' | |
| assert options.main_activity is not None or options.use_existing_profile, \ | |
| 'Argument --main-activity is required except when running with ' \ | |
| '--use-existing-profile.' | |
| return options, args | |
| def run_on_device(device, options, startup_descriptors): | |
| adb.root(device.device_id) | |
| if options.apk: | |
| adb.uninstall(options.app_id, device.device_id) | |
| adb.install(options.apk, device.device_id) | |
| elif options.apks: | |
| adb.uninstall(options.app_id, device.device_id) | |
| adb.install_apks(options.apks, device.device_id) | |
| elif options.bundle: | |
| adb.uninstall(options.app_id, device.device_id) | |
| adb.install_bundle(options.bundle, device.device_id) | |
| # Grant notifications. | |
| if options.grant_post_notification_permission: | |
| adb.grant(options.app_id, 'android.permission.POST_NOTIFICATIONS', | |
| device.device_id) | |
| if options.until_stable: | |
| iteration = 0 | |
| stable_iterations = 0 | |
| while True: | |
| old_startup_descriptors = startup_descriptors | |
| startup_descriptors = extend_startup_descriptors( | |
| old_startup_descriptors, iteration, device, options) | |
| diff = len(startup_descriptors) - len(old_startup_descriptors) | |
| if diff == 0: | |
| stable_iterations = stable_iterations + 1 | |
| if stable_iterations == options.until_stable_iterations: | |
| break | |
| else: | |
| stable_iterations = 0 | |
| iteration = iteration + 1 | |
| else: | |
| for iteration in range(options.iterations): | |
| startup_descriptors = extend_startup_descriptors( | |
| startup_descriptors, iteration, device, options) | |
| return startup_descriptors | |
| def main(argv): | |
| (options, args) = parse_options(argv) | |
| startup_descriptors = {} | |
| for device in options.devices: | |
| startup_descriptors = run_on_device(device, options, | |
| startup_descriptors) | |
| if options.out is not None: | |
| with open(options.out, 'w') as f: | |
| for startup_descriptor, flags in startup_descriptors.items(): | |
| if should_include_startup_descriptor(startup_descriptor, flags, | |
| options): | |
| f.write( | |
| startup_descriptor_to_string(startup_descriptor, flags)) | |
| f.write('\n') | |
| else: | |
| for startup_descriptor, flags in startup_descriptors.items(): | |
| if should_include_startup_descriptor(startup_descriptor, flags, | |
| options): | |
| print(startup_descriptor_to_string(startup_descriptor, flags)) | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv[1:])) |