Benchmarks for various keep rules.

Change-Id: I0dfd6f7bd645810e85cdf3aa0a5491de7e648ff8
diff --git a/tools/keeprule_benchmark.py b/tools/keeprule_benchmark.py
new file mode 100755
index 0000000..6594688
--- /dev/null
+++ b/tools/keeprule_benchmark.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python
+# Copyright (c) 2020, 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 os
+import subprocess
+import sys
+import time
+
+import jdk
+import utils
+
+SHRINKERS = ['r8', 'pg']
+
+INPUT_PROGRAM = utils.PINNED_R8_JAR
+
+ANNO = 'com.android.tools.r8.com.google.common.annotations.VisibleForTesting'
+
+R8_OPTIONS = [
+  'printTimes',
+  'passthroughDexCode',
+  'enableClassMerging',
+  'enableDevirtualization',
+  'enableNonNullTracking',
+  'enableInlining',
+  'enableSwitchMapRemoval',
+  'enableValuePropagation',
+  'useSmaliSyntax',
+  'verbose',
+  'quiet',
+  'invalidDebugInfoFatal',
+  'intermediate',
+  'enableLambdaMerging',
+  'enableDesugaring',
+  'enableMainDexListCheck',
+  'enableTreeShaking',
+  'printCfg',
+  'ignoreMissingClasses',
+  'forceProguardCompatibility',
+  'enableMinification',
+  'disableAssertions',
+  'debugKeepRules',
+  'debug',
+  'minimalMainDex',
+  'skipReadingDexCode',
+]
+
+R8_CLASSES = [
+  'com.android.tools.r8.code.Format11x',
+  'com.android.tools.r8.code.MoveFrom16',
+  'com.android.tools.r8.code.AddLong2Addr',
+  'com.android.tools.r8.code.AgetByte',
+  'com.android.tools.r8.code.SubDouble',
+  'com.android.tools.r8.code.Sput',
+  'com.android.tools.r8.code.Format10x',
+  'com.android.tools.r8.code.RemInt',
+  'com.android.tools.r8.code.ConstWide',
+  'com.android.tools.r8.code.SgetWide',
+  'com.android.tools.r8.code.OrInt2Addr',
+  'com.android.tools.r8.code.Iget',
+  'com.android.tools.r8.code.Instruction',
+  'com.android.tools.r8.code.SubInt2Addr',
+  'com.android.tools.r8.code.SwitchPayload',
+  'com.android.tools.r8.code.Const4',
+  'com.android.tools.r8.code.ShrIntLit8',
+  'com.android.tools.r8.code.ConstWide16',
+  'com.android.tools.r8.code.NegInt',
+  'com.android.tools.r8.code.SgetBoolean',
+  'com.android.tools.r8.code.Format22x',
+  'com.android.tools.r8.code.InvokeVirtualRange',
+  'com.android.tools.r8.code.Format45cc',
+  'com.android.tools.r8.code.DivFloat2Addr',
+  'com.android.tools.r8.code.MulIntLit16',
+  'com.android.tools.r8.code.BytecodeStream',
+]
+
+KEEP_MAIN = \
+  '-keep class com.android.tools.r8.R8 { void main(java.lang.String[]); }'
+
+BENCHMARKS = [
+  # Baseline compile just keeps R8.main (implicitly kept for all benchmarks).
+  ('KeepBaseline', ''),
+
+  # Mirror default keep getters/setters, but independent of hierarchy.
+  ('KeepGetters',
+    '-keepclassmembers class * { *** get*(); }'),
+  ('KeepGettersIf',
+    '-if class * { *** get*(); } -keep class <1> { *** get<2>(); }'),
+
+  # Mirror default keep getters/setters below View (here a class with a B).
+  ('KeepSubGetters',
+    '-keepclassmembers class * extends **.*B* { *** get*(); }'),
+  ('KeepSubGettersIf',
+    '-if class * extends **.*B* -keep class <1> { *** get*(); }'),
+
+  # General keep rule to keep annotated members.
+  ('KeepAnnoMethod',
+    '-keepclasseswithmembers class * { @%s *** *(...); }' % ANNO),
+  ('KeepAnnoMethodCond',
+    '-keepclassmembers class * { @%s *** *(...); }' % ANNO),
+  ('KeepAnnoMethodIf',
+    '-if class * { @%s *** *(...); } -keep class <1> { @%s *** <2>(...); }' \
+        % (ANNO, ANNO)),
+
+  # Large collection of rules mirroring AAPT conditional rules on R fields.
+  ('KeepAaptFieldIf',
+   '\n'.join([
+     '-if class **.InternalOptions { boolean %s; }'
+     ' -keep class %s { <init>(...); }' % (f, c)
+     for (f, c) in zip(R8_OPTIONS, R8_CLASSES) * 1 #100
+   ])),
+
+  # If rules with predicates that will never by true, but will need
+  # consideration. The CodeSize of these should be equal to the baseline run.
+  ('KeepIfNonExistingClass',
+   '-if class **.*A*B*C*D*E*F* -keep class %s' % ANNO),
+  ('KeepIfNonExistingMember',
+   '-if class **.*A* { *** *a*b*c*d*e*f*(...); } -keep class %s' % ANNO)
+]
+
+def parse_arguments(argv):
+  parser = argparse.ArgumentParser(
+                    description = 'Run keep-rule benchmarks.')
+  parser.add_argument('--golem',
+                    help = 'Link in third party dependencies.',
+                    default = False,
+                    action = 'store_true')
+  parser.add_argument('--ignore-java-version',
+                    help='Do not check java version',
+                    default=False,
+                    action='store_true')
+  parser.add_argument('--shrinker',
+                    help='The shrinker to use',
+                    choices=SHRINKERS,
+                    default=SHRINKERS[0])
+  parser.add_argument('--runs',
+                      help='Number of runs to average out time on',
+                      type=int,
+                      default=3)
+  parser.add_argument('--benchmark',
+                      help='Benchmark to run (default all)',
+                      choices=map(lambda (x,y): x, BENCHMARKS),
+                      default=None)
+  options = parser.parse_args(argv)
+  return options
+
+class BenchmarkResult:
+  def __init__(self, name, size, runs):
+    self.name = name
+    self.size = size
+    self.runs = runs
+
+def shrinker_args(shrinker, keepfile, output):
+  if shrinker == 'r8':
+    return [
+      jdk.GetJavaExecutable(),
+      '-cp', utils.R8LIB_JAR,
+      'com.android.tools.r8.R8',
+      INPUT_PROGRAM,
+      '--lib', utils.RT_JAR,
+      '--output', output,
+      '--classfile',
+      '--pg-conf', keepfile,
+      ]
+  elif shrinker == 'pg':
+    return [
+      jdk.GetJavaExecutable(),
+      '-jar', utils.PROGUARD_JAR,
+      '-injars', INPUT_PROGRAM,
+      '-libraryjars', utils.RT_JAR,
+      '-outjars', output,
+      '-dontwarn', '**',
+      '-optimizationpasses', '2',
+      '@' + keepfile,
+    ]
+  else:
+    assert False, "Unexpected shrinker " + shrinker
+
+def run_shrinker(options, temp):
+  benchmarks = BENCHMARKS
+  if options.benchmark:
+    for (name, rules) in BENCHMARKS:
+      if name == options.benchmark:
+        benchmarks = [(name, rules)]
+        break
+    assert len(benchmarks) == 1, "Unexpected benchmark " + options.benchmark
+
+  run_count = options.runs
+  benchmark_results = []
+  for (name, rule) in benchmarks:
+    benchmark_keep = os.path.join(temp, '%s-keep.txt' % name)
+    with open(benchmark_keep, 'w') as fp:
+      fp.write(KEEP_MAIN)
+      fp.write('\n')
+      fp.write(rule)
+
+    benchmark_runs = []
+    benchmark_size = 0
+    for i in range(run_count):
+      out = os.path.join(temp, '%s-out%d.jar' % (name, i))
+      cmd = shrinker_args(options.shrinker, benchmark_keep, out)
+      utils.PrintCmd(cmd)
+      t0 = time.time()
+      subprocess.check_output(cmd)
+      t1 = time.time()
+      benchmark_runs.append(t1 - t0)
+      benchmark_size = utils.uncompressed_size(out)
+    benchmark_results.append(
+        BenchmarkResult(name, benchmark_size, benchmark_runs))
+
+  print 'Runs:', options.runs
+  for result in benchmark_results:
+    benchmark_avg = sum(result.runs) / run_count
+    print '%s(CodeSize): %d' % (result.name, result.size)
+    print '%s(RunTimeRaw): %d ms' % (result.name, 1000.0 * benchmark_avg)
+
+if __name__ == '__main__':
+  options = parse_arguments(sys.argv[1:])
+  if options.golem:
+    golem.link_third_party()
+  if not options.ignore_java_version:
+    utils.check_java_version()
+  with utils.TempDir() as temp:
+    run_shrinker(options, temp)