| Index: build/android/incremental_install/installer.py
 | 
| diff --git a/build/android/incremental_install.py b/build/android/incremental_install/installer.py
 | 
| similarity index 55%
 | 
| rename from build/android/incremental_install.py
 | 
| rename to build/android/incremental_install/installer.py
 | 
| index d88a04d6d7680bcc2419e631af7a71d2608c2c69..1d70335491e3016a155c6763e8ff25bd9817df31 100755
 | 
| --- a/build/android/incremental_install.py
 | 
| +++ b/build/android/incremental_install/installer.py
 | 
| @@ -9,20 +9,32 @@
 | 
|  import argparse
 | 
|  import glob
 | 
|  import logging
 | 
| +import os
 | 
|  import posixpath
 | 
| +import shutil
 | 
|  import sys
 | 
| -import time
 | 
|  
 | 
| +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
 | 
|  from devil.android import apk_helper
 | 
|  from devil.android import device_utils
 | 
|  from devil.android import device_errors
 | 
| +from devil.android.sdk import version_codes
 | 
|  from devil.utils import reraiser_thread
 | 
|  from pylib import constants
 | 
|  from pylib.utils import run_tests_helper
 | 
| +from pylib.utils import time_profile
 | 
| +
 | 
| +sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
 | 
| +from util import build_utils
 | 
| +
 | 
| +
 | 
| +def _TransformDexPaths(paths):
 | 
| +  """Given paths like ["/a/b/c", "/a/c/d"], returns ["b.c", "c.d"]."""
 | 
| +  prefix_len = len(os.path.commonprefix(paths))
 | 
| +  return [p[prefix_len:].replace(os.sep, '.') for p in paths]
 | 
|  
 | 
|  
 | 
|  def main():
 | 
| -  start_time = time.time()
 | 
|    parser = argparse.ArgumentParser()
 | 
|    parser.add_argument('apk_path',
 | 
|                        help='The path to the APK to install.')
 | 
| @@ -33,6 +45,10 @@ def main():
 | 
|                             'Can be specified multiple times.')
 | 
|    parser.add_argument('--lib-dir',
 | 
|                        help='Path to native libraries directory.')
 | 
| +  parser.add_argument('--dex-files',
 | 
| +                      help='List of dex files to push.',
 | 
| +                      action='append',
 | 
| +                      default=[])
 | 
|    parser.add_argument('-d', '--device', dest='device',
 | 
|                        help='Target device for apk to install on.')
 | 
|    parser.add_argument('--uninstall',
 | 
| @@ -54,12 +70,16 @@ def main():
 | 
|  
 | 
|    args = parser.parse_args()
 | 
|  
 | 
| -  logging.basicConfig(format='%(asctime)s (%(thread)d) %(message)s')
 | 
|    run_tests_helper.SetLogLevel(args.verbose_count)
 | 
|    constants.SetBuildType('Debug')
 | 
|    if args.output_directory:
 | 
|      constants.SetOutputDirectory(args.output_directory)
 | 
|  
 | 
| +  main_timer = time_profile.TimeProfile()
 | 
| +  install_timer = time_profile.TimeProfile()
 | 
| +  push_native_timer = time_profile.TimeProfile()
 | 
| +  push_dex_timer = time_profile.TimeProfile()
 | 
| +
 | 
|    if args.device:
 | 
|      # Retries are annoying when commands fail for legitimate reasons. Might want
 | 
|      # to enable them if this is ever used on bots though.
 | 
| @@ -80,19 +100,26 @@ def main():
 | 
|          msg += '  %s (%s)\n' % (d, desc)
 | 
|        raise Exception(msg)
 | 
|  
 | 
| -  apk_package = apk_helper.ApkHelper(args.apk_path).GetPackageName()
 | 
| +  apk_help = apk_helper.ApkHelper(args.apk_path)
 | 
| +  apk_package = apk_help.GetPackageName()
 | 
|    device_incremental_dir = '/data/local/tmp/incremental-app-%s' % apk_package
 | 
|  
 | 
|    if args.uninstall:
 | 
| -    logging.info('Uninstalling .apk')
 | 
|      device.Uninstall(apk_package)
 | 
| -    logging.info('Removing side-loaded files')
 | 
|      device.RunShellCommand(['rm', '-rf', device_incremental_dir],
 | 
|                             check_return=True)
 | 
| +    logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
 | 
|      return
 | 
|  
 | 
| +  if device.build_version_sdk >= version_codes.MARSHMALLOW:
 | 
| +    if apk_help.HasIsolatedProcesses():
 | 
| +      raise Exception('Cannot use perform incremental installs on Android M+ '
 | 
| +                      'without first disabling isolated processes. Use GN arg: '
 | 
| +                      'disable_incremental_isolated_processes=true to do so.')
 | 
| +
 | 
|    # Install .apk(s) if any of them have changed.
 | 
|    def do_install():
 | 
| +    install_timer.Start()
 | 
|      if args.splits:
 | 
|        splits = []
 | 
|        for split_glob in args.splits:
 | 
| @@ -101,25 +128,60 @@ def main():
 | 
|                               allow_cached_props=True)
 | 
|      else:
 | 
|        device.Install(args.apk_path, reinstall=True)
 | 
| -    logging.info('Finished installing .apk')
 | 
| +    install_timer.Stop(log=False)
 | 
|  
 | 
| -  # Push .so files to the device (if they have changed).
 | 
| -  def do_push_libs():
 | 
| +  # Push .so and .dex files to the device (if they have changed).
 | 
| +  def do_push_files():
 | 
|      if args.lib_dir:
 | 
| +      push_native_timer.Start()
 | 
|        device_lib_dir = posixpath.join(device_incremental_dir, 'lib')
 | 
|        device.PushChangedFiles([(args.lib_dir, device_lib_dir)],
 | 
|                                delete_device_stale=True)
 | 
| -      logging.info('Finished pushing native libs')
 | 
| +      push_native_timer.Stop(log=False)
 | 
| +
 | 
| +    if args.dex_files:
 | 
| +      push_dex_timer.Start()
 | 
| +      # Put all .dex files to be pushed into a temporary directory so that we
 | 
| +      # can use delete_device_stale=True.
 | 
| +      with build_utils.TempDir() as temp_dir:
 | 
| +        device_dex_dir = posixpath.join(device_incremental_dir, 'dex')
 | 
| +        # Ensure no two files have the same name.
 | 
| +        transformed_names = _TransformDexPaths(args.dex_files)
 | 
| +        for src_path, dest_name in zip(args.dex_files, transformed_names):
 | 
| +          shutil.copyfile(src_path, os.path.join(temp_dir, dest_name))
 | 
| +        device.PushChangedFiles([(temp_dir, device_dex_dir)],
 | 
| +                                delete_device_stale=True)
 | 
| +      push_dex_timer.Stop(log=False)
 | 
| +
 | 
| +  # Create 2 lock files:
 | 
| +  # * install.lock tells the app to pause on start-up (until we release it).
 | 
| +  # * firstrun.lock is used by the app to pause all secondary processes until
 | 
| +  #   the primary process finishes loading the .dex / .so files.
 | 
| +  def create_lock_files():
 | 
| +    # Creates or zeros out lock files.
 | 
| +    cmd = ('D="%s";'
 | 
| +           'mkdir -p $D &&'
 | 
| +           'echo -n >$D/install.lock 2>$D/firstrun.lock')
 | 
| +    device.RunShellCommand(cmd % device_incremental_dir, check_return=True)
 | 
| +
 | 
| +  # The firstrun.lock is released by the app itself.
 | 
| +  def release_installer_lock():
 | 
| +    device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir,
 | 
| +                           check_return=True)
 | 
|  
 | 
| +  create_lock_files()
 | 
|    # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't
 | 
|    # been designed for multi-threading. Enabling only because this is a
 | 
|    # developer-only tool.
 | 
|    if args.no_threading:
 | 
|      do_install()
 | 
| -    do_push_libs()
 | 
| +    do_push_files()
 | 
|    else:
 | 
| -    reraiser_thread.RunAsync((do_install, do_push_libs))
 | 
| -  logging.info('Took %s seconds', round(time.time() - start_time, 1))
 | 
| +    reraiser_thread.RunAsync((do_install, do_push_files))
 | 
| +  release_installer_lock()
 | 
| +  logging.info('Took %s seconds (install=%s, libs=%s, dex=%s)',
 | 
| +               main_timer.GetDelta(), install_timer.GetDelta(),
 | 
| +               push_native_timer.GetDelta(), push_dex_timer.GetDelta())
 | 
|  
 | 
|  
 | 
|  if __name__ == '__main__':
 | 
| 
 |