Support for identifying startup classes from instrumentation

This also updates generate_startup_descriptors.py to store the artifacts in an (optional) out directory, so that the generated profiles are accessible after script executing.

Change-Id: I3fa67d13d10a110f6c32e0696297f8a292b5c759
diff --git a/tools/startup/adb_utils.py b/tools/startup/adb_utils.py
index 981bcbd..589bc7f 100644
--- a/tools/startup/adb_utils.py
+++ b/tools/startup/adb_utils.py
@@ -4,11 +4,33 @@
 # BSD-style license that can be found in the LICENSE file.
 
 from enum import Enum
+import os
 import subprocess
+import sys
+import threading
 import time
 
+sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import utils
+
 DEVNULL=subprocess.DEVNULL
 
+class ProcessReader(threading.Thread):
+
+  def __init__(self, process):
+    threading.Thread.__init__(self)
+    self.lines = []
+    self.process = process
+
+  def run(self):
+    for line in self.process.stdout:
+      line = line.decode('utf-8').strip()
+      self.lines.append(line)
+
+  def stop(self):
+    self.process.kill()
+
 class ScreenState(Enum):
   OFF_LOCKED = 1,
   OFF_UNLOCKED = 2
@@ -39,7 +61,7 @@
 def capture_app_profile_data(app_id, device_id=None):
   cmd = create_adb_cmd(
       'shell killall -s SIGUSR1 %s' % app_id, device_id)
-  subprocess.check_output(cmd)
+  subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
   time.sleep(5)
 
 def check_app_has_profile_data(app_id, device_id=None):
@@ -54,6 +76,10 @@
   if size == 4:
     raise ValueError('Expected size of profile at %s to be > 4K' % profile_path)
 
+def clear_logcat(device_id=None):
+  cmd = create_adb_cmd('logcat -c', device_id)
+  subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
+
 def clear_profile_data(app_id, device_id=None):
   cmd = create_adb_cmd(
       'shell cmd package compile --reset %s' % app_id, device_id)
@@ -86,6 +112,15 @@
         'Expected stdout to end with ".apk", was: %s' % stdout)
   return apk_path
 
+def get_profile_data(app_id, device_id=None):
+  with utils.TempDir() as temp:
+    source = get_profile_path(app_id)
+    target = os.path.join(temp, 'primary.prof')
+    cmd = create_adb_cmd('pull %s %s' % (source, target), device_id)
+    subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
+    with open(target, 'rb') as f:
+      return f.read()
+
 def get_profile_path(app_id):
   return '/data/misc/profiles/cur/0/%s/primary.prof' % app_id
 
@@ -226,6 +261,24 @@
   stdout = subprocess.check_output(cmd).decode('utf-8').strip()
   assert len(stdout) == 0
 
+def start_logcat(device_id=None, format=None, filter=None):
+  args = ['logcat']
+  if format:
+    args.extend(['--format', format])
+  if filter:
+    args.append(filter)
+  cmd = create_adb_cmd(args, device_id)
+  logcat_process = subprocess.Popen(
+      cmd, bufsize=1024*1024, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  reader = ProcessReader(logcat_process)
+  reader.start()
+  return reader
+
+def stop_logcat(logcat_reader):
+  logcat_reader.stop()
+  logcat_reader.join()
+  return logcat_reader.lines
+
 def stop_app(app_id, device_id=None):
   cmd = create_adb_cmd('shell am force-stop %s' % app_id, device_id)
   subprocess.check_call(cmd, stdout=DEVNULL, stderr=DEVNULL)
diff --git a/tools/startup/generate_startup_descriptors.py b/tools/startup/generate_startup_descriptors.py
index ea0fdf3..f1faee43 100755
--- a/tools/startup/generate_startup_descriptors.py
+++ b/tools/startup/generate_startup_descriptors.py
@@ -6,16 +6,23 @@
 import adb_utils
 
 import argparse
+import os
 import sys
 import time
 
 def extend_startup_descriptors(startup_descriptors, iteration, options):
-  generate_startup_profile_on_device(options)
-  classes_and_methods = adb_utils.get_classes_and_methods_from_app_profile(
-      options.app_id, options.device_id)
-  current_startup_descriptors = \
-      transform_classes_and_methods_to_r8_startup_descriptors(
-          classes_and_methods, options)
+  (logcat, profile, profile_classes_and_methods) = \
+      generate_startup_profile(options)
+  if options.logcat:
+    write_tmp_logcat(logcat, iteration, options)
+    current_startup_descriptors = get_r8_startup_descriptors_from_logcat(logcat)
+  else:
+    write_tmp_profile(profile, iteration, options)
+    write_tmp_profile_classes_and_methods(profile_classes_and_methods, iteration, options)
+    current_startup_descriptors = \
+        transform_classes_and_methods_to_r8_startup_descriptors(
+            profile_classes_and_methods, options)
+  write_tmp_startup_descriptors(current_startup_descriptors, iteration, options)
   number_of_new_startup_descriptors = add_r8_startup_descriptors(
       startup_descriptors, current_startup_descriptors)
   if options.out is not None:
@@ -24,15 +31,32 @@
             % (number_of_new_startup_descriptors, iteration + 1))
   return number_of_new_startup_descriptors
 
-def generate_startup_profile_on_device(options):
-  if not options.use_existing_profile:
-    # Clear existing profile data.
-    adb_utils.clear_profile_data(options.app_id, options.device_id)
-
+def generate_startup_profile(options):
+  logcat = None
+  profile = None
+  profile_classes_and_methods = None
+  if options.use_existing_profile:
+    # Verify presence of profile.
+    adb_utils.check_app_has_profile_data(options.app_id, options.device_id)
+    profile = adb_utils.get_profile_data(options.app_id, options.device_id)
+    profile_classes_and_methods = \
+        adb_utils.get_classes_and_methods_from_app_profile(
+            options.app_id, options.device_id)
+  else:
     # Unlock device.
     tear_down_options = adb_utils.prepare_for_interaction_with_device(
         options.device_id, options.device_pin)
 
+    logcat_process = None
+    if options.logcat:
+      # Clear logcat and start capturing logcat.
+      adb_utils.clear_logcat(options.device_id)
+      logcat_process = adb_utils.start_logcat(
+          options.device_id, format='raw', filter='r8:I *:S')
+    else:
+      # Clear existing profile data.
+      adb_utils.clear_profile_data(options.app_id, options.device_id)
+
     # Launch activity to generate startup profile on device.
     adb_utils.launch_activity(
         options.app_id, options.main_activity, options.device_id)
@@ -40,17 +64,36 @@
     # Wait for activity startup.
     time.sleep(options.startup_duration)
 
-    # Capture startup profile.
-    adb_utils.capture_app_profile_data(options.app_id, options.device_id)
+    if options.logcat:
+      # Get startup descriptors from logcat.
+      logcat = adb_utils.stop_logcat(logcat_process)
+    else:
+      # Capture startup profile.
+      adb_utils.capture_app_profile_data(options.app_id, options.device_id)
+      profile = adb_utils.get_profile_data(options.app_id, options.device_id)
+      profile_classes_and_methods = \
+          adb_utils.get_classes_and_methods_from_app_profile(
+              options.app_id, options.device_id)
 
     # Shutdown app.
     adb_utils.stop_app(options.app_id, options.device_id)
-
     adb_utils.tear_down_after_interaction_with_device(
         tear_down_options, options.device_id)
 
-  # Verify presence of profile.
-  adb_utils.check_app_has_profile_data(options.app_id, options.device_id)
+  return (logcat, profile, profile_classes_and_methods)
+
+def get_r8_startup_descriptors_from_logcat(logcat):
+  startup_descriptors = []
+  for line in logcat:
+    if line == '--------- beginning of main':
+      continue
+    if line == '--------- beginning of system':
+      continue
+    if not line.startswith('L') or not line.endswith(';'):
+      print('Unrecognized line in logcat: %s' % line)
+      continue
+    startup_descriptors.append(line)
+  return startup_descriptors
 
 def transform_classes_and_methods_to_r8_startup_descriptors(
     classes_and_methods, options):
@@ -72,6 +115,53 @@
   return new_number_of_startup_descriptors \
       - previous_number_of_startup_descriptors
 
+def write_tmp_binary_artifact(artifact, iteration, options, name):
+  if not options.tmp_dir:
+    return
+  out_dir = os.path.join(options.tmp_dir, str(iteration))
+  os.makedirs(out_dir, exist_ok=True)
+  path = os.path.join(out_dir, name)
+  with open(path, 'wb') as f:
+    f.write(artifact)
+
+def write_tmp_textual_artifact(artifact, iteration, options, name, item_to_string=None):
+  if not options.tmp_dir:
+    return
+  out_dir = os.path.join(options.tmp_dir, str(iteration))
+  os.makedirs(out_dir, exist_ok=True)
+  path = os.path.join(out_dir, name)
+  with open(path, 'w') as f:
+    for item in artifact:
+      f.write(item if item_to_string is None else item_to_string(item))
+      f.write('\n')
+
+def write_tmp_logcat(logcat, iteration, options):
+  write_tmp_textual_artifact(logcat, iteration, options, 'logcat.txt')
+
+def write_tmp_profile(profile, iteration, options):
+  write_tmp_binary_artifact(profile, iteration, options, 'primary.prof')
+
+def write_tmp_profile_classes_and_methods(
+    profile_classes_and_methods, iteration, options):
+  def item_to_string(item):
+    descriptor = item.get('descriptor')
+    flags = item.get('flags')
+    return '%s%s%s%s' % (
+        'H' if flags.get('hot') else '',
+        'S' if flags.get('startup') else '',
+        'P' if flags.get('post_startup') else '',
+        descriptor)
+  write_tmp_textual_artifact(
+      profile_classes_and_methods,
+      iteration,
+      options,
+      'profile.txt',
+      item_to_string)
+
+def write_tmp_startup_descriptors(startup_descriptors, iteration, options):
+  write_tmp_textual_artifact(
+      startup_descriptors, iteration, options, 'startup-descriptors.txt')
+
 def parse_options(argv):
   result = argparse.ArgumentParser(
       description='Generate a perfetto trace file.')
@@ -84,6 +174,9 @@
                       help='Device id (e.g., emulator-5554).')
   result.add_argument('--device-pin',
                       help='Device pin code (e.g., 1234)')
+  result.add_argument('--logcat',
+                      action='store_true',
+                      default=False)
   result.add_argument('--include-post-startup',
                       help='Include post startup classes and methods in the R8 '
                            'startup descriptors',
@@ -102,6 +195,9 @@
                       help='Duration in seconds before shutting down app',
                       default=15,
                       type=int)
+  result.add_argument('--tmp-dir',
+                      help='Directory where to store intermediate artifacts'
+                            ' (by default these are not emitted)')
   result.add_argument('--until-stable',
                       help='Repeat profile generation until no new startup '
                            'descriptors are found',