Output profile information from run_on_as_app.py

Bug: 122586281
Change-Id: I40bdb1bd2b37dfd5a4328ea55f605db2af19d690
diff --git a/tools/as_utils.py b/tools/as_utils.py
index 8b887bf..65ab470 100644
--- a/tools/as_utils.py
+++ b/tools/as_utils.py
@@ -4,6 +4,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 from distutils.version import LooseVersion
+from HTMLParser import HTMLParser
 import os
 import shutil
 
@@ -65,6 +66,32 @@
       if (utils.R8_JAR not in line) and (utils.R8LIB_JAR not in line):
         f.write(line)
 
+def IsGradleTaskName(x):
+  # Check that it is non-empty.
+  if not x:
+    return False
+  # Check that there is no whitespace.
+  for c in x:
+    if c.isspace():
+      return False
+  # Check that the first character following an optional ':' is a lower-case
+  # alphabetic character.
+  c = x[0]
+  if c == ':' and len(x) >= 2:
+    c = x[1]
+  return c.isalpha() and c.islower()
+
+def IsGradleCompilerTask(x, shrinker):
+  if 'r8' in shrinker:
+    assert 'transformClassesWithDexBuilderFor' not in x
+    assert 'transformDexArchiveWithDexMergerFor' not in x
+    return 'transformClassesAndResourcesWithR8For' in x
+
+  assert shrinker == 'proguard'
+  return ('transformClassesAndResourcesWithProguard' in x
+      or 'transformClassesWithDexBuilderFor' in x
+      or 'transformDexArchiveWithDexMergerFor' in x)
+
 def SetPrintConfigurationDirective(app, config, checkout_dir, destination):
   proguard_config_file = FindProguardConfigurationFile(
       app, config, checkout_dir)
@@ -126,3 +153,60 @@
   html_dir = os.path.dirname(html_file)
   for dir_name in ['css', 'js']:
     MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name))
+
+def ParseProfileReport(profile_dir):
+  html_file = os.path.join(profile_dir, 'index.html')
+  assert os.path.isfile(html_file)
+
+  parser = ProfileReportParser()
+  with open(html_file) as f:
+    for line in f.readlines():
+      parser.feed(line)
+  return parser.result
+
+# A simple HTML parser that recognizes the following pattern:
+#
+# <tr>
+# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
+# <td class="numeric">3.490s</td>
+# <td></td>
+# </tr>
+class ProfileReportParser(HTMLParser):
+  entered_table_row = False
+  entered_task_name_cell = False
+  entered_duration_cell = False
+
+  current_task_name = None
+  current_duration = None
+
+  result = {}
+
+  def handle_starttag(self, tag, attrs):
+    entered_table_row_before = self.entered_table_row
+    entered_task_name_cell_before = self.entered_task_name_cell
+
+    self.entered_table_row = (tag == 'tr')
+    self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
+    self.entered_duration_cell = (
+        self.current_task_name
+            and tag == 'td'
+            and entered_task_name_cell_before)
+
+  def handle_endtag(self, tag):
+    if tag == 'tr':
+      if self.current_task_name and self.current_duration:
+        self.result[self.current_task_name] = self.current_duration
+      self.current_task_name = None
+      self.current_duration = None
+    self.entered_table_row = False
+
+  def handle_data(self, data):
+    stripped = data.strip()
+    if not stripped:
+      return
+    if self.entered_task_name_cell:
+      if IsGradleTaskName(stripped):
+        self.current_task_name = stripped
+    elif self.entered_duration_cell and stripped.endswith('s'):
+      self.current_duration = float(stripped[:-1])
+    self.entered_table_row = False
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index deea382..770a651 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -17,7 +17,7 @@
 
 import as_utils
 
-SHRINKERS = ['r8', 'r8full', 'r8-minified', 'r8full-minified', 'proguard']
+SHRINKERS = ['r8', 'r8-minified', 'r8full', 'r8full-minified', 'proguard']
 WORKING_DIR = utils.BUILD
 
 if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']):
@@ -130,6 +130,12 @@
   subprocess.check_call(
       ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest])
 
+def PercentageDiffAsString(before, after):
+  if after < before:
+    return '-' + str(round((1.0 - after / before) * 100)) + '%'
+  else:
+    return '+' + str(round((after - before) / before * 100)) + '%'
+
 def UninstallApkOnEmulator(app, config):
   app_id = config.get('app_id')
   process = subprocess.Popen(
@@ -218,6 +224,11 @@
         result['build_status'] = 'success'
         result['dex_size'] = dex_size
         result['profile_dest_dir'] = profile_dest_dir
+
+        profile = as_utils.ParseProfileReport(profile_dest_dir)
+        result['profile'] = {
+            task_name:duration for task_name, duration in profile.iteritems()
+            if as_utils.IsGradleCompilerTask(task_name, shrinker)}
       except Exception as e:
         warn('Failed to build {} with {}'.format(app, shrinker))
         if e:
@@ -262,6 +273,7 @@
   return result_per_shrinker
 
 def BuildAppWithShrinker(app, config, shrinker, checkout_dir, options):
+  print()
   print('Building {} with {}'.format(app, shrinker))
 
   # Add/remove 'r8.jar' from top-level build.gradle.
@@ -416,8 +428,10 @@
       print('  skipped ({})'.format(error_message))
       continue
 
-    baseline = float(
-        result_per_shrinker.get('proguard', {}).get('dex_size', -1))
+    proguard_result = result_per_shrinker.get('proguard', {})
+    proguard_dex_size = float(proguard_result.get('dex_size', -1))
+    proguard_duration = sum(proguard_result.get('profile', {}).values())
+
     for shrinker in SHRINKERS:
       if shrinker not in result_per_shrinker:
         continue
@@ -428,17 +442,29 @@
       else:
         print('  {}:'.format(shrinker))
         dex_size = result.get('dex_size')
-        if dex_size != baseline and baseline >= 0:
-          if dex_size < baseline:
-            success('    dex size: {} ({}, -{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((1.0 - dex_size / baseline) * 100), 1))
-          elif dex_size >= baseline:
-            warn('    dex size: {} ({}, +{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((baseline - dex_size) / dex_size * 100, 1)))
+        msg = '    dex size: {}'.format(dex_size)
+        if dex_size != proguard_dex_size and proguard_dex_size >= 0:
+          msg = '{} ({}, {})'.format(
+              msg, dex_size - proguard_dex_size,
+              PercentageDiffAsString(proguard_dex_size, dex_size))
+          success(msg) if dex_size < proguard_dex_size else warn(msg)
         else:
-          print('    dex size: {}'.format(dex_size))
+          print(msg)
+
+        profile = result.get('profile')
+        duration = sum(profile.values())
+        msg = '    performance: {}s'.format(duration)
+        if duration != proguard_duration and proguard_duration > 0:
+          msg = '{} ({}s, {})'.format(
+              msg, duration - proguard_duration,
+              PercentageDiffAsString(proguard_duration, duration))
+          success(msg) if duration < proguard_duration else warn(msg)
+        else:
+          print(msg)
+        if len(profile) >= 2:
+          for task_name, task_duration in profile.iteritems():
+            print('      {}: {}s'.format(task_name, task_duration))
+
         if options.monkey:
           monkey_status = result.get('monkey_status')
           if monkey_status != 'success':