blob: 211be89cf79aa9caa53562eef9d18232651f39df [file] [log] [blame]
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +01001#!/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
6import adb_utils
Christoffer Quist Adamsen1c07af92022-08-11 13:46:25 +02007import profile_utils
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +01008
9import argparse
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020010import os
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010011import sys
12import time
13
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020014
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +020015class Device:
16
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020017 def __init__(self, device_id, device_pin):
18 self.device_id = device_id
19 self.device_pin = device_pin
20
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +020021
22def extend_startup_descriptors(startup_descriptors, iteration, device, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020023 (logcat, profile, profile_classes_and_methods) = \
24 generate_startup_profile(device, options)
25 if options.logcat:
26 write_tmp_logcat(logcat, iteration, options)
27 current_startup_descriptors = get_r8_startup_descriptors_from_logcat(
28 logcat, options)
29 else:
30 write_tmp_profile(profile, iteration, options)
31 write_tmp_profile_classes_and_methods(profile_classes_and_methods,
32 iteration, options)
33 current_startup_descriptors = \
34 profile_utils.transform_art_profile_to_r8_startup_list(
35 profile_classes_and_methods, options.generalize_synthetics)
36 write_tmp_startup_descriptors(current_startup_descriptors, iteration,
37 options)
38 new_startup_descriptors = add_r8_startup_descriptors(
39 startup_descriptors, current_startup_descriptors)
40 number_of_new_startup_descriptors = \
41 len(new_startup_descriptors) - len(startup_descriptors)
42 if options.out is not None:
43 print('Found %i new startup descriptors in iteration %i' %
44 (number_of_new_startup_descriptors, iteration + 1))
45 return new_startup_descriptors
46
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010047
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +020048def generate_startup_profile(device, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020049 logcat = None
50 profile = None
51 profile_classes_and_methods = None
52 if options.use_existing_profile:
53 # Verify presence of profile.
54 adb_utils.check_app_has_profile_data(options.app_id, device.device_id)
55 profile = adb_utils.get_profile_data(options.app_id, device.device_id)
56 profile_classes_and_methods = \
57 adb_utils.get_classes_and_methods_from_app_profile(
58 options.app_id, device.device_id)
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020059 else:
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020060 # Unlock device.
61 tear_down_options = adb_utils.prepare_for_interaction_with_device(
62 device.device_id, device.device_pin)
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +020063
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020064 logcat_process = None
65 if options.logcat:
66 # Clear logcat and start capturing logcat.
67 adb_utils.clear_logcat(device.device_id)
68 logcat_process = adb_utils.start_logcat(
69 device.device_id,
70 format='tag',
71 filter='R8:I ActivityTaskManager:I *:S')
72 else:
73 # Clear existing profile data.
74 adb_utils.clear_profile_data(options.app_id, device.device_id)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010075
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020076 # Launch activity to generate startup profile on device.
77 adb_utils.launch_activity(options.app_id, options.main_activity,
78 device.device_id)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010079
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020080 # Wait for activity startup.
81 time.sleep(options.startup_duration)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010082
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020083 if options.logcat:
84 # Get startup descriptors from logcat.
85 logcat = adb_utils.stop_logcat(logcat_process)
86 else:
87 # Capture startup profile.
88 adb_utils.capture_app_profile_data(options.app_id, device.device_id)
89 profile = adb_utils.get_profile_data(options.app_id,
90 device.device_id)
91 profile_classes_and_methods = \
92 adb_utils.get_classes_and_methods_from_app_profile(
93 options.app_id, device.device_id)
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +010094
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020095 # Shutdown app.
96 adb_utils.stop_app(options.app_id, device.device_id)
97 adb_utils.teardown_after_interaction_with_device(
98 tear_down_options, device.device_id)
99
100 return (logcat, profile, profile_classes_and_methods)
101
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200102
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200103def get_r8_startup_descriptors_from_logcat(logcat, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200104 post_startup = False
105 startup_descriptors = {}
106 for line in logcat:
107 line_elements = parse_logcat_line(line)
108 if line_elements is None:
109 continue
110 (priority, tag, message) = line_elements
111 if tag == 'ActivityTaskManager':
112 if message.startswith('START') \
113 or message.startswith('Activity pause timeout for') \
114 or message.startswith('Activity top resumed state loss timeout for') \
115 or message.startswith('Force removing') \
116 or message.startswith(
117 'Launch timeout has expired, giving up wake lock!'):
118 continue
119 elif message.startswith('Displayed %s/' % options.app_id):
120 print('Entering post startup: %s' % message)
121 post_startup = True
122 continue
123 elif tag == 'R8':
124 if is_startup_descriptor(message):
125 startup_descriptors[message] = {
126 'conditional_startup': False,
127 'hot': False,
128 'post_startup': post_startup,
129 'startup': True
130 }
131 continue
132 # Reaching here means we didn't expect this line.
133 report_unrecognized_logcat_line(line)
134 return startup_descriptors
135
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100136
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200137def is_startup_descriptor(string):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200138 # The descriptor should start with the holder (possibly prefixed with 'S').
139 if not any(string.startswith('%sL' % flags) for flags in ['', 'S']):
140 return False
141 # The descriptor should end with ';', a primitive type, or void.
142 if not string.endswith(';') \
143 and not any(string.endswith(c) for c in get_primitive_descriptors()) \
144 and not string.endswith('V'):
145 return False
146 return True
147
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200148
149def get_primitive_descriptors():
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200150 return ['Z', 'B', 'S', 'C', 'I', 'F', 'J', 'D']
151
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200152
153def parse_logcat_line(line):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200154 if line == '--------- beginning of kernel':
155 return None
156 if line == '--------- beginning of main':
157 return None
158 if line == '--------- beginning of system':
159 return None
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200160
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200161 priority = None
162 tag = None
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200163
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200164 try:
165 priority_end = line.index('/')
166 priority = line[0:priority_end]
167 line = line[priority_end + 1:]
168 except ValueError:
169 return report_unrecognized_logcat_line(line)
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200170
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200171 try:
172 tag_end = line.index(':')
173 tag = line[0:tag_end].strip()
174 line = line[tag_end + 1:]
175 except ValueError:
176 return report_unrecognized_logcat_line(line)
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200177
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200178 message = line.strip()
179 return (priority, tag, message)
180
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200181
182def report_unrecognized_logcat_line(line):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200183 print('Unrecognized line in logcat: %s' % line)
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200184
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200185
186def add_r8_startup_descriptors(old_startup_descriptors,
187 startup_descriptors_to_add):
188 new_startup_descriptors = {}
189 if len(old_startup_descriptors) == 0:
190 for startup_descriptor, flags in startup_descriptors_to_add.items():
191 new_startup_descriptors[startup_descriptor] = flags.copy()
192 else:
193 # Merge the new startup descriptors with the old descriptors in a way so
194 # that new startup descriptors are added next to the startup descriptors
195 # they are close to in the newly generated list of startup descriptors.
196 startup_descriptors_to_add_after_key = {}
197 startup_descriptors_to_add_in_the_end = {}
198 closest_seen_startup_descriptor = None
199 for startup_descriptor, flags in startup_descriptors_to_add.items():
200 if startup_descriptor in old_startup_descriptors:
201 closest_seen_startup_descriptor = startup_descriptor
202 else:
203 if closest_seen_startup_descriptor is None:
204 # Insert this new startup descriptor in the end of the result.
205 startup_descriptors_to_add_in_the_end[
206 startup_descriptor] = flags
207 else:
208 # Record that this should be inserted after
209 # closest_seen_startup_descriptor.
210 pending_startup_descriptors = \
211 startup_descriptors_to_add_after_key.setdefault(
212 closest_seen_startup_descriptor, {})
213 pending_startup_descriptors[startup_descriptor] = flags
214 for startup_descriptor, flags in old_startup_descriptors.items():
215 # Merge flags if this also exists in startup_descriptors_to_add.
216 if startup_descriptor in startup_descriptors_to_add:
217 merged_flags = flags.copy()
218 other_flags = startup_descriptors_to_add[startup_descriptor]
219 assert not other_flags['conditional_startup']
220 merged_flags['hot'] = \
221 merged_flags['hot'] or other_flags['hot']
222 merged_flags['startup'] = \
223 merged_flags['startup'] or other_flags['startup']
224 merged_flags['post_startup'] = \
225 merged_flags['post_startup'] or other_flags['post_startup']
226 new_startup_descriptors[startup_descriptor] = merged_flags
227 else:
228 new_startup_descriptors[startup_descriptor] = flags.copy()
229 # Flush startup descriptors that followed this item in the new trace.
230 if startup_descriptor in startup_descriptors_to_add_after_key:
231 pending_startup_descriptors = \
232 startup_descriptors_to_add_after_key[startup_descriptor]
233 for pending_startup_descriptor, pending_flags \
234 in pending_startup_descriptors.items():
235 new_startup_descriptors[pending_startup_descriptor] = \
236 pending_flags.copy()
237 # Insert remaining new startup descriptors in the end.
238 for startup_descriptor, flags \
239 in startup_descriptors_to_add_in_the_end.items():
240 assert startup_descriptor not in new_startup_descriptors
241 new_startup_descriptors[startup_descriptor] = flags.copy()
242 return new_startup_descriptors
243
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100244
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200245def write_tmp_binary_artifact(artifact, iteration, options, name):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200246 if not options.tmp_dir:
247 return
248 out_dir = os.path.join(options.tmp_dir, str(iteration))
249 os.makedirs(out_dir, exist_ok=True)
250 path = os.path.join(out_dir, name)
251 with open(path, 'wb') as f:
252 f.write(artifact)
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200253
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200254
255def write_tmp_textual_artifact(artifact,
256 iteration,
257 options,
258 name,
259 item_to_string=None):
260 if not options.tmp_dir:
261 return
262 out_dir = os.path.join(options.tmp_dir, str(iteration))
263 os.makedirs(out_dir, exist_ok=True)
264 path = os.path.join(out_dir, name)
265 with open(path, 'w') as f:
266 for item in artifact:
267 f.write(item if item_to_string is None else item_to_string(item))
268 f.write('\n')
269
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200270
271def write_tmp_logcat(logcat, iteration, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200272 write_tmp_textual_artifact(logcat, iteration, options, 'logcat.txt')
273
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200274
275def write_tmp_profile(profile, iteration, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200276 write_tmp_binary_artifact(profile, iteration, options, 'primary.prof')
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200277
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200278
279def write_tmp_profile_classes_and_methods(profile_classes_and_methods,
280 iteration, options):
281
282 def item_to_string(item):
283 (descriptor, flags) = item
284 return '%s%s%s%s' % ('H' if flags.get('hot') else '',
285 'S' if flags.get('startup') else '', 'P'
286 if flags.get('post_startup') else '', descriptor)
287
288 write_tmp_textual_artifact(profile_classes_and_methods.items(), iteration,
289 options, 'profile.txt', item_to_string)
290
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200291
292def write_tmp_startup_descriptors(startup_descriptors, iteration, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200293 lines = [
294 startup_descriptor_to_string(startup_descriptor, flags)
295 for startup_descriptor, flags in startup_descriptors.items()
296 ]
297 write_tmp_textual_artifact(lines, iteration, options,
298 'startup-descriptors.txt')
299
Christoffer Quist Adamsen1f60e522022-08-03 14:13:03 +0200300
301def startup_descriptor_to_string(startup_descriptor, flags):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200302 result = ''
303 if flags['hot']:
304 result += 'H'
305 if flags['startup']:
306 result += 'S'
307 if flags['post_startup']:
308 result += 'P'
309 result += startup_descriptor
310 return result
311
Christoffer Quist Adamsenc6b19de2022-04-21 13:07:53 +0200312
Christoffer Quist Adamsen13e43132022-08-05 11:31:56 +0200313def should_include_startup_descriptor(descriptor, flags, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200314 if flags.get('conditional_startup') \
315 and not options.include_conditional_startup:
316 return False
317 if flags.get('post_startup') \
318 and not flags.get('startup') \
319 and not options.include_post_startup:
320 return False
321 return True
322
Christoffer Quist Adamsen13e43132022-08-05 11:31:56 +0200323
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100324def parse_options(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200325 result = argparse.ArgumentParser(
326 description='Generate a perfetto trace file.')
327 result.add_argument('--apk', help='Path to the .apk')
328 result.add_argument('--apks', help='Path to the .apks')
329 result.add_argument('--app-id',
330 help='The application ID of interest',
331 required=True)
332 result.add_argument('--bundle', help='Path to the .aab')
333 result.add_argument('--device-id',
334 help='Device id (e.g., emulator-5554).',
335 action='append')
336 result.add_argument('--device-pin',
337 help='Device pin code (e.g., 1234)',
338 action='append')
339 result.add_argument(
340 '--generalize-synthetics',
341 help='Whether synthetics should be abstracted into their '
342 'synthetic contexts',
343 action='store_true',
344 default=False)
345 result.add_argument('--grant-post-notification-permission',
346 help='Grants the android.permission.POST_NOTIFICATIONS '
347 'permission before launching the app',
348 default=False,
349 action='store_true')
350 result.add_argument('--logcat', action='store_true', default=False)
351 result.add_argument(
352 '--include-conditional-startup',
353 help='Include conditional startup classes and methods in '
354 'the R8 startup descriptors',
355 action='store_true',
356 default=False)
357 result.add_argument(
358 '--include-post-startup',
359 help='Include post startup classes and methods in the R8 '
360 'startup descriptors',
361 action='store_true',
362 default=False)
363 result.add_argument('--iterations',
364 help='Number of profiles to generate',
365 default=1,
366 type=int)
367 result.add_argument('--main-activity', help='Main activity class name')
368 result.add_argument(
369 '--out',
370 help='File where to store startup descriptors (defaults '
371 'to stdout)')
372 result.add_argument('--startup-duration',
373 help='Duration in seconds before shutting down app',
374 default=15,
375 type=int)
376 result.add_argument('--tmp-dir',
377 help='Directory where to store intermediate artifacts'
378 ' (by default these are not emitted)')
379 result.add_argument('--until-stable',
380 help='Repeat profile generation until no new startup '
381 'descriptors are found',
382 action='store_true',
383 default=False)
384 result.add_argument(
385 '--until-stable-iterations',
386 help='Number of times that profile generation must must '
387 'not find new startup descriptors before exiting',
388 default=1,
389 type=int)
390 result.add_argument('--use-existing-profile',
391 help='Do not launch app to generate startup profile',
392 action='store_true',
393 default=False)
394 options, args = result.parse_known_args(argv)
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200395
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200396 # Read the device pins.
397 device_pins = options.device_pin or []
398 del options.device_pin
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200399
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200400 # Convert the device ids and pins into a list of devices.
401 options.devices = []
402 if options.device_id is None:
403 # Assume a single device is attached.
404 options.devices.append(
405 Device(None, device_pins[0] if len(device_pins) > 0 else None))
406 else:
407 for i in range(len(options.device_id)):
408 device_id = options.device_id[i]
409 device_pin = device_pins[i] if i < len(device_pins) else None
410 options.devices.append(Device(device_id, device_pin))
411 del options.device_id
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200412
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200413 paths = [
414 path for path in [options.apk, options.apks, options.bundle]
415 if path is not None
416 ]
417 assert len(paths) <= 1, 'Expected at most one .apk, .apks, or .aab file.'
418 assert options.main_activity is not None or options.use_existing_profile, \
419 'Argument --main-activity is required except when running with ' \
420 '--use-existing-profile.'
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200421
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200422 return options, args
423
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100424
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200425def run_on_device(device, options, startup_descriptors):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200426 adb_utils.root(device.device_id)
427 if options.apk:
428 adb_utils.uninstall(options.app_id, device.device_id)
429 adb_utils.install(options.apk, device.device_id)
430 elif options.apks:
431 adb_utils.uninstall(options.app_id, device.device_id)
432 adb_utils.install_apks(options.apks, device.device_id)
433 elif options.bundle:
434 adb_utils.uninstall(options.app_id, device.device_id)
435 adb_utils.install_bundle(options.bundle, device.device_id)
436 # Grant notifications.
437 if options.grant_post_notification_permission:
438 adb_utils.grant(options.app_id, 'android.permission.POST_NOTIFICATIONS',
439 device.device_id)
440 if options.until_stable:
441 iteration = 0
Christoffer Quist Adamsen2d5d9632022-04-06 14:48:30 +0200442 stable_iterations = 0
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200443 while True:
444 old_startup_descriptors = startup_descriptors
445 startup_descriptors = extend_startup_descriptors(
446 old_startup_descriptors, iteration, device, options)
447 diff = len(startup_descriptors) - len(old_startup_descriptors)
448 if diff == 0:
449 stable_iterations = stable_iterations + 1
450 if stable_iterations == options.until_stable_iterations:
451 break
452 else:
453 stable_iterations = 0
454 iteration = iteration + 1
455 else:
456 for iteration in range(options.iterations):
457 startup_descriptors = extend_startup_descriptors(
458 startup_descriptors, iteration, device, options)
459 return startup_descriptors
460
Christoffer Quist Adamsen3937f502022-08-08 14:44:31 +0200461
462def main(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200463 (options, args) = parse_options(argv)
464 startup_descriptors = {}
465 for device in options.devices:
466 startup_descriptors = run_on_device(device, options,
467 startup_descriptors)
468 if options.out is not None:
469 with open(options.out, 'w') as f:
470 for startup_descriptor, flags in startup_descriptors.items():
471 if should_include_startup_descriptor(startup_descriptor, flags,
472 options):
473 f.write(
474 startup_descriptor_to_string(startup_descriptor, flags))
475 f.write('\n')
476 else:
477 for startup_descriptor, flags in startup_descriptors.items():
478 if should_include_startup_descriptor(startup_descriptor, flags,
479 options):
480 print(startup_descriptor_to_string(startup_descriptor, flags))
481
Christoffer Quist Adamsen2771c362022-03-14 15:36:43 +0100482
483if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200484 sys.exit(main(sys.argv[1:]))