Script to generalize synthetic descriptors in ART profile

Change-Id: I6ec4482c8cf1d16bd55d6e1b624c3123a18d6d5d
diff --git a/tools/startup/adb_utils.py b/tools/startup/adb_utils.py
index 44f70aa..60a7ee3 100755
--- a/tools/startup/adb_utils.py
+++ b/tools/startup/adb_utils.py
@@ -14,6 +14,7 @@
 
 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
 
+import profile_utils
 import utils
 
 DEVNULL=subprocess.DEVNULL
@@ -199,39 +200,13 @@
 def get_classes_and_methods_from_app_profile(app_id, device_id=None):
   apk_path = get_apk_path(app_id, device_id)
   profile_path = get_profile_path(app_id)
-
-  # Generates a list of class and method descriptors, prefixed with one or more
-  # flags 'H' (hot), 'S' (startup), 'P' (post startup).
-  #
-  # Example:
-  #
-  # HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
-  # HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
-  # HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
-  # PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
-  # HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
-  # Landroidx/compose/runtime/ComposerImpl;
-  #
-  # See also https://developer.android.com/studio/profile/baselineprofiles.
   cmd = create_adb_cmd(
     'shell profman --dump-classes-and-methods'
     ' --profile-file=%s --apk=%s --dex-location=%s'
         % (profile_path, apk_path, apk_path), device_id)
   stdout = subprocess.check_output(cmd).decode('utf-8').strip()
   lines = stdout.splitlines()
-  classes_and_methods = {}
-  flags_to_name = { 'H': 'hot', 'S': 'startup', 'P': 'post_startup' }
-  for line in lines:
-    flags = { 'hot': False, 'startup': False, 'post_startup': False }
-    while line[0] in flags_to_name:
-      flag_abbreviation = line[0]
-      flag_name = flags_to_name.get(flag_abbreviation)
-      flags[flag_name] = True
-      line = line[1:]
-    assert line.startswith('L')
-    descriptor = line
-    classes_and_methods[descriptor] = flags
-  return classes_and_methods
+  return profile_utils.parse_art_profile(lines)
 
 def get_screen_off_timeout(device_id=None):
   cmd = create_adb_cmd(
diff --git a/tools/startup/generate_startup_descriptors.py b/tools/startup/generate_startup_descriptors.py
index c4fa05a..ee3f27a 100755
--- a/tools/startup/generate_startup_descriptors.py
+++ b/tools/startup/generate_startup_descriptors.py
@@ -4,6 +4,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 import adb_utils
+import profile_utils
 
 import argparse
 import os
@@ -28,12 +29,13 @@
     write_tmp_profile_classes_and_methods(
         profile_classes_and_methods, iteration, options)
     current_startup_descriptors = \
-        transform_classes_and_methods_to_r8_startup_descriptors(
+        profile_utils.transform_art_profile_to_r8_startup_list(
             profile_classes_and_methods)
   write_tmp_startup_descriptors(current_startup_descriptors, iteration, options)
   new_startup_descriptors = add_r8_startup_descriptors(
       startup_descriptors, current_startup_descriptors)
-  number_of_new_startup_descriptors = len(new_startup_descriptors) - len(startup_descriptors)
+  number_of_new_startup_descriptors = \
+      len(new_startup_descriptors) - len(startup_descriptors)
   if options.out is not None:
     print(
         'Found %i new startup descriptors in iteration %i'
@@ -167,16 +169,6 @@
 def report_unrecognized_logcat_line(line):
   print('Unrecognized line in logcat: %s' % line)
 
-def transform_classes_and_methods_to_r8_startup_descriptors(
-    classes_and_methods):
-  startup_descriptors = {}
-  for startup_descriptor, flags in classes_and_methods.items():
-    startup_descriptors[startup_descriptor] = {
-      'conditional_startup': False,
-      'post_startup': flags['post_startup']
-    }
-  return startup_descriptors
-
 def add_r8_startup_descriptors(old_startup_descriptors, startup_descriptors_to_add):
   new_startup_descriptors = {}
   if len(old_startup_descriptors) == 0:
diff --git a/tools/startup/profile_utils.py b/tools/startup/profile_utils.py
new file mode 100755
index 0000000..0e69397
--- /dev/null
+++ b/tools/startup/profile_utils.py
@@ -0,0 +1,105 @@
+#!/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 sys
+
+COMPANION_CLASS_SUFFIX = '$-CC'
+EXTERNAL_SYNTHETIC_SUFFIX = '$$ExternalSynthetic'
+SYNTHETIC_PREFIX = 'S'
+
+# Parses a list of class and method descriptors, prefixed with one or more flags
+# 'H' (hot), 'S' (startup), 'P' (post startup).
+#
+# Example:
+#
+# HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
+# HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
+# HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
+# PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
+# HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
+# Landroidx/compose/runtime/ComposerImpl;
+#
+# See also https://developer.android.com/studio/profile/baselineprofiles.
+def parse_art_profile(lines):
+  art_profile = {}
+  flags_to_name = { 'H': 'hot', 'S': 'startup', 'P': 'post_startup' }
+  for line in lines:
+    line = line.strip()
+    if not line:
+      continue
+    flags = { 'hot': False, 'startup': False, 'post_startup': False }
+    while line[0] in flags_to_name:
+      flag_abbreviation = line[0]
+      flag_name = flags_to_name.get(flag_abbreviation)
+      flags[flag_name] = True
+      line = line[1:]
+    assert line.startswith('L')
+    descriptor = line
+    art_profile[descriptor] = flags
+  return art_profile
+
+def transform_art_profile_to_r8_startup_list(art_profile):
+  r8_startup_list = {}
+  for startup_descriptor, flags in art_profile.items():
+    transformed_startup_descriptor = transform_synthetic_descriptor(
+        startup_descriptor)
+    r8_startup_list[transformed_startup_descriptor] = {
+      'conditional_startup': False,
+      'post_startup': flags['post_startup'],
+      'startup': flags['startup']
+    }
+  return r8_startup_list
+
+def transform_synthetic_descriptor(descriptor):
+  companion_class_index = descriptor.find(COMPANION_CLASS_SUFFIX)
+  if companion_class_index >= 0:
+    return SYNTHETIC_PREFIX + descriptor[0:companion_class_index] + ';'
+  external_synthetic_index = descriptor.find(EXTERNAL_SYNTHETIC_SUFFIX)
+  if external_synthetic_index >= 0:
+    return SYNTHETIC_PREFIX + descriptor[0:external_synthetic_index] + ';'
+  return descriptor
+
+def filter_r8_startup_list(r8_startup_list, options):
+  filtered_r8_startup_list = {}
+  for startup_descriptor, flags in r8_startup_list.items():
+    if not options.include_post_startup \
+        and flags.get('post_startup') \
+        and not flags.get('startup'):
+      continue
+    filtered_r8_startup_list[startup_descriptor] = flags
+  return filtered_r8_startup_list
+
+def parse_options(argv):
+  result = argparse.ArgumentParser(
+      description='Utilities for converting an ART profile into an R8 startup '
+                  'list.')
+  result.add_argument('--art-profile', help='Path to the ART profile')
+  result.add_argument('--include-post-startup',
+                      help='Include post startup classes and methods in the R8 '
+                           'startup list',
+                      action='store_true',
+                      default=False)
+  result.add_argument('--out', help='Where to store the R8 startup list')
+  options, args = result.parse_known_args(argv)
+  return options, args
+
+def main(argv):
+  (options, args) = parse_options(argv)
+  with open(options.art_profile, 'r') as f:
+    art_profile = parse_art_profile(f.read().splitlines())
+  r8_startup_list = transform_art_profile_to_r8_startup_list(art_profile)
+  filtered_r8_startup_list = filter_r8_startup_list(r8_startup_list, options)
+  if options.out is not None:
+    with open(options.out, 'w') as f:
+      for startup_descriptor, flags in filtered_r8_startup_list.items():
+        f.write(startup_descriptor)
+        f.write('\n')
+  else:
+    for startup_descriptor, flags in filtered_r8_startup_list.items():
+      print(startup_descriptor)
+
+if __name__ == '__main__':
+  sys.exit(main(sys.argv[1:]))