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)