| #!/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 adb_utils |
| |
| import argparse |
| import os |
| import sys |
| import time |
| |
| def extend_startup_descriptors(startup_descriptors, iteration, options): |
| (logcat, profile, profile_classes_and_methods) = \ |
| generate_startup_profile(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 = \ |
| transform_classes_and_methods_to_r8_startup_descriptors( |
| profile_classes_and_methods, options) |
| 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(options): |
| logcat = None |
| profile = None |
| profile_classes_and_methods = None |
| if options.use_existing_profile: |
| # Verify presence of profile. |
| adb_utils.check_app_has_profile_data(options.app_id, options.device_id) |
| profile = adb_utils.get_profile_data(options.app_id, options.device_id) |
| profile_classes_and_methods = \ |
| adb_utils.get_classes_and_methods_from_app_profile( |
| options.app_id, options.device_id) |
| else: |
| # Unlock device. |
| tear_down_options = adb_utils.prepare_for_interaction_with_device( |
| options.device_id, options.device_pin) |
| |
| logcat_process = None |
| if options.logcat: |
| # Clear logcat and start capturing logcat. |
| adb_utils.clear_logcat(options.device_id) |
| logcat_process = adb_utils.start_logcat( |
| options.device_id, format='tag', filter='r8:I ActivityTaskManager:I *:S') |
| else: |
| # Clear existing profile data. |
| adb_utils.clear_profile_data(options.app_id, options.device_id) |
| |
| # Launch activity to generate startup profile on device. |
| adb_utils.launch_activity( |
| options.app_id, options.main_activity, options.device_id) |
| |
| # Wait for activity startup. |
| time.sleep(options.startup_duration) |
| |
| if options.logcat: |
| # Get startup descriptors from logcat. |
| logcat = adb_utils.stop_logcat(logcat_process) |
| else: |
| # Capture startup profile. |
| adb_utils.capture_app_profile_data(options.app_id, options.device_id) |
| profile = adb_utils.get_profile_data(options.app_id, options.device_id) |
| profile_classes_and_methods = \ |
| adb_utils.get_classes_and_methods_from_app_profile( |
| options.app_id, options.device_id) |
| |
| # Shutdown app. |
| adb_utils.stop_app(options.app_id, options.device_id) |
| adb_utils.teardown_after_interaction_with_device( |
| tear_down_options, options.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, |
| 'post_startup': post_startup |
| } |
| 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 transform_classes_and_methods_to_r8_startup_descriptors( |
| classes_and_methods, options): |
| startup_descriptors = [] |
| for class_or_method in classes_and_methods: |
| descriptor = class_or_method.get('descriptor') |
| flags = class_or_method.get('flags') |
| if should_include_startup_descriptor(descriptor, flags, options): |
| startup_descriptors.append(descriptor) |
| return startup_descriptors |
| |
| 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'] |
| if other_flags['post_startup']: |
| merged_flags['post_startup'] = True |
| 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 = item.get('descriptor') |
| flags = item.get('flags') |
| 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, |
| 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['conditional_startup']: |
| pass # result += 'C' |
| if flags['post_startup']: |
| pass # 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('--app-id', |
| help='The application ID of interest', |
| required=True) |
| result.add_argument('--device-id', |
| help='Device id (e.g., emulator-5554).') |
| result.add_argument('--device-pin', |
| help='Device pin code (e.g., 1234)') |
| 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) |
| 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 main(argv): |
| (options, args) = parse_options(argv) |
| adb_utils.root(options.device_id) |
| if options.apk: |
| adb_utils.uninstall(options.app_id, options.device_id) |
| adb_utils.install(options.apk, options.device_id) |
| startup_descriptors = {} |
| 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, 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, options) |
| 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:])) |