Add tool for finding minimum ram we can run in for specific app

Add an argument to run_on_app that makes the tool find the minimum amount of heap space neccesary
to compile the given app with the given compiler

I will add another tool for running this on a range of commits

Bug: 121020829

Change-Id: I88329e85f6cb4d91c41d06a86ca24c45bc5e04ca
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index c5e0f7d..31cecfc 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -24,6 +24,8 @@
 APPS = ['gmscore', 'nest', 'youtube', 'gmail', 'chrome']
 COMPILERS = ['d8', 'r8', 'r8lib-r8', 'r8lib-d8']
 R8_COMPILERS = ['r8', 'r8lib-r8']
+# We use this magic exit code to signal that the program OOM'ed
+OOM_EXIT_CODE = 42
 
 def ParseOptions(argv):
   result = optparse.OptionParser()
@@ -47,6 +49,10 @@
                     help='Run without building first',
                     default=False,
                     action='store_true')
+  result.add_option('--find-min-xmx',
+                    help='Find the minimum amount of memory we can run in',
+                    default=False,
+                    action='store_true')
   result.add_option('--golem',
                     help='Running on golem, do not build or download',
                     default=False,
@@ -160,6 +166,37 @@
       print('Failed %s %s %s with %s' % (name, version, type, compiler))
       exit(exit_code)
 
+def find_min_xmx(options, args):
+  # Args will be destroyed
+  assert len(args) == 0
+  # If we can run in 128 MB then we are good (which we can for small examples
+  # or D8 on medium sized examples)
+  not_working = 128
+  working = 1024 * 8
+  exit_code = 0
+  while working - not_working > 32:
+    next_candidate = working - ((working - not_working)/2)
+    print('working: %s, non_working: %s, next_candidate: %s' %
+          (working, not_working, next_candidate))
+    extra_args = ['-Xmx%sM' % next_candidate]
+    new_options = copy.copy(options)
+    t0 = time.time()
+    exit_code = run_with_options(options, [], extra_args)
+    t1 = time.time()
+    print('Running took: %s ms' % (1000.0 * (t1 - t0)))
+    if exit_code != 0 and exit_code != OOM_EXIT_CODE:
+      print('Non OOM error executing, exiting')
+      return 2
+    if exit_code == 0:
+      working = next_candidate
+    else:
+      assert exit_code == OOM_EXIT_CODE
+      not_working = next_candidate
+
+  assert working - not_working <= 32
+  print('Found range: %s - %s' % (not_working, working))
+  return 0
+
 def main(argv):
   (options, args) = ParseOptions(argv)
   if not options.ignore_java_version:
@@ -167,11 +204,12 @@
 
   if options.run_all:
     return run_all(options, args)
+  if options.find_min_xmx:
+    return find_min_xmx(options, args)
   return run_with_options(options, args)
 
-def run_with_options(options, args):
+def run_with_options(options, args, extra_args=[]):
   app_provided_pg_conf = False;
-  extra_args = []
   # todo(121018500): remove when memory is under control
   extra_args.append('-Xmx8G')
   if options.golem:
@@ -292,14 +330,22 @@
             temp, os.path.abspath(pg_outdir))
         args.extend(['--pg-conf', additional_pg_conf])
       build = not options.no_build and not options.golem
-      exit_code = toolhelper.run(options.compiler, args,
-                     build=build,
-                     debug=not options.no_debug,
-                     profile=options.profile,
-                     track_memory_file=options.track_memory_to_file,
-                     extra_args=extra_args)
+      stderr_path = os.path.join(temp, 'stderr')
+      with open(stderr_path, 'w') as stderr:
+        exit_code = toolhelper.run(options.compiler, args,
+            build=build,
+            debug=not options.no_debug,
+            profile=options.profile,
+            track_memory_file=options.track_memory_to_file,
+            extra_args=extra_args, stderr=stderr)
       if exit_code != 0:
-        return exit_code
+        with open(stderr_path) as stderr:
+          stderr_text = stderr.read()
+          print(stderr_text)
+          if 'java.lang.OutOfMemoryError' in stderr_text:
+            print('Failure was OOM')
+            return OOM_EXIT_CODE
+          return exit_code
 
       if options.print_memoryuse:
         print('{}(MemoryUse): {}'
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index 6a443ac..86787be 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -9,7 +9,8 @@
 import utils
 
 def run(tool, args, build=None, debug=True,
-        profile=False, track_memory_file=None, extra_args=None):
+        profile=False, track_memory_file=None, extra_args=None,
+        stderr=None, stdout=None):
   if build is None:
     build, args = extract_build_from_args(args)
   if build:
@@ -35,7 +36,7 @@
     cmd.extend(["--lib", lib])
   cmd.extend(args)
   utils.PrintCmd(cmd)
-  return subprocess.call(cmd)
+  return subprocess.call(cmd, stdout=stdout, stderr=stderr)
 
 def run_in_tests(tool, args, build=None, debug=True, extra_args=None):
   if build is None: