blob: 04076ece408d3b0a3e4838dcddd5f41b1618a65e [file] [log] [blame]
#!/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 statistics
import sys
import time
try:
from perfetto.trace_processor import TraceProcessor
except ImportError:
sys.exit(
'Unable to analyze perfetto trace without the perfetto library. '
'Install instructions:\n'
' sudo apt install python3-pip\n'
' pip3 install perfetto')
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import adb_utils
import apk_utils
import perfetto_utils
import utils
def setup(options):
# Increase screen off timeout to avoid device screen turns off.
twenty_four_hours_in_millis = 24 * 60 * 60 * 1000
previous_screen_off_timeout = adb_utils.get_screen_off_timeout(
options.device_id)
adb_utils.set_screen_off_timeout(
twenty_four_hours_in_millis, options.device_id)
# Unlock device.
adb_utils.unlock(options.device_id, options.device_pin)
teardown_options = {
'previous_screen_off_timeout': previous_screen_off_timeout
}
return teardown_options
def teardown(options, teardown_options):
# Reset screen off timeout.
adb_utils.set_screen_off_timeout(
teardown_options['previous_screen_off_timeout'],
options.device_id)
def run_all(apk, options, tmp_dir):
# Launch app while collecting information.
data_total = {}
for iteration in range(1, options.iterations + 1):
print('Starting iteration %i' % iteration)
out_dir = os.path.join(options.out_dir, str(iteration))
teardown_options = setup_for_run(apk, out_dir, options)
data = run(out_dir, options, tmp_dir)
teardown_for_run(options, teardown_options)
add_data(data_total, data)
print('Result:')
print(data)
print(compute_data_summary(data_total))
print('Done')
print('Average result:')
data_summary = compute_data_summary(data_total)
print(data_summary)
write_data(options.out_dir, data_summary)
def compute_data_summary(data_total):
data_summary = {}
for key, value in data_total.items():
if not isinstance(value, list):
data_summary[key] = value
continue
data_summary['%s_avg' % key] = round(statistics.mean(value), 1)
data_summary['%s_med' % key] = statistics.median(value)
return data_summary
def setup_for_run(apk, out_dir, options):
adb_utils.root(options.device_id)
print('Installing')
adb_utils.uninstall(options.app_id, options.device_id)
adb_utils.install(apk, options.device_id)
os.makedirs(out_dir, exist_ok=True)
# AOT compile.
if options.aot:
print('AOT compiling')
if options.baseline_profile:
adb_utils.clear_profile_data(options.app_id, options.device_id)
adb_utils.install_profile(options.app_id, options.device_id)
else:
adb_utils.force_compilation(options.app_id, options.device_id)
# Cooldown and then unlock device.
if options.cooldown > 0:
print('Cooling down for %i seconds' % options.cooldown)
assert adb_utils.get_screen_state(options.device_id).is_off()
time.sleep(options.cooldown)
teardown_options = adb_utils.prepare_for_interaction_with_device(
options.device_id, options.device_pin)
else:
teardown_options = None
# Prelaunch for hot startup.
if options.hot_startup:
print('Prelaunching')
adb_utils.launch_activity(
options.app_id,
options.main_activity,
options.device_id,
wait_for_activity_to_launch=False)
time.sleep(options.startup_duration)
adb_utils.navigate_to_home_screen(options.device_id)
time.sleep(1)
# Drop caches before run.
adb_utils.drop_caches(options.device_id)
return teardown_options
def teardown_for_run(options, teardown_options):
assert adb_utils.get_screen_state(options.device_id).is_on_and_unlocked()
if options.cooldown > 0:
adb_utils.teardown_after_interaction_with_device(
teardown_options, options.device_id)
adb_utils.ensure_screen_off(options.device_id)
else:
assert teardown_options is None
def run(out_dir, options, tmp_dir):
assert adb_utils.get_screen_state(options.device_id).is_on_and_unlocked()
# Start perfetto trace collector.
perfetto_process = None
perfetto_trace_path = None
if options.perfetto:
perfetto_process, perfetto_trace_path = perfetto_utils.record_android_trace(
out_dir, tmp_dir, options.device_id)
# Launch main activity.
launch_activity_result = adb_utils.launch_activity(
options.app_id,
options.main_activity,
options.device_id,
wait_for_activity_to_launch=True)
# Wait for perfetto trace collector to stop.
if options.perfetto:
perfetto_utils.stop_record_android_trace(perfetto_process, out_dir)
# Get minor and major page faults from app process.
data = compute_data(launch_activity_result, perfetto_trace_path, options)
write_data(out_dir, data)
return data
def add_data(data_total, data):
for key, value in data.items():
if key == 'app_id':
assert data_total.get(key, value) == value
data_total[key] = value
if key == 'time':
continue
if key in data_total:
if key == 'app_id':
assert data_total[key] == value
else:
existing_value = data_total[key]
assert isinstance(value, int)
assert isinstance(existing_value, list)
existing_value.append(value)
else:
assert isinstance(value, int), key
data_total[key] = [value]
def compute_data(launch_activity_result, perfetto_trace_path, options):
minfl, majfl = adb_utils.get_minor_major_page_faults(
options.app_id, options.device_id)
data = {
'app_id': options.app_id,
'time': time.ctime(time.time()),
'minfl': minfl,
'majfl': majfl
}
startup_data = compute_startup_data(
launch_activity_result, perfetto_trace_path, options)
return data | startup_data
def compute_startup_data(launch_activity_result, perfetto_trace_path, options):
startup_data = {
'adb_startup': launch_activity_result.get('total_time')
}
perfetto_startup_data = {}
if options.perfetto:
trace_processor = TraceProcessor(file_path=perfetto_trace_path)
# Compute time to first frame according to the builtin android_startup
# metric.
startup_metric = trace_processor.metric(['android_startup'])
time_to_first_frame_ms = \
startup_metric.android_startup.startup[0].to_first_frame.dur_ms
perfetto_startup_data['perfetto_startup'] = round(time_to_first_frame_ms)
if not options.hot_startup:
# Compute time to first and last doFrame event.
bind_application_slice = perfetto_utils.find_unique_slice_by_name(
'bindApplication', options, trace_processor)
activity_start_slice = perfetto_utils.find_unique_slice_by_name(
'activityStart', options, trace_processor)
do_frame_slices = perfetto_utils.find_slices_by_name(
'Choreographer#doFrame', options, trace_processor)
first_do_frame_slice = next(do_frame_slices)
*_, last_do_frame_slice = do_frame_slices
perfetto_startup_data.update({
'time_to_first_choreographer_do_frame_ms':
round(perfetto_utils.get_slice_end_since_start(
first_do_frame_slice, bind_application_slice)),
'time_to_last_choreographer_do_frame_ms':
round(perfetto_utils.get_slice_end_since_start(
last_do_frame_slice, bind_application_slice))
})
# Return combined startup data.
return startup_data | perfetto_startup_data
def write_data(out_dir, data):
data_path = os.path.join(out_dir, 'data.txt')
with open(data_path, 'w') as f:
for key, value in data.items():
f.write('%s=%s\n' % (key, str(value)))
def parse_options(argv):
result = argparse.ArgumentParser(
description='Generate a perfetto trace file.')
result.add_argument('--app-id',
help='The application ID of interest',
required=True)
result.add_argument('--aot',
help='Enable force compilation',
default=False,
action='store_true')
result.add_argument('--aot-profile',
help='Enable force compilation using profiles',
default=False,
action='store_true')
result.add_argument('--apk',
help='Path to the APK',
required=True)
result.add_argument('--cooldown',
help='Seconds to wait before running each iteration',
default=0,
type=int)
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('--hot-startup',
help='Measure hot startup instead of cold startup',
default=False,
action='store_true')
result.add_argument('--iterations',
help='Number of traces to generate',
default=1,
type=int)
result.add_argument('--main-activity',
help='Main activity class name',
required=True)
result.add_argument('--no-perfetto',
help='Disables perfetto trace generation',
action='store_true',
default=False)
result.add_argument('--out-dir',
help='Directory to store trace files in',
required=True)
result.add_argument('--baseline-profile',
help='Baseline profile to install')
result.add_argument('--startup-duration',
help='Duration in seconds before shutting down app',
default=15,
type=int)
options, args = result.parse_known_args(argv)
setattr(options, 'perfetto', not options.no_perfetto)
# Profile is only used with --aot.
assert options.aot or not options.baseline_profile
return options, args
def global_setup(options):
# If there is no cooldown then unlock the screen once. Otherwise we turn off
# the screen during the cooldown and unlock the screen before each iteration.
teardown_options = None
if options.cooldown == 0:
teardown_options = adb_utils.prepare_for_interaction_with_device(
options.device_id, options.device_pin)
assert adb_utils.get_screen_state(options.device_id).is_on()
else:
adb_utils.ensure_screen_off(options.device_id)
return teardown_options
def global_teardown(options, teardown_options):
if options.cooldown == 0:
adb_utils.teardown_after_interaction_with_device(
teardown_options, options.device_id)
else:
assert teardown_options is None
def main(argv):
(options, args) = parse_options(argv)
with utils.TempDir() as tmp_dir:
apk = apk_utils.add_baseline_profile_to_apk(
options.apk, options.baseline_profile, tmp_dir)
teardown_options = global_setup(options)
run_all(apk, options, tmp_dir)
global_teardown(options, teardown_options)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))