Support for measuring startup with profile
Change-Id: I03afd9efd60c466ecd5db5a52d82bfe19f4edb7f
diff --git a/tools/apk_masseur.py b/tools/apk_masseur.py
index b363bb7..a4a010b 100755
--- a/tools/apk_masseur.py
+++ b/tools/apk_masseur.py
@@ -45,9 +45,6 @@
apk = args[0]
return (options, apk)
-def findKeystore():
- return os.path.join(os.getenv('HOME'), '.android', 'app.keystore')
-
def repack(apk, processed_out, resources, temp, quiet, logging):
processed_apk = os.path.join(temp, 'processed.apk')
shutil.copyfile(apk, processed_apk)
@@ -80,25 +77,13 @@
def sign(unsigned_apk, keystore, temp, quiet, logging):
signed_apk = os.path.join(temp, 'unaligned.apk')
- apk_utils.sign_with_apksigner(
+ return apk_utils.sign_with_apksigner(
unsigned_apk, signed_apk, keystore, quiet=quiet, logging=logging)
- return signed_apk
def align(signed_apk, temp, quiet, logging):
utils.Print('Aligning', quiet=quiet)
aligned_apk = os.path.join(temp, 'aligned.apk')
- zipalign_path = (
- 'zipalign' if 'build_tools' in os.environ.get('PATH')
- else os.path.join(utils.getAndroidBuildTools(), 'zipalign'))
- cmd = [
- zipalign_path,
- '-f',
- '4',
- signed_apk,
- aligned_apk
- ]
- utils.RunCmd(cmd, quiet=quiet, logging=logging)
- return signed_apk
+ return apk_utils.align(signed_apk, aligned_apk)
def masseur(
apk, dex=None, resources=None, out=None, adb_options=None, keystore=None,
@@ -106,7 +91,7 @@
if not out:
out = os.path.basename(apk)
if not keystore:
- keystore = findKeystore()
+ keystore = apk_utils.default_keystore()
with utils.TempDir() as temp:
processed_apk = None
if dex:
diff --git a/tools/apk_utils.py b/tools/apk_utils.py
index c3c616b..86d3b0f 100755
--- a/tools/apk_utils.py
+++ b/tools/apk_utils.py
@@ -5,9 +5,13 @@
import optparse
import os
+import shutil
import subprocess
import sys
+import time
+
import utils
+import zip_utils
USAGE = 'usage: %prog [options] <apk>'
@@ -34,6 +38,30 @@
apk = args[0]
return (options, apk)
+def add_baseline_profile_to_apk(apk, baseline_profile, tmp_dir):
+ if baseline_profile is None:
+ return apk
+ ts = time.time_ns()
+ dest_apk = os.path.join(tmp_dir, 'app-%s.apk' % ts)
+ dest_apk_aligned = os.path.join(tmp_dir, 'app-aligned-%s.apk' % ts)
+ dest_apk_signed = os.path.join(tmp_dir, 'app-signed-%s.apk' % ts)
+ shutil.copy2(apk, dest_apk)
+ zip_utils.add_file_to_zip(
+ baseline_profile, 'assets/dexopt/baseline.prof', dest_apk)
+ align(dest_apk, dest_apk_aligned)
+ sign_with_apksigner(dest_apk_aligned, dest_apk_signed)
+ return dest_apk_signed
+
+def align(apk, aligned_apk):
+ zipalign_path = (
+ 'zipalign' if 'build_tools' in os.environ.get('PATH')
+ else os.path.join(utils.getAndroidBuildTools(), 'zipalign'))
+ cmd = [zipalign_path, '-f', '4', apk, aligned_apk]
+ utils.RunCmd(cmd, quiet=True, logging=False)
+ return aligned_apk
+
+def default_keystore():
+ return os.path.join(os.getenv('HOME'), '.android', 'app.keystore')
def sign(unsigned_apk, signed_apk, keystore, quiet=False, logging=True):
utils.Print('Signing (ignore the warnings)', quiet=quiet)
@@ -52,20 +80,20 @@
utils.RunCmd(cmd, quiet=quiet)
def sign_with_apksigner(
- unsigned_apk, signed_apk, keystore, password='android', quiet=False,
+ unsigned_apk, signed_apk, keystore=None, password='android', quiet=False,
logging=True):
cmd = [
os.path.join(utils.getAndroidBuildTools(), 'apksigner'),
'sign',
'-v',
- '--ks', keystore,
+ '--ks', keystore or default_keystore(),
'--ks-pass', 'pass:' + password,
'--min-sdk-version', '19',
'--out', signed_apk,
unsigned_apk
]
utils.RunCmd(cmd, quiet=quiet, logging=logging)
-
+ return signed_apk
def main():
(options, apk) = parse_options()
diff --git a/tools/startup/adb_utils.py b/tools/startup/adb_utils.py
index 589bc7f..8dba4a2 100644
--- a/tools/startup/adb_utils.py
+++ b/tools/startup/adb_utils.py
@@ -49,6 +49,11 @@
def is_on_and_unlocked(self):
return self == ScreenState.ON_UNLOCKED
+def broadcast(action, component, device_id=None):
+ print('Sending broadcast %s' % action)
+ cmd = create_adb_cmd('shell am broadcast -a %s %s' % (action, component), device_id)
+ return subprocess.check_output(cmd).decode('utf-8').strip().splitlines()
+
def create_adb_cmd(arguments, device_id=None):
assert isinstance(arguments, list) or isinstance(arguments, str)
cmd = ['adb']
@@ -91,11 +96,13 @@
subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
def force_compilation(app_id, device_id=None):
+ print('Applying AOT (full)')
cmd = create_adb_cmd(
'shell cmd package compile -m speed -f %s' % app_id, device_id)
subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
def force_profile_compilation(app_id, device_id=None):
+ print('Applying AOT (profile)')
cmd = create_adb_cmd(
'shell cmd package compile -m speed-profile -f %s' % app_id, device_id)
subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
@@ -205,10 +212,23 @@
return screen_off_timeout
def install(apk, device_id=None):
+ print('Installing %s' % apk)
cmd = create_adb_cmd('install %s' % apk, device_id)
stdout = subprocess.check_output(cmd).decode('utf-8')
assert 'Success' in stdout
+def install_profile(app_id, device_id=None):
+ # This assumes that the profileinstaller library has been added to the app,
+ # https://developer.android.com/jetpack/androidx/releases/profileinstaller.
+ action = 'androidx.profileinstaller.action.INSTALL_PROFILE'
+ component = '%s/androidx.profileinstaller.ProfileInstallReceiver' % app_id
+ stdout = broadcast(action, component, device_id)
+ assert len(stdout) == 2
+ assert stdout[0] == ('Broadcasting: Intent { act=%s flg=0x400000 cmp=%s }' % (action, component))
+ assert stdout[1] == 'Broadcast completed: result=1', stdout[1]
+ stop_app(app_id, device_id)
+ force_profile_compilation(app_id, device_id)
+
def issue_key_event(key_event, device_id=None, sleep_in_seconds=1):
cmd = create_adb_cmd('shell input keyevent %s' % key_event, device_id)
stdout = subprocess.check_output(cmd).decode('utf-8').strip()
@@ -280,6 +300,7 @@
return logcat_reader.lines
def stop_app(app_id, device_id=None):
+ print('Shutting down %s' % app_id)
cmd = create_adb_cmd('shell am force-stop %s' % app_id, device_id)
subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
@@ -290,6 +311,7 @@
device_id)
def uninstall(app_id, device_id=None):
+ print('Uninstalling %s' % app_id)
cmd = create_adb_cmd('uninstall %s' % app_id, device_id)
process_result = subprocess.run(cmd, capture_output=True)
stdout = process_result.stdout.decode('utf-8')
@@ -299,7 +321,8 @@
else:
expected_error = (
'java.lang.IllegalArgumentException: Unknown package: %s' % app_id)
- assert expected_error in stderr
+ assert 'Failure [DELETE_FAILED_INTERNAL_ERROR]' in stdout \
+ or expected_error in stderr
def unlock(device_id=None, device_pin=None):
screen_state = get_screen_state(device_id)
diff --git a/tools/startup/measure_startup.py b/tools/startup/measure_startup.py
index 080f827..d9e0801 100755
--- a/tools/startup/measure_startup.py
+++ b/tools/startup/measure_startup.py
@@ -20,6 +20,7 @@
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import adb_utils
+import apk_utils
import perfetto_utils
import utils
@@ -45,13 +46,13 @@
tear_down_options['previous_screen_off_timeout'],
options.device_id)
-def run_all(options, tmp_dir):
+def run_all(apk, options, tmp_dir):
# Launch app while collecting information.
data_avg = {}
for iteration in range(options.iterations):
print('Starting iteration %i' % iteration)
out_dir = os.path.join(options.out_dir, str(iteration))
- prepare_for_run(out_dir, options)
+ prepare_for_run(apk, out_dir, options)
data = run(out_dir, options, tmp_dir)
add_data(data_avg, data)
print("Result:")
@@ -64,20 +65,16 @@
print(data_avg)
write_data(options.out_dir, data_avg)
-def prepare_for_run(out_dir, options):
+def prepare_for_run(apk, out_dir, options):
adb_utils.root(options.device_id)
adb_utils.uninstall(options.app_id, options.device_id)
- adb_utils.install(options.apk, options.device_id)
- adb_utils.clear_profile_data(options.app_id, options.device_id)
+ adb_utils.install(apk, options.device_id)
if options.aot:
- adb_utils.force_compilation(options.app_id, options.device_id)
- elif options.aot_profile:
- adb_utils.launch_activity(
- options.app_id, options.main_activity, options.device_id)
- time.sleep(options.aot_profile_sleep)
- adb_utils.stop_app(options.app_id, options.device_id)
- adb_utils.force_profile_compilation(options.app_id, options.device_id)
-
+ 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)
adb_utils.drop_caches(options.device_id)
os.makedirs(out_dir, exist_ok=True)
@@ -87,9 +84,9 @@
# Start perfetto trace collector.
perfetto_process = None
perfetto_trace_path = None
- if not options.no_perfetto:
+ if options.perfetto:
perfetto_process, perfetto_trace_path = perfetto_utils.record_android_trace(
- out_dir, tmp_dir)
+ out_dir, tmp_dir, options.device_id)
# Launch main activity.
launch_activity_result = adb_utils.launch_activity(
@@ -99,7 +96,7 @@
wait_for_activity_to_launch=True)
# Wait for perfetto trace collector to stop.
- if not options.no_perfetto:
+ if options.perfetto:
perfetto_utils.stop_record_android_trace(perfetto_process, out_dir)
# Get minor and major page faults from app process.
@@ -137,16 +134,18 @@
def compute_startup_data(launch_activity_result, perfetto_trace_path, options):
startup_data = {
- 'time_to_activity_started_ms': launch_activity_result.get('total_time')
+ 'adb_startup': launch_activity_result.get('total_time')
}
perfetto_startup_data = {}
- if not options.no_perfetto:
+ if options.perfetto:
trace_processor = TraceProcessor(file_path=perfetto_trace_path)
- # Compute time to first frame according to the builtin android_startup metric.
+ # 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)
# Compute time to first and last doFrame event.
bind_application_slice = perfetto_utils.find_unique_slice_by_name(
@@ -158,15 +157,14 @@
first_do_frame_slice = next(do_frame_slices)
*_, last_do_frame_slice = do_frame_slices
- perfetto_startup_data = {
- 'time_to_first_frame_ms': round(time_to_first_frame_ms),
+ 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
@@ -191,10 +189,6 @@
help='Enable force compilation using profiles',
default=False,
action='store_true')
- result.add_argument('--aot-profile-sleep',
- help='Duration in seconds before forcing compilation',
- default=15,
- type=int)
result.add_argument('--apk',
help='Path to the APK',
required=True)
@@ -216,16 +210,22 @@
result.add_argument('--out-dir',
help='Directory to store trace files in',
required=True)
+ result.add_argument('--baseline-profile',
+ help='Baseline profile to install')
options, args = result.parse_known_args(argv)
- assert (not options.aot) or (not options.aot_profile)
+ 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 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)
tear_down_options = adb_utils.prepare_for_interaction_with_device(
options.device_id, options.device_pin)
- run_all(options, tmp_dir)
+ run_all(apk, options, tmp_dir)
adb_utils.tear_down_after_interaction_with_device(
tear_down_options, options.device_id)
diff --git a/tools/startup/perfetto_utils.py b/tools/startup/perfetto_utils.py
index d85f53e..d2f53b4 100644
--- a/tools/startup/perfetto_utils.py
+++ b/tools/startup/perfetto_utils.py
@@ -31,7 +31,7 @@
assert os.path.exists(record_android_trace_path)
return record_android_trace_path
-def record_android_trace(out_dir, tmp_dir):
+def record_android_trace(out_dir, tmp_dir, device_id=None):
record_android_trace_path = ensure_record_android_trace(tmp_dir)
config_path = os.path.join(os.path.dirname(__file__), 'config.pbtx')
perfetto_trace_path = os.path.join(out_dir, 'trace.perfetto-trace')
@@ -43,6 +43,8 @@
'--out',
perfetto_trace_path,
'--no-open']
+ if device_id is not None:
+ cmd.extend(['--serial', device_id])
perfetto_process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
lines = []
diff --git a/tools/zip_utils.py b/tools/zip_utils.py
new file mode 100644
index 0000000..b0571f2
--- /dev/null
+++ b/tools/zip_utils.py
@@ -0,0 +1,10 @@
+#!/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 zipfile
+
+def add_file_to_zip(file, destination, zip_file):
+ with zipfile.ZipFile(zip_file, 'a') as zip:
+ zip.write(file, destination)