Christoffer Quist Adamsen | fad33a0 | 2022-03-14 14:45:09 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file |
| 3 | # for details. All rights reserved. Use of this source code is governed by a |
| 4 | # BSD-style license that can be found in the LICENSE file. |
| 5 | |
| 6 | import argparse |
| 7 | import os |
| 8 | import sys |
| 9 | import time |
| 10 | |
| 11 | try: |
| 12 | from perfetto.trace_processor import TraceProcessor |
| 13 | except ImportError: |
| 14 | sys.exit( |
| 15 | 'Unable to analyze perfetto trace without the perfetto library. ' |
| 16 | 'Install instructions:\n' |
| 17 | ' sudo apt install python3-pip\n' |
| 18 | ' pip3 install perfetto') |
| 19 | |
| 20 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| 21 | |
| 22 | import adb_utils |
| 23 | import perfetto_utils |
| 24 | import utils |
| 25 | |
| 26 | def setup(options): |
| 27 | # Increase screen off timeout to avoid device screen turns off. |
| 28 | twenty_four_hours_in_millis = 24 * 60 * 60 * 1000 |
| 29 | previous_screen_off_timeout = adb_utils.get_screen_off_timeout( |
| 30 | options.device_id) |
| 31 | adb_utils.set_screen_off_timeout( |
| 32 | twenty_four_hours_in_millis, options.device_id) |
| 33 | |
| 34 | # Unlock device. |
| 35 | adb_utils.unlock(options.device_id, options.device_pin) |
| 36 | |
| 37 | tear_down_options = { |
| 38 | 'previous_screen_off_timeout': previous_screen_off_timeout |
| 39 | } |
| 40 | return tear_down_options |
| 41 | |
| 42 | def tear_down(options, tear_down_options): |
| 43 | # Reset screen off timeout. |
| 44 | adb_utils.set_screen_off_timeout( |
| 45 | tear_down_options['previous_screen_off_timeout'], |
| 46 | options.device_id) |
| 47 | |
| 48 | def run_all(options, tmp_dir): |
| 49 | # Launch app while collecting information. |
| 50 | data_avg = {} |
| 51 | for iteration in range(options.iterations): |
| 52 | print('Starting iteration %i' % iteration) |
| 53 | out_dir = os.path.join(options.out_dir, str(iteration)) |
| 54 | prepare_for_run(out_dir, options) |
| 55 | data = run(out_dir, options, tmp_dir) |
| 56 | add_data(data_avg, data) |
| 57 | print("Result:") |
| 58 | print(data) |
| 59 | print("Done") |
| 60 | for key, value in data_avg.items(): |
| 61 | if isinstance(value, int): |
| 62 | data_avg[key] = value / options.iterations |
| 63 | print("Average result:") |
| 64 | print(data_avg) |
| 65 | write_data(options.out_dir, data_avg) |
| 66 | |
| 67 | def prepare_for_run(out_dir, options): |
| 68 | adb_utils.root(options.device_id) |
| 69 | adb_utils.uninstall(options.app_id, options.device_id) |
| 70 | adb_utils.install(options.apk, options.device_id) |
| 71 | adb_utils.clear_profile_data(options.app_id, options.device_id) |
| 72 | if options.aot: |
| 73 | adb_utils.force_compilation(options.app_id, options.device_id) |
| 74 | elif options.aot_profile: |
| 75 | adb_utils.launch_activity( |
| 76 | options.app_id, options.main_activity, options.device_id) |
| 77 | time.sleep(options.aot_profile_sleep) |
| 78 | adb_utils.stop_app(options.app_id, options.device_id) |
| 79 | adb_utils.force_profile_compilation(options.app_id, options.device_id) |
| 80 | |
| 81 | adb_utils.drop_caches(options.device_id) |
| 82 | os.makedirs(out_dir, exist_ok=True) |
| 83 | |
| 84 | def run(out_dir, options, tmp_dir): |
| 85 | assert adb_utils.get_screen_state().is_on_and_unlocked() |
| 86 | |
| 87 | # Start perfetto trace collector. |
| 88 | perfetto_process, perfetto_trace_path = perfetto_utils.record_android_trace( |
| 89 | out_dir, tmp_dir) |
| 90 | |
| 91 | # Launch main activity. |
| 92 | adb_utils.launch_activity( |
| 93 | options.app_id, options.main_activity, options.device_id) |
| 94 | |
| 95 | # Wait for perfetto trace collector to stop. |
| 96 | perfetto_utils.stop_record_android_trace(perfetto_process, out_dir) |
| 97 | |
| 98 | # Get minor and major page faults from app process. |
| 99 | data = compute_data(perfetto_trace_path, options) |
| 100 | write_data(out_dir, data) |
| 101 | return data |
| 102 | |
| 103 | def add_data(sum_data, data): |
| 104 | for key, value in data.items(): |
| 105 | if key == 'time': |
| 106 | continue |
| 107 | if hasattr(sum_data, key): |
| 108 | if key == 'app_id': |
| 109 | assert sum_data[key] == value |
| 110 | else: |
| 111 | existing_value = sum_data[key] |
| 112 | assert isinstance(value, int) |
| 113 | assert isinstance(existing_value, int) |
| 114 | sum_data[key] = existing_value + value |
| 115 | else: |
| 116 | sum_data[key] = value |
| 117 | |
| 118 | def compute_data(perfetto_trace_path, options): |
| 119 | minfl, majfl = adb_utils.get_minor_major_page_faults( |
| 120 | options.app_id, options.device_id) |
| 121 | data = { |
| 122 | 'app_id': options.app_id, |
| 123 | 'time': time.ctime(time.time()), |
| 124 | 'minfl': minfl, |
| 125 | 'majfl': majfl |
| 126 | } |
| 127 | startup_data = compute_startup_data(perfetto_trace_path, options) |
| 128 | return data | startup_data |
| 129 | |
| 130 | def compute_startup_data(perfetto_trace_path, options): |
| 131 | trace_processor = TraceProcessor(file_path=perfetto_trace_path) |
| 132 | |
| 133 | # Compute time to first frame according to the builtin android_startup metric. |
| 134 | startup_metric = trace_processor.metric(['android_startup']) |
| 135 | time_to_first_frame_ms = \ |
| 136 | startup_metric.android_startup.startup[0].to_first_frame.dur_ms |
| 137 | |
| 138 | # Compute time to first and last doFrame event. |
| 139 | bind_application_slice = perfetto_utils.find_unique_slice_by_name( |
| 140 | 'bindApplication', options, trace_processor) |
| 141 | activity_start_slice = perfetto_utils.find_unique_slice_by_name( |
| 142 | 'activityStart', options, trace_processor) |
| 143 | do_frame_slices = perfetto_utils.find_slices_by_name( |
| 144 | 'Choreographer#doFrame', options, trace_processor) |
| 145 | first_do_frame_slice = next(do_frame_slices) |
| 146 | *_, last_do_frame_slice = do_frame_slices |
| 147 | |
| 148 | return { |
| 149 | 'time_to_first_frame_ms': time_to_first_frame_ms, |
| 150 | 'time_to_first_choreographer_do_frame_ms': |
| 151 | perfetto_utils.get_slice_end_since_start( |
| 152 | first_do_frame_slice, bind_application_slice), |
| 153 | 'time_to_last_choreographer_do_frame_ms': |
| 154 | perfetto_utils.get_slice_end_since_start( |
| 155 | last_do_frame_slice, bind_application_slice) |
| 156 | } |
| 157 | |
| 158 | def write_data(out_dir, data): |
| 159 | data_path = os.path.join(out_dir, 'data.txt') |
| 160 | with open(data_path, 'w') as f: |
| 161 | for key, value in data.items(): |
| 162 | f.write('%s=%s\n' % (key, str(value))) |
| 163 | |
| 164 | def parse_options(argv): |
| 165 | result = argparse.ArgumentParser( |
| 166 | description='Generate a perfetto trace file.') |
| 167 | result.add_argument('--app-id', |
| 168 | help='The application ID of interest', |
| 169 | required=True) |
| 170 | result.add_argument('--aot', |
| 171 | help='Enable force compilation', |
| 172 | default=False, |
| 173 | action='store_true') |
| 174 | result.add_argument('--aot-profile', |
| 175 | help='Enable force compilation using profiles', |
| 176 | default=False, |
| 177 | action='store_true') |
| 178 | result.add_argument('--aot-profile-sleep', |
| 179 | help='Duration in seconds before forcing compilation', |
| 180 | default=15, |
| 181 | type=int) |
| 182 | result.add_argument('--apk', |
| 183 | help='Path to the APK', |
| 184 | required=True) |
| 185 | result.add_argument('--device-id', |
| 186 | help='Device id (e.g., emulator-5554).') |
| 187 | result.add_argument('--device-pin', |
| 188 | help='Device pin code (e.g., 1234)') |
| 189 | result.add_argument('--iterations', |
| 190 | help='Number of traces to generate', |
| 191 | default=1, |
| 192 | type=int) |
| 193 | result.add_argument('--main-activity', |
| 194 | help='Main activity class name', |
| 195 | required=True) |
| 196 | result.add_argument('--out-dir', |
| 197 | help='Directory to store trace files in', |
| 198 | required=True) |
| 199 | options, args = result.parse_known_args(argv) |
| 200 | assert (not options.aot) or (not options.aot_profile) |
| 201 | return options, args |
| 202 | |
| 203 | def main(argv): |
| 204 | (options, args) = parse_options(argv) |
| 205 | with utils.TempDir() as tmp_dir: |
| 206 | tear_down_options = setup(options) |
| 207 | run_all(options, tmp_dir) |
| 208 | tear_down(options, tear_down_options) |
| 209 | |
| 210 | if __name__ == '__main__': |
| 211 | sys.exit(main(sys.argv[1:])) |