Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(21)

Unified Diff: tools/binary_size/diagnose_apk_bloat.py

Issue 2810813003: Add resource_sizes diffs to diagnose_apk_bloat.py. (Closed)
Patch Set: Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | tools/binary_size/helpers.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: tools/binary_size/diagnose_apk_bloat.py
diff --git a/tools/binary_size/diagnose_apk_bloat.py b/tools/binary_size/diagnose_apk_bloat.py
index 847c3059d3327b2dcf083e83bd767df19cb83aa4..58aeba9bed28e2a5a0a27aae3dd1843077fc50c2 100755
--- a/tools/binary_size/diagnose_apk_bloat.py
+++ b/tools/binary_size/diagnose_apk_bloat.py
@@ -9,7 +9,10 @@ Run diagnose_apk_bloat.py -h for detailed usage help.
"""
import argparse
-import logging
+import collections
+from contextlib import contextmanager
+import itertools
+import json
import multiprocessing
import os
import shutil
@@ -17,11 +20,98 @@ import subprocess
import sys
import helpers
+import map2size
_DEFAULT_OUT_DIR = os.path.join(helpers.SRC_ROOT, 'out', 'diagnose-apk-bloat')
_DEFAULT_TARGET = 'monochrome_public_apk'
_DEFAULT_ARCHIVE_DIR = os.path.join(helpers.SRC_ROOT, 'binary-size-bloat')
+# Global variable for storing the initial branch before the script was launched
+# so that it doesn't need to be passed everywhere in case we fail and exit.
+_initial_branch = None
+
+
+class BaseDiff(object):
+ """Base class capturing binary size diffs."""
+ def __init__(self, name):
+ self.banner = '\n' + '*' * 30 + name + '*' * 30
+ self.ProduceDiff()
+
+ def PrintResults(self, output_file=None):
+ with _MaybeStdoutToFile(output_file):
+ _Print(self.banner)
+ _Print('Summary:')
+ self.PrintSummary()
+ _Print('\nDetails:')
+ self.PrintDetailedResults()
+
+ def PrintSummary(self):
+ """A short description that summarizes the source of binary size bloat."""
+ raise NotImplementedError()
+
+ def PrintDetailedResults(self):
+ """A detailed description about the cause of binary size bloat."""
+ raise NotImplementedError()
+
+ def ProduceDiff(self):
+ """Prepare a binary size diff with ready to print results."""
+ raise NotImplementedError()
+
+
+_ResourceSizesDiffResult = collections.namedtuple(
+ 'ResourceSizesDiffResult', ['section', 'value', 'units'])
+
+
+class ResourceSizesDiff(BaseDiff):
+ _RESOURCE_SIZES_PATH = os.path.join(
+ helpers.SRC_ROOT, 'build', 'android', 'resource_sizes.py')
+
+ def __init__(self, archive_dirs, apk_name, slow_options=False):
+ self._archive_dirs = archive_dirs
+ self._apk_name = apk_name
+ self._slow_options = slow_options
+ super(ResourceSizesDiff, self).__init__('Resource Sizes Diff')
+
+ def PrintDetailedResults(self):
+ for section, value, units in self._diff:
+ _Print('{:>+10,} {} {}', value, units, section)
+
+ def PrintSummary(self):
+ for s in self._diff:
+ if 'normalized' in s.section:
+ _Print('Normalized APK size: {:+,} {}', s.value, s.units)
+
+ def ProduceDiff(self):
+ chartjsons = self._RunResourceSizes()
+ diff = []
+ with_patch = chartjsons[0]['charts']
+ without_patch = chartjsons[1]['charts']
+ for section, section_dict in with_patch.iteritems():
+ for subsection, v in section_dict.iteritems():
+ diff.append(
+ _ResourceSizesDiffResult(
+ '%s %s' % (section, subsection),
+ v['value'] - without_patch[section][subsection]['value'],
+ v['units']))
agrieve 2017/04/11 14:10:32 nit: Let's add logic to have it fail gracefully if
estevenson 2017/04/11 21:53:16 Done.
+ self._diff = sorted(diff, key=lambda x: abs(x.value), reverse=True)
+
+ def _RunResourceSizes(self):
+ chartjsons = []
+ for archive_dir in self._archive_dirs:
+ apk_path = os.path.join(archive_dir, self._apk_name)
+ chartjson_file = os.path.join(archive_dir, 'results-chart.json')
+ cmd = [self._RESOURCE_SIZES_PATH, '--output-dir', archive_dir,
+ '--no-output-dir',
+ '--chartjson', apk_path]
+ if self._slow_options:
+ cmd += ['--estimate-patch-size']
+ else:
+ cmd += ['--no-static-initializer-check']
+ _RunCmd(cmd)
+ with open(chartjson_file) as f:
+ chartjsons.append(json.load(f))
+ return chartjsons
+
class _BuildHelper(object):
"""Helper class for generating and building targets."""
@@ -62,26 +152,25 @@ class _BuildHelper(object):
return cmd
def Build(self):
- logging.info('Building %s. This may take a while (run with -vv for '
- 'detailed ninja output).', self.target)
+ _Print('Building: {}.', self.target)
_RunCmd(self._GenGnCmd())
_RunCmd(self._GenNinjaCmd(), print_stdout=True)
-def _GetLinkerMapPath(target_os, target):
+def _GetMainLibPath(target_os, target):
# TODO(estevenson): Get this from GN instead of hardcoding.
if target_os == 'linux':
- return 'chrome.map.gz'
+ return 'chrome'
elif 'monochrome' in target:
- return 'lib.unstripped/libmonochrome.so.map.gz'
+ return 'lib.unstripped/libmonochrome.so'
else:
- return 'lib.unstripped/libchrome.so.map.gz'
+ return 'lib.unstripped/libchrome.so'
-def _ApkPathFromTarget(target):
+def _ApkNameFromTarget(target):
# Only works on apk targets that follow: my_great_apk naming convention.
apk_name = ''.join(s.title() for s in target.split('_')[:-1]) + '.apk'
- return os.path.join('apks', apk_name)
+ return apk_name.replace('Webview', 'WebView')
def _RunCmd(cmd, print_stdout=False):
@@ -90,27 +179,23 @@ def _RunCmd(cmd, print_stdout=False):
Args:
cmd: the command to run.
print_stdout: if this is True, then the stdout of the process will be
- printed (to stdout if log level is DEBUG otherwise to /dev/null).
- If false, stdout will be returned.
+ printed, otherwise stdout will be returned.
Returns:
Command stdout if |print_stdout| is False otherwise ''.
"""
cmd_str = ' '.join(c for c in cmd)
- logging.debug('Running: %s', cmd_str)
- if not print_stdout:
- proc_stdout = subprocess.PIPE
- elif logging.getLogger().isEnabledFor(logging.DEBUG):
+ _Print('Running: {}', cmd_str)
+ if print_stdout:
proc_stdout = sys.stdout
else:
- proc_stdout = open(os.devnull, 'wb')
+ proc_stdout = subprocess.PIPE
proc = subprocess.Popen(cmd, stdout=proc_stdout, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
- logging.error('Command failed: %s\nstderr:\n%s' % (cmd_str, stderr))
- sys.exit(1)
+ _Die('command failed: {}\nstderr:\n{}', cmd_str, stderr)
return stdout.strip() if stdout else ''
@@ -122,57 +207,92 @@ def _GitCmd(args):
def _GclientSyncCmd(rev):
cwd = os.getcwd()
os.chdir(helpers.SRC_ROOT)
- logging.info('gclient sync to %s', rev)
_RunCmd(['gclient', 'sync', '-r', 'src@' + rev], print_stdout=True)
os.chdir(cwd)
def _ArchiveBuildResult(archive_dir, build_helper):
- """Save resulting APK and mapping file."""
- def ArchiveFile(file_path):
- file_path = os.path.join(build_helper.output_directory, file_path)
- if os.path.exists(file_path):
- if not os.path.exists(archive_dir):
- os.makedirs(archive_dir)
- shutil.copy(file_path, archive_dir)
- else:
- logging.error('Expected file: %s not found.' % file_path)
- sys.exit(1)
-
- logging.info('Saving build results to: %s', archive_dir)
- ArchiveFile(_GetLinkerMapPath(build_helper.target_os, build_helper.target))
- if build_helper.target_os == 'android':
- ArchiveFile(_ApkPathFromTarget(build_helper.target))
+ """Save build artifacts necessary for diffing."""
+ _Print('Saving build results to: {}', archive_dir)
+ if not os.path.exists(archive_dir):
+ os.makedirs(archive_dir)
+
+ def ArchiveFile(filename):
+ if not os.path.exists(filename):
+ _Die('missing expected file: {}', filename)
+ shutil.copy(filename, archive_dir)
+
+ lib_path = os.path.join(
+ build_helper.output_directory,
+ _GetMainLibPath(build_helper.target_os, build_helper.target))
+ ArchiveFile(lib_path)
+
+ size_path = os.path.join(
+ archive_dir, os.path.splitext(os.path.basename(lib_path))[0] + '.size')
+ map2size.main(['tools/binary_size/map2size.py', '--output-directory',
agrieve 2017/04/11 14:10:32 Probably better to use subprocess for this since i
estevenson 2017/04/11 21:53:16 Done.
+ build_helper.output_directory, lib_path, size_path, '-v'])
+ if build_helper.target_os == 'android':
+ apk_path = os.path.join(build_helper.output_directory, 'apks',
+ _ApkNameFromTarget(build_helper.target))
+ ArchiveFile(apk_path)
-def _SyncAndBuild(rev_with_patch, rev_without_patch, archive_dir, build_helper):
- rev_with_patch = _GitCmd(['rev-parse', rev_with_patch])
- rev_without_patch = _GitCmd([
- 'rev-parse', rev_without_patch or rev_with_patch + '^'])
+def _SyncAndBuild(revs, archive_dirs, build_helper):
# Move to a detached state since gclient sync doesn't work with local commits
# on a branch.
_GitCmd(['checkout', '--detach'])
+ for rev, archive_dir in itertools.izip(revs, archive_dirs):
+ _GclientSyncCmd(rev)
+ build_helper.Build()
+ _ArchiveBuildResult(archive_dir, build_helper)
- _GclientSyncCmd(rev_with_patch)
- build_helper.Build()
- _ArchiveBuildResult(
- os.path.join(archive_dir, 'with_patch_%s' % rev_with_patch), build_helper)
- _GclientSyncCmd(rev_without_patch)
- build_helper.Build()
- _ArchiveBuildResult(
- os.path.join(archive_dir, 'without_patch_%s' % rev_without_patch),
- build_helper)
+def _NormalizeRevs(*revs):
agrieve 2017/04/11 14:10:32 nit: This interface seems a bit awkward to me. How
estevenson 2017/04/11 21:53:16 Done. Still left revs as a list (instead of a rev_
+ """Use actual revs instead of HEAD, HEAD^, etc."""
+ return [_GitCmd(['rev-parse', r]) for r in revs]
def _EnsureDirectoryClean():
- logging.info('Checking source directory')
+ _Print('Checking source directory')
stdout = _GitCmd(['status', '--porcelain'])
# Ignore untracked files.
if stdout and stdout[:2] != '??':
- logging.error('Failure: please ensure working directory is clean.')
- sys.exit(1)
+ _Die('please ensure working directory is clean.')
+
+
+def _SetInitialBranch():
+ global _initial_branch
+ _initial_branch = _GitCmd(['rev-parse', '--abbrev-ref', 'HEAD'])
+
+
+def _RestoreInitialBranch():
+ if _initial_branch:
+ _GitCmd(['checkout', _initial_branch])
+
+
+def _Die(s, *args, **kwargs):
+ _Print('Failure: ' + s, *args, **kwargs)
+ _RestoreInitialBranch()
+ sys.exit(1)
+
+
+def _Print(s, *args, **kwargs):
+ print s.format(*args, **kwargs)
+
+
+@contextmanager
+def _MaybeStdoutToFile(filename):
+ """Print to |filename| instead of stdout."""
+ if not filename:
+ yield
+ else:
+ with open(filename, 'w+') as f:
+ sys.stdout, sys_stdout = f, sys.stdout
+ try:
+ yield
+ finally:
+ sys.stdout = sys_stdout
def main():
@@ -188,6 +308,11 @@ def main():
parser.add_argument('--rev-without-patch',
help='Older patch to diff against. If not supplied, '
'the previous commit to rev_with_patch will be used.')
+ parser.add_argument('--save-results',
agrieve 2017/04/11 14:10:32 Rather than adding another option, I think it woul
estevenson 2017/04/11 21:53:16 Done.
+ help='Save diff to this file instead of printing.')
+ parser.add_argument('--include-slow-options',
+ action='store_true',
+ help='Run some extra steps that take longer to complete.')
agrieve 2017/04/11 14:10:32 nit: Should say exactly which options this enables
estevenson 2017/04/11 21:53:16 Done.
build_group = parser.add_argument_group('ninja', 'Args to use with ninja/gn')
build_group.add_argument('-j',
@@ -215,13 +340,24 @@ def main():
build_group.add_argument('--target',
default=_DEFAULT_TARGET,
help='GN APK target to build.')
- args = helpers.AddCommonOptionsAndParseArgs(parser, sys.argv, pypy_warn=False)
+ args = parser.parse_args()
_EnsureDirectoryClean()
+ _SetInitialBranch()
+ revs = _NormalizeRevs(args.rev_with_patch,
+ args.rev_without_patch or args.rev_with_patch + '^')
build_helper = _BuildHelper(args)
- _SyncAndBuild(args.rev_with_patch, args.rev_without_patch, args.archive_dir,
- build_helper)
-
+ archive_dirs = [os.path.join(args.archive_dir, '%d-%s' % (i + 1, rev))
agrieve 2017/04/11 14:10:32 nit: might be better as len(revs) - i, so that the
estevenson 2017/04/11 21:53:16 Done.
+ for i, rev in enumerate(revs)]
+ _SyncAndBuild(revs, archive_dirs, build_helper)
+ _RestoreInitialBranch()
+
+ diffs = [
+ ResourceSizesDiff(archive_dirs, _ApkNameFromTarget(args.target),
+ slow_options=args.include_slow_options),
+ ]
+ for d in diffs:
+ d.PrintResults(output_file=args.save_results)
if __name__ == '__main__':
sys.exit(main())
« no previous file with comments | « no previous file | tools/binary_size/helpers.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698