chromium/tools/binary_size/milestone_apk_sizes.py

#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Prints the large commits given a .csv file from a telemetry size graph."""

# Our version of pylint doesn't know about python3 yet.
# pylint: disable=unexpected-keyword-arg
import argparse
import csv
import json
import os
import posixpath
import logging
import multiprocessing.dummy
import subprocess
import sys
import tempfile
import zipfile

_DIR_SOURCE_ROOT = os.path.normpath(
    os.path.join(os.path.dirname(__file__), '../..'))

sys.path.insert(1, os.path.join(_DIR_SOURCE_ROOT, 'build/android/pylib'))
from utils import app_bundle_utils

_GSUTIL = os.path.join(_DIR_SOURCE_ROOT, 'third_party/depot_tools/gsutil.py')
_RESOURCE_SIZES = os.path.join(_DIR_SOURCE_ROOT,
                               'build/android/resource_sizes.py')
_AAPT2 = os.path.join(_DIR_SOURCE_ROOT,
                      'third_party/android_build_tools/aapt2/cipd/aapt2')
_KEYSTORE = os.path.join(_DIR_SOURCE_ROOT,
                         'build/android/chromium-debug.keystore')
_KEYSTORE_PASSWORD = 'chromium'
_KEYSTORE_ALIAS = 'chromiumdebugkey'


class _Artifact:
  def __init__(self, prefix, name, staging_dir):
    self.name = name
    self._gs_url = posixpath.join(prefix, name)
    self._path = os.path.join(staging_dir, name)
    self._resource_sizes_json = None

    os.makedirs(os.path.dirname(self._path), exist_ok=True)

  def FetchAndMeasure(self):
    args = [_GSUTIL, 'cp', self._gs_url, self._path]
    logging.warning(' '.join(args))
    if not os.path.exists(self._path):
      subprocess.check_call(args)

    path_to_measure = self._path

    if self.name.endswith('.aab'):
      path_to_measure += '.apks'
      app_bundle_utils.GenerateBundleApks(self._path,
                                          path_to_measure,
                                          _AAPT2,
                                          _KEYSTORE,
                                          _KEYSTORE_PASSWORD,
                                          _KEYSTORE_ALIAS,
                                          minimal=True)

    args = [
        _RESOURCE_SIZES,
        '--output-format',
        'chartjson',
        '--output-file',
        '-',
        path_to_measure,
    ]
    logging.warning(' '.join(args))
    self._resource_sizes_json = json.loads(subprocess.check_output(args))

  def GetCompressedSize(self):
    return self._resource_sizes_json['charts']['TransferSize'][
        'Transfer size (deflate)']['value']

  def GetApkSize(self):
    return self._resource_sizes_json['charts']['InstallSize']['APK size'][
        'value']

  def GetAndroidGoSize(self):
    return self._resource_sizes_json['charts']['InstallSize'][
        'Estimated installed size (Android Go)']['value']

  def AddSize(self, metrics):
    metrics[self.name] = self.GetApkSize()

  def AddMethodCount(self, metrics):
    metrics[self.name + ' (method count)'] = self._resource_sizes_json[
        'charts']['Dex']['unique methods']['value']

  def AddDfmSizes(self, metrics, base_name):
    for k, v in sorted(self._resource_sizes_json['charts'].items()):
      if k.startswith('DFM_') and k != 'DFM_test_dummy':
        if k == 'DFM_base':
          name = 'base ({})'.format(base_name)
        else:
          name = k[4:]
        metrics['DFM: ' + name] = v['Size with hindi']['value']


def _DumpCsvAndClear(metrics):
  csv_writer = csv.DictWriter(
      sys.stdout, fieldnames=list(metrics.keys()), delimiter='\t')
  csv_writer.writeheader()
  csv_writer.writerow(metrics)
  metrics.clear()


def _DownloadAndAnalyze(signed_prefix, unsigned_prefix, staging_dir):
  artifacts = []

  def make_artifact(name, prefix=signed_prefix):
    artifacts.append(_Artifact(prefix, name, staging_dir))
    return artifacts[-1]

  webview = make_artifact('arm/AndroidWebviewStable.aab')
  webview64 = make_artifact('arm_64/AndroidWebviewStable.aab')
  monochrome = make_artifact('arm/MonochromeStable.aab')
  monochrome64 = make_artifact('arm_64/MonochromeStable.aab')
  trichrome_chrome = make_artifact('arm/TrichromeChromeGoogleStable.aab')
  trichrome_webview = make_artifact('arm/TrichromeWebViewGoogleStable.aab')
  trichrome_library = make_artifact('arm/TrichromeLibraryGoogleStable.apk')
  trichrome64_chrome = make_artifact('arm_64/TrichromeChromeGoogleStable.aab')
  trichrome64_webview = make_artifact('arm_64/TrichromeWebViewGoogleStable.aab')
  trichrome64_library = make_artifact('arm_64/TrichromeLibraryGoogleStable.apk')
  trichrome64_high_chrome = make_artifact(
      'high-arm_64/TrichromeChromeGoogle6432Stable.aab')
  trichrome64_high_webview = make_artifact(
      'high-arm_64/TrichromeWebViewGoogle6432Stable.aab')
  trichrome64_high_library = make_artifact(
      'high-arm_64/TrichromeLibraryGoogle6432Stable.apk')

  trichrome_system_apks = [
      make_artifact('arm/TrichromeWebViewGoogleSystemStable.apk'),
      make_artifact('arm/TrichromeLibraryGoogleSystemStable.apk'),
      make_artifact(
          'arm/for-signing-only/TrichromeChromeGoogleSystemStable.apk',
          prefix=unsigned_prefix),
  ]
  trichrome64_system_apks = [
      make_artifact('arm_64/TrichromeWebViewGoogleSystemStable.apk'),
      make_artifact('arm_64/TrichromeLibraryGoogleSystemStable.apk'),
      make_artifact(
          'arm_64/for-signing-only/TrichromeChromeGoogleSystemStable.apk',
          prefix=unsigned_prefix),
  ]
  trichrome64_system_apks_high = [
      make_artifact('high-arm_64/TrichromeWebViewGoogle6432SystemStable.apk'),
      make_artifact('high-arm_64/TrichromeLibraryGoogle6432SystemStable.apk'),
      make_artifact(('high-arm_64/for-signing-only/'
                     'TrichromeChromeGoogle6432SystemStable.apk'),
                    prefix=unsigned_prefix),
  ]
  trichrome_system_stubs = [
      make_artifact('arm/TrichromeWebViewGoogleSystemStubStable.apk'),
      make_artifact('arm/TrichromeLibraryGoogleSystemStubStable.apk'),
      make_artifact(
          'arm/for-signing-only/TrichromeChromeGoogleSystemStubStable.apk',
          prefix=unsigned_prefix),
  ]

  # Download and run resource_sizes.py concurrently.
  pool = multiprocessing.dummy.Pool()
  pool.map(_Artifact.FetchAndMeasure, artifacts)
  pool.close()

  # Add metrics in the order that we want them in the .csv output.
  metrics = {}
  webview.AddSize(metrics)
  webview64.AddSize(metrics)
  monochrome.AddSize(metrics)
  monochrome64.AddSize(metrics)
  trichrome_chrome.AddSize(metrics)
  trichrome_webview.AddSize(metrics)
  trichrome_library.AddSize(metrics)

  # Separate where spreadsheet has computed columns for easier copy/paste.
  _DumpCsvAndClear(metrics)
  trichrome64_chrome.AddSize(metrics)
  trichrome64_webview.AddSize(metrics)
  trichrome64_library.AddSize(metrics)
  _DumpCsvAndClear(metrics)

  trichrome64_high_chrome.AddSize(metrics)
  trichrome64_high_webview.AddSize(metrics)
  trichrome64_high_library.AddSize(metrics)

  _DumpCsvAndClear(metrics)

  metrics['System Image Size (arm32)'] = sum(x.GetApkSize()
                                             for x in trichrome_system_apks)
  metrics['System Image Size (arm64)'] = sum(x.GetApkSize()
                                             for x in trichrome64_system_apks)
  metrics['System Image Size (arm64-high)'] = sum(
      x.GetApkSize() for x in trichrome64_system_apks_high)

  go_install_size = (trichrome_chrome.GetAndroidGoSize() +
                     trichrome_webview.GetAndroidGoSize() +
                     trichrome_library.GetAndroidGoSize())
  metrics['Android Go (TriChrome) Install Size'] = go_install_size

  compressed_system_apks_size = sum(x.GetCompressedSize()
                                    for x in trichrome_system_apks)
  stubs_sizes = sum(x.GetApkSize() for x in trichrome_system_stubs)
  metrics['Android Go (Trichrome) Compressed System Image'] = (
      compressed_system_apks_size + stubs_sizes)

  monochrome.AddMethodCount(metrics)

  # Separate where spreadsheet has computed columns for easier copy/paste.
  _DumpCsvAndClear(metrics)

  trichrome_chrome.AddDfmSizes(metrics, 'Chrome')
  trichrome_webview.AddDfmSizes(metrics, 'WebView')
  _DumpCsvAndClear(metrics)


def _CheckGnArgs(unsigned_prefix, version):
  args = [_GSUTIL, 'cat', unsigned_prefix + '/arm/gn-args-derived.txt']
  logging.warning(' '.join(args))
  gn_args_data = subprocess.check_output(args, text=True)

  def check_arg(name, value):
    if f'{name} = {value}' not in gn_args_data:
      if f'{name} =' not in gn_args_data:
        sys.stderr.write(f'{name} is not in gn-args-derived.txt.\n')
      else:
        sys.stderr.write(f'{name} != {value} in gn-args-derived.txt.\n')
        sys.stderr.write('Sizes will not be accurate. Try again with a later '
                         'patch version.\n')
      sys.stderr.write('Manually verify via: ' + ' '.join(args) + '\n')
      sys.exit(1)

  if int(version.split('.')[0]) < 120:
    check_arg('is_on_release_branch', 'true')
  else:
    check_arg('v8_is_on_release_branch', 'true')

  check_arg('v8_enable_runtime_call_stats', 'false')


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('--version', required=True, help='e.g.: "75.0.3770.143"')
  parser.add_argument(
      '--signed-bucket',
      required=True,
      help='GCS bucket to find files in. (e.g. "gs://bucket/subdir")')
  parser.add_argument('--keep-files',
                      action='store_true',
                      help='Do not delete downloaded files.')
  options = parser.parse_args()

  signed_prefix = posixpath.join(options.signed_bucket, options.version)
  unsigned_prefix = signed_prefix.replace('signed', 'unsigned')

  # Ensure the binary size isn't inflated by v8_is_on_release_branch=true
  # not being set yet.
  _CheckGnArgs(unsigned_prefix, options.version)

  with tempfile.TemporaryDirectory() as staging_dir:
    if options.keep_files:
      staging_dir = 'milestone_apk_sizes-staging'
      os.makedirs(staging_dir, exist_ok=True)

    _DownloadAndAnalyze(signed_prefix, unsigned_prefix, staging_dir)

    if options.keep_files:
      print('Saved files to', staging_dir)


if __name__ == '__main__':
  main()