Ingemar Ã…dahl | 504c076 | 2017-07-05 14:42:43 | [diff] [blame] | 1 | #!/usr/bin/env python |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 2 | # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | |
Andrew Grieve | 2143a47 | 2017-09-26 21:09:11 | [diff] [blame] | 6 | """Reports binary size and static initializer metrics for an APK. |
| 7 | |
| 8 | More information at //docs/speed/binary_size/metrics.md. |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 9 | """ |
| 10 | |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 11 | import argparse |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 12 | import collections |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 13 | from contextlib import contextmanager |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 14 | import json |
rnephew | 526a5c3 | 2016-04-28 21:37:05 | [diff] [blame] | 15 | import logging |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 16 | import os |
| 17 | import re |
agrieve | 83011ed | 2016-05-12 19:56:45 | [diff] [blame] | 18 | import struct |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 19 | import sys |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 20 | import zipfile |
| 21 | import zlib |
| 22 | |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 23 | from binary_size import apk_downloader |
jbudorick | d28554a | 2016-01-11 16:22:59 | [diff] [blame] | 24 | import devil_chromium |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 25 | from devil.android.sdk import build_tools |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 26 | from devil.utils import cmd_helper |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 27 | from devil.utils import lazy |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 28 | import method_count |
agrieve | 05957d7e | 2016-04-05 21:06:06 | [diff] [blame] | 29 | from pylib import constants |
jbudorick | d28554a | 2016-01-11 16:22:59 | [diff] [blame] | 30 | from pylib.constants import host_paths |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 31 | |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 32 | _AAPT_PATH = lazy.WeakConstant(lambda: build_tools.GetPath('aapt')) |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 33 | _BUILD_UTILS_PATH = os.path.join( |
| 34 | host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gyp') |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 35 | _APK_PATCH_SIZE_ESTIMATOR_PATH = os.path.join( |
| 36 | host_paths.DIR_SOURCE_ROOT, 'third_party', 'apk-patch-size-estimator') |
jbudorick | d28554a | 2016-01-11 16:22:59 | [diff] [blame] | 37 | |
jbudorick | d28554a | 2016-01-11 16:22:59 | [diff] [blame] | 38 | with host_paths.SysPath(host_paths.BUILD_COMMON_PATH): |
| 39 | import perf_tests_results_helper # pylint: disable=import-error |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 40 | |
mikecase | 79ae465 | 2017-05-19 17:29:22 | [diff] [blame] | 41 | with host_paths.SysPath(_BUILD_UTILS_PATH, 0): |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 42 | from util import build_utils # pylint: disable=import-error |
| 43 | |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 44 | with host_paths.SysPath(_APK_PATCH_SIZE_ESTIMATOR_PATH): |
| 45 | import apk_patch_size_estimator # pylint: disable=import-error |
| 46 | |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 47 | |
agrieve | 83011ed | 2016-05-12 19:56:45 | [diff] [blame] | 48 | # Python had a bug in zipinfo parsing that triggers on ChromeModern.apk |
| 49 | # https://bugs.python.org/issue14315 |
| 50 | def _PatchedDecodeExtra(self): |
| 51 | # Try to decode the extra field. |
| 52 | extra = self.extra |
| 53 | unpack = struct.unpack |
| 54 | while len(extra) >= 4: |
| 55 | tp, ln = unpack('<HH', extra[:4]) |
| 56 | if tp == 1: |
| 57 | if ln >= 24: |
| 58 | counts = unpack('<QQQ', extra[4:28]) |
| 59 | elif ln == 16: |
| 60 | counts = unpack('<QQ', extra[4:20]) |
| 61 | elif ln == 8: |
| 62 | counts = unpack('<Q', extra[4:12]) |
| 63 | elif ln == 0: |
| 64 | counts = () |
| 65 | else: |
| 66 | raise RuntimeError, "Corrupt extra field %s"%(ln,) |
| 67 | |
| 68 | idx = 0 |
| 69 | |
| 70 | # ZIP64 extension (large files and/or large archives) |
| 71 | if self.file_size in (0xffffffffffffffffL, 0xffffffffL): |
| 72 | self.file_size = counts[idx] |
| 73 | idx += 1 |
| 74 | |
| 75 | if self.compress_size == 0xFFFFFFFFL: |
| 76 | self.compress_size = counts[idx] |
| 77 | idx += 1 |
| 78 | |
| 79 | if self.header_offset == 0xffffffffL: |
| 80 | self.header_offset = counts[idx] |
| 81 | idx += 1 |
| 82 | |
| 83 | extra = extra[ln + 4:] |
| 84 | |
| 85 | zipfile.ZipInfo._decodeExtra = ( # pylint: disable=protected-access |
| 86 | _PatchedDecodeExtra) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 87 | |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 88 | # Captures an entire config from aapt output. |
| 89 | _AAPT_CONFIG_PATTERN = r'config %s:(.*?)config [a-zA-Z-]+:' |
| 90 | # Matches string resource entries from aapt output. |
| 91 | _AAPT_ENTRY_RE = re.compile( |
| 92 | r'resource (?P<id>\w{10}) [\w\.]+:string/.*?"(?P<val>.+?)"', re.DOTALL) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 93 | _BASE_CHART = { |
| 94 | 'format_version': '0.1', |
| 95 | 'benchmark_name': 'resource_sizes', |
| 96 | 'benchmark_description': 'APK resource size information.', |
| 97 | 'trace_rerun_options': [], |
| 98 | 'charts': {} |
| 99 | } |
rnephew | b5fe097 | 2016-02-10 17:45:13 | [diff] [blame] | 100 | _DUMP_STATIC_INITIALIZERS_PATH = os.path.join( |
| 101 | host_paths.DIR_SOURCE_ROOT, 'tools', 'linux', 'dump-static-initializers.py') |
agrieve | 12ff4bb | 2016-05-19 15:57:34 | [diff] [blame] | 102 | # Pragma exists when enable_resource_whitelist_generation=true. |
| 103 | _RC_HEADER_RE = re.compile( |
| 104 | r'^#define (?P<name>\w+) (?:_Pragma\(.*?\) )?(?P<id>\d+)$') |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 105 | _RE_NON_LANGUAGE_PAK = re.compile(r'^assets/.*(resources|percent)\.pak$') |
| 106 | _RE_COMPRESSED_LANGUAGE_PAK = re.compile( |
| 107 | r'\.lpak$|^assets/(?!stored-locales/).*(?!resources|percent)\.pak$') |
| 108 | _RE_STORED_LANGUAGE_PAK = re.compile(r'^assets/stored-locales/.*\.pak$') |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 109 | _READELF_SIZES_METRICS = { |
| 110 | 'text': ['.text'], |
| 111 | 'data': ['.data', '.rodata', '.data.rel.ro', '.data.rel.ro.local'], |
| 112 | 'relocations': ['.rel.dyn', '.rel.plt', '.rela.dyn', '.rela.plt'], |
Andrew Grieve | c3dd28e | 2018-04-04 17:17:23 | [diff] [blame] | 113 | 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr', |
| 114 | '.ARM.exidxsentinel_section_after_text'], |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 115 | 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt', |
Andrew Grieve | c3dd28e | 2018-04-04 17:17:23 | [diff] [blame] | 116 | '.got.plt', '.hash', '.gnu.hash'], |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 117 | 'bss': ['.bss'], |
| 118 | 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version', |
Andrew Grieve | c3dd28e | 2018-04-04 17:17:23 | [diff] [blame] | 119 | '.note.crashpad.info', '.note.android.ident', |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 120 | '.ARM.attributes', '.note.gnu.build-id', '.gnu.version', |
| 121 | '.gnu.version_d', '.gnu.version_r', '.interp', '.gcc_except_table'] |
| 122 | } |
| 123 | |
| 124 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 125 | def _RunReadelf(so_path, options, tool_prefix=''): |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 126 | return cmd_helper.GetCmdOutput( |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 127 | [tool_prefix + 'readelf'] + options + [so_path]) |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 128 | |
| 129 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 130 | def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path, tool_prefix): |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 131 | with Unzip(apk_path, filename=main_lib_path) as extracted_lib_path: |
| 132 | grouped_section_sizes = collections.defaultdict(int) |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 133 | section_sizes = _CreateSectionNameSizeMap(extracted_lib_path, tool_prefix) |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 134 | for group_name, section_names in _READELF_SIZES_METRICS.iteritems(): |
| 135 | for section_name in section_names: |
| 136 | if section_name in section_sizes: |
| 137 | grouped_section_sizes[group_name] += section_sizes.pop(section_name) |
| 138 | |
| 139 | # Group any unknown section headers into the "other" group. |
| 140 | for section_header, section_size in section_sizes.iteritems(): |
| 141 | print "Unknown elf section header:", section_header |
| 142 | grouped_section_sizes['other'] += section_size |
| 143 | |
| 144 | return grouped_section_sizes |
| 145 | |
| 146 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 147 | def _CreateSectionNameSizeMap(so_path, tool_prefix): |
| 148 | stdout = _RunReadelf(so_path, ['-S', '--wide'], tool_prefix) |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 149 | section_sizes = {} |
| 150 | # Matches [ 2] .hash HASH 00000000006681f0 0001f0 003154 04 A 3 0 8 |
| 151 | for match in re.finditer(r'\[[\s\d]+\] (\..*)$', stdout, re.MULTILINE): |
| 152 | items = match.group(1).split() |
| 153 | section_sizes[items[0]] = int(items[4], 16) |
| 154 | |
| 155 | return section_sizes |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 156 | |
| 157 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 158 | def _ParseLibBuildId(so_path, tool_prefix): |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 159 | """Returns the Build ID of the given native library.""" |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 160 | stdout = _RunReadelf(so_path, ['-n'], tool_prefix) |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 161 | match = re.search(r'Build ID: (\w+)', stdout) |
| 162 | return match.group(1) if match else None |
| 163 | |
| 164 | |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 165 | def _ParseManifestAttributes(apk_path): |
| 166 | # Check if the manifest specifies whether or not to extract native libs. |
| 167 | skip_extract_lib = False |
| 168 | output = cmd_helper.GetCmdOutput([ |
| 169 | _AAPT_PATH.read(), 'd', 'xmltree', apk_path, 'AndroidManifest.xml']) |
| 170 | m = re.search(r'extractNativeLibs\(.*\)=\(.*\)(\w)', output) |
| 171 | if m: |
| 172 | skip_extract_lib = not bool(int(m.group(1))) |
| 173 | |
| 174 | # Dex decompression overhead varies by Android version. |
| 175 | m = re.search(r'android:minSdkVersion\(\w+\)=\(type \w+\)(\w+)\n', output) |
| 176 | sdk_version = int(m.group(1), 16) |
| 177 | # Pre-L: Dalvik - .odex file is simply decompressed/optimized dex file (~1x). |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 178 | # L, M: ART - .odex file is compiled version of the dex file (~4x). |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 179 | # N: ART - Uses Dalvik-like JIT for normal apps (~1x), full compilation for |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 180 | # shared apps (~4x). |
| 181 | # Actual multipliers calculated using "apk_operations.py disk-usage". |
| 182 | # Will need to update multipliers once apk obfuscation is enabled. |
| 183 | # E.g. with obfuscation, the 4.04 changes to 4.46. |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 184 | if sdk_version < 21: |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 185 | dex_multiplier = 1.16 |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 186 | elif sdk_version < 24: |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 187 | dex_multiplier = 4.04 |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 188 | elif 'Monochrome' in apk_path or 'WebView' in apk_path: |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 189 | dex_multiplier = 4.04 # compilation_filter=speed |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 190 | else: |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 191 | dex_multiplier = 1.17 # compilation_filter=speed-profile |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 192 | |
| 193 | return dex_multiplier, skip_extract_lib |
| 194 | |
| 195 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 196 | def CountStaticInitializers(so_path, tool_prefix): |
| 197 | # Mostly copied from //infra/scripts/legacy/scripts/slave/chromium/sizes.py. |
rnephew | b5fe097 | 2016-02-10 17:45:13 | [diff] [blame] | 198 | def get_elf_section_size(readelf_stdout, section_name): |
| 199 | # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8 |
| 200 | match = re.search(r'\.%s.*$' % re.escape(section_name), |
| 201 | readelf_stdout, re.MULTILINE) |
| 202 | if not match: |
| 203 | return (False, -1) |
| 204 | size_str = re.split(r'\W+', match.group(0))[5] |
| 205 | return (True, int(size_str, 16)) |
| 206 | |
| 207 | # Find the number of files with at least one static initializer. |
| 208 | # First determine if we're 32 or 64 bit |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 209 | stdout = _RunReadelf(so_path, ['-h'], tool_prefix) |
rnephew | b5fe097 | 2016-02-10 17:45:13 | [diff] [blame] | 210 | elf_class_line = re.search('Class:.*$', stdout, re.MULTILINE).group(0) |
| 211 | elf_class = re.split(r'\W+', elf_class_line)[1] |
| 212 | if elf_class == 'ELF32': |
| 213 | word_size = 4 |
| 214 | else: |
| 215 | word_size = 8 |
| 216 | |
| 217 | # Then find the number of files with global static initializers. |
| 218 | # NOTE: this is very implementation-specific and makes assumptions |
| 219 | # about how compiler and linker implement global static initializers. |
| 220 | si_count = 0 |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 221 | stdout = _RunReadelf(so_path, ['-SW'], tool_prefix) |
rnephew | b5fe097 | 2016-02-10 17:45:13 | [diff] [blame] | 222 | has_init_array, init_array_size = get_elf_section_size(stdout, 'init_array') |
| 223 | if has_init_array: |
| 224 | si_count = init_array_size / word_size |
| 225 | si_count = max(si_count, 0) |
| 226 | return si_count |
| 227 | |
| 228 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 229 | def GetStaticInitializers(so_path, tool_prefix): |
rnephew | b5fe097 | 2016-02-10 17:45:13 | [diff] [blame] | 230 | output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d', |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 231 | so_path, '-t', tool_prefix]) |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 232 | summary = re.search(r'Found \d+ static initializers in (\d+) files.', output) |
| 233 | return output.splitlines()[:-1], int(summary.group(1)) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 234 | |
| 235 | |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 236 | def _NormalizeLanguagePaks(translations, normalized_apk_size, factor): |
| 237 | english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak') |
| 238 | num_translations = translations.GetNumEntries() |
| 239 | if english_pak: |
| 240 | normalized_apk_size -= translations.ComputeZippedSize() |
| 241 | normalized_apk_size += int( |
| 242 | english_pak.compress_size * num_translations * factor) |
| 243 | return normalized_apk_size |
| 244 | |
| 245 | |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 246 | def _NormalizeResourcesArsc(apk_path, num_arsc_files, num_translations, |
| 247 | out_dir): |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 248 | """Estimates the expected overhead of untranslated strings in resources.arsc. |
| 249 | |
| 250 | See http://crbug.com/677966 for why this is necessary. |
| 251 | """ |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 252 | # If there are multiple .arsc files, use the resource packaged APK instead. |
| 253 | if num_arsc_files > 1: |
| 254 | if not out_dir: |
| 255 | print 'Skipping resources.arsc normalization (output directory required)' |
| 256 | return 0 |
| 257 | ap_name = os.path.basename(apk_path).replace('.apk', '.intermediate.ap_') |
| 258 | ap_path = os.path.join(out_dir, 'gen/arsc/apks', ap_name) |
| 259 | if not os.path.exists(ap_path): |
| 260 | raise Exception('Missing expected file: %s, try rebuilding.' % ap_path) |
| 261 | apk_path = ap_path |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 262 | |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 263 | aapt_output = _RunAaptDumpResources(apk_path) |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 264 | # en-rUS is in the default config and may be cluttered with non-translatable |
| 265 | # strings, so en-rGB is a better baseline for finding missing translations. |
| 266 | en_strings = _CreateResourceIdValueMap(aapt_output, 'en-rGB') |
| 267 | fr_strings = _CreateResourceIdValueMap(aapt_output, 'fr') |
| 268 | |
Eric Stevenson | 62bfb2cb4 | 2017-09-08 21:14:19 | [diff] [blame] | 269 | # en-US and en-GB will never be translated. |
| 270 | config_count = num_translations - 2 |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 271 | |
| 272 | size = 0 |
| 273 | for res_id, string_val in en_strings.iteritems(): |
| 274 | if string_val == fr_strings[res_id]: |
| 275 | string_size = len(string_val) |
| 276 | # 7 bytes is the per-entry overhead (not specific to any string). See |
| 277 | # https://android.googlesource.com/platform/frameworks/base.git/+/android-4.2.2_r1/tools/aapt/StringPool.cpp#414. |
| 278 | # The 1.5 factor was determined experimentally and is meant to account for |
| 279 | # other languages generally having longer strings than english. |
| 280 | size += config_count * (7 + string_size * 1.5) |
| 281 | |
| 282 | return size |
| 283 | |
| 284 | |
| 285 | def _CreateResourceIdValueMap(aapt_output, lang): |
| 286 | """Return a map of resource ids to string values for the given |lang|.""" |
| 287 | config_re = _AAPT_CONFIG_PATTERN % lang |
| 288 | return {entry.group('id'): entry.group('val') |
| 289 | for config_section in re.finditer(config_re, aapt_output, re.DOTALL) |
| 290 | for entry in re.finditer(_AAPT_ENTRY_RE, config_section.group(0))} |
| 291 | |
| 292 | |
| 293 | def _RunAaptDumpResources(apk_path): |
| 294 | cmd = [_AAPT_PATH.read(), 'dump', '--values', 'resources', apk_path] |
| 295 | status, output = cmd_helper.GetCmdStatusAndOutput(cmd) |
| 296 | if status != 0: |
| 297 | raise Exception('Failed running aapt command: "%s" with output "%s".' % |
| 298 | (' '.join(cmd), output)) |
| 299 | return output |
| 300 | |
| 301 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 302 | class _FileGroup(object): |
| 303 | """Represents a category that apk files can fall into.""" |
| 304 | |
| 305 | def __init__(self, name): |
| 306 | self.name = name |
| 307 | self._zip_infos = [] |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 308 | self._extracted_multipliers = [] |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 309 | |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 310 | def AddZipInfo(self, zip_info, extracted_multiplier=0): |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 311 | self._zip_infos.append(zip_info) |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 312 | self._extracted_multipliers.append(extracted_multiplier) |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 313 | |
agrieve | 0c10a53 | 2017-01-03 19:41:00 | [diff] [blame] | 314 | def AllEntries(self): |
| 315 | return iter(self._zip_infos) |
| 316 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 317 | def GetNumEntries(self): |
| 318 | return len(self._zip_infos) |
| 319 | |
| 320 | def FindByPattern(self, pattern): |
agrieve | bc2287b0 | 2016-12-12 23:13:01 | [diff] [blame] | 321 | return next((i for i in self._zip_infos if re.match(pattern, i.filename)), |
| 322 | None) |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 323 | |
| 324 | def FindLargest(self): |
jbudorick | 53e27679 | 2017-04-06 17:13:54 | [diff] [blame] | 325 | if not self._zip_infos: |
| 326 | return None |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 327 | return max(self._zip_infos, key=lambda i: i.file_size) |
| 328 | |
| 329 | def ComputeZippedSize(self): |
| 330 | return sum(i.compress_size for i in self._zip_infos) |
| 331 | |
| 332 | def ComputeUncompressedSize(self): |
| 333 | return sum(i.file_size for i in self._zip_infos) |
| 334 | |
| 335 | def ComputeExtractedSize(self): |
| 336 | ret = 0 |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 337 | for zi, multiplier in zip(self._zip_infos, self._extracted_multipliers): |
| 338 | ret += zi.file_size * multiplier |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 339 | return ret |
| 340 | |
| 341 | def ComputeInstallSize(self): |
| 342 | return self.ComputeExtractedSize() + self.ComputeZippedSize() |
| 343 | |
| 344 | |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 345 | def PrintApkAnalysis(apk_filename, tool_prefix, out_dir, chartjson=None): |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 346 | """Analyse APK to determine size contributions of different file classes.""" |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 347 | file_groups = [] |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 348 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 349 | def make_group(name): |
| 350 | group = _FileGroup(name) |
| 351 | file_groups.append(group) |
| 352 | return group |
| 353 | |
| 354 | native_code = make_group('Native code') |
| 355 | java_code = make_group('Java code') |
| 356 | native_resources_no_translations = make_group('Native resources (no l10n)') |
| 357 | translations = make_group('Native resources (l10n)') |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 358 | stored_translations = make_group('Native resources stored (l10n)') |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 359 | icu_data = make_group('ICU (i18n library) data') |
| 360 | v8_snapshots = make_group('V8 Snapshots') |
| 361 | png_drawables = make_group('PNG drawables') |
| 362 | res_directory = make_group('Non-compiled Android resources') |
| 363 | arsc = make_group('Compiled Android resources') |
| 364 | metadata = make_group('Package metadata') |
| 365 | unknown = make_group('Unknown files') |
agrieve | 0c10a53 | 2017-01-03 19:41:00 | [diff] [blame] | 366 | notices = make_group('licenses.notice file') |
Andrew Grieve | 9b8952c8 | 2018-04-04 16:38:31 | [diff] [blame] | 367 | unwind_cfi = make_group('unwind_cfi (dev and canary only)') |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 368 | |
| 369 | apk = zipfile.ZipFile(apk_filename, 'r') |
| 370 | try: |
| 371 | apk_contents = apk.infolist() |
| 372 | finally: |
| 373 | apk.close() |
| 374 | |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 375 | dex_multiplier, skip_extract_lib = _ParseManifestAttributes(apk_filename) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 376 | total_apk_size = os.path.getsize(apk_filename) |
| 377 | apk_basename = os.path.basename(apk_filename) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 378 | for member in apk_contents: |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 379 | filename = member.filename |
| 380 | if filename.endswith('/'): |
| 381 | continue |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 382 | if filename.endswith('.so'): |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 383 | should_extract_lib = not (skip_extract_lib or 'crazy' in filename) |
| 384 | native_code.AddZipInfo( |
| 385 | member, extracted_multiplier=int(should_extract_lib)) |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 386 | elif filename.endswith('.dex'): |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 387 | java_code.AddZipInfo(member, extracted_multiplier=dex_multiplier) |
| 388 | elif re.search(_RE_NON_LANGUAGE_PAK, filename): |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 389 | native_resources_no_translations.AddZipInfo(member) |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 390 | elif re.search(_RE_COMPRESSED_LANGUAGE_PAK, filename): |
| 391 | translations.AddZipInfo( |
| 392 | member, |
| 393 | extracted_multiplier=int('en_' in filename or 'en-' in filename)) |
| 394 | elif re.search(_RE_STORED_LANGUAGE_PAK, filename): |
| 395 | stored_translations.AddZipInfo(member) |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 396 | elif filename == 'assets/icudtl.dat': |
| 397 | icu_data.AddZipInfo(member) |
| 398 | elif filename.endswith('.bin'): |
| 399 | v8_snapshots.AddZipInfo(member) |
| 400 | elif filename.endswith('.png') or filename.endswith('.webp'): |
| 401 | png_drawables.AddZipInfo(member) |
| 402 | elif filename.startswith('res/'): |
| 403 | res_directory.AddZipInfo(member) |
| 404 | elif filename.endswith('.arsc'): |
| 405 | arsc.AddZipInfo(member) |
| 406 | elif filename.startswith('META-INF') or filename == 'AndroidManifest.xml': |
| 407 | metadata.AddZipInfo(member) |
agrieve | 0c10a53 | 2017-01-03 19:41:00 | [diff] [blame] | 408 | elif filename.endswith('.notice'): |
| 409 | notices.AddZipInfo(member) |
Andrew Grieve | 9b8952c8 | 2018-04-04 16:38:31 | [diff] [blame] | 410 | elif filename.startswith('assets/unwind_cfi'): |
| 411 | unwind_cfi.AddZipInfo(member) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 412 | else: |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 413 | unknown.AddZipInfo(member) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 414 | |
| 415 | total_install_size = total_apk_size |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 416 | zip_overhead = total_apk_size |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 417 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 418 | for group in file_groups: |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 419 | actual_size = group.ComputeZippedSize() |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 420 | install_size = group.ComputeInstallSize() |
Andrew Grieve | 58f4470 | 2017-09-25 18:21:54 | [diff] [blame] | 421 | uncompressed_size = group.ComputeUncompressedSize() |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 422 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 423 | total_install_size += group.ComputeExtractedSize() |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 424 | zip_overhead -= actual_size |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 425 | |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 426 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 427 | apk_basename + '_Breakdown', group.name + ' size', |
| 428 | actual_size, 'bytes') |
| 429 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 430 | apk_basename + '_InstallBreakdown', |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 431 | group.name + ' size', install_size, 'bytes') |
Andrew Grieve | 58f4470 | 2017-09-25 18:21:54 | [diff] [blame] | 432 | # Only a few metrics are compressed in the first place. |
| 433 | # To avoid over-reporting, track uncompressed size only for compressed |
| 434 | # entries. |
| 435 | if uncompressed_size != actual_size: |
| 436 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 437 | apk_basename + '_Uncompressed', |
| 438 | group.name + ' size', uncompressed_size, |
| 439 | 'bytes') |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 440 | |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 441 | # Per-file zip overhead is caused by: |
| 442 | # * 30 byte entry header + len(file name) |
| 443 | # * 46 byte central directory entry + len(file name) |
| 444 | # * 0-3 bytes for zipalign. |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 445 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 446 | apk_basename + '_Breakdown', 'Zip Overhead', |
Andrew Grieve | 767a6f5 | 2017-07-28 18:11:29 | [diff] [blame] | 447 | zip_overhead, 'bytes') |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 448 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 449 | apk_basename + '_InstallSize', 'APK size', |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 450 | total_apk_size, 'bytes') |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 451 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 452 | apk_basename + '_InstallSize', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 453 | 'Estimated installed size', total_install_size, 'bytes') |
| 454 | transfer_size = _CalculateCompressedSize(apk_filename) |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 455 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 456 | apk_basename + '_TransferSize', |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 457 | 'Transfer size (deflate)', transfer_size, 'bytes') |
| 458 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 459 | # Size of main dex vs remaining. |
| 460 | main_dex_info = java_code.FindByPattern('classes.dex') |
| 461 | if main_dex_info: |
| 462 | main_dex_size = main_dex_info.file_size |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 463 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 464 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 465 | 'main dex size', main_dex_size, 'bytes') |
| 466 | secondary_size = java_code.ComputeUncompressedSize() - main_dex_size |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 467 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 468 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 469 | 'secondary dex size', secondary_size, 'bytes') |
| 470 | |
| 471 | # Size of main .so vs remaining. |
| 472 | main_lib_info = native_code.FindLargest() |
| 473 | if main_lib_info: |
| 474 | main_lib_size = main_lib_info.file_size |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 475 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 476 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 477 | 'main lib size', main_lib_size, 'bytes') |
| 478 | secondary_size = native_code.ComputeUncompressedSize() - main_lib_size |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 479 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 480 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 481 | 'other lib size', secondary_size, 'bytes') |
| 482 | |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 483 | main_lib_section_sizes = _ExtractMainLibSectionSizesFromApk( |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 484 | apk_filename, main_lib_info.filename, tool_prefix) |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 485 | for metric_name, size in main_lib_section_sizes.iteritems(): |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 486 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 487 | apk_basename + '_MainLibInfo', |
estevenson | b57a134 | 2017-02-06 11:34:23 | [diff] [blame] | 488 | metric_name, size, 'bytes') |
| 489 | |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 490 | # Main metric that we want to monitor for jumps. |
| 491 | normalized_apk_size = total_apk_size |
Andrew Grieve | 9b8952c8 | 2018-04-04 16:38:31 | [diff] [blame] | 492 | # unwind_cfi exists only in dev, canary, and non-channel builds. |
| 493 | normalized_apk_size -= unwind_cfi.ComputeZippedSize() |
Andrew Grieve | f8ff0e5 | 2017-08-17 21:43:03 | [diff] [blame] | 494 | # Always look at uncompressed .so. |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 495 | normalized_apk_size -= native_code.ComputeZippedSize() |
| 496 | normalized_apk_size += native_code.ComputeUncompressedSize() |
Andrew Grieve | 3581906 | 2017-08-30 20:14:49 | [diff] [blame] | 497 | # TODO(agrieve): Once we have better tooling (which can tell you where dex |
| 498 | # size came from), change this to "ComputeExtractedSize()". |
| 499 | normalized_apk_size += java_code.ComputeUncompressedSize() |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 500 | # Avoid noise caused when strings change and translations haven't yet been |
| 501 | # updated. |
estevenson | f320230 | 2017-02-02 17:24:08 | [diff] [blame] | 502 | num_translations = translations.GetNumEntries() |
Eric Stevenson | 62bfb2cb4 | 2017-09-08 21:14:19 | [diff] [blame] | 503 | num_stored_translations = stored_translations.GetNumEntries() |
| 504 | |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 505 | if num_translations > 1: |
| 506 | # Multipliers found by looking at MonochromePublic.apk and seeing how much |
| 507 | # smaller en-US.pak is relative to the average locale.pak. |
| 508 | normalized_apk_size = _NormalizeLanguagePaks( |
| 509 | translations, normalized_apk_size, 1.17) |
Eric Stevenson | 62bfb2cb4 | 2017-09-08 21:14:19 | [diff] [blame] | 510 | if num_stored_translations > 1: |
estevenson | 3311c02 | 2017-06-19 19:39:05 | [diff] [blame] | 511 | normalized_apk_size = _NormalizeLanguagePaks( |
| 512 | stored_translations, normalized_apk_size, 1.43) |
Eric Stevenson | 62bfb2cb4 | 2017-09-08 21:14:19 | [diff] [blame] | 513 | if num_translations + num_stored_translations > 1: |
| 514 | if num_translations == 0: |
| 515 | # WebView stores all locale paks uncompressed. |
| 516 | num_arsc_translations = num_stored_translations |
| 517 | else: |
| 518 | # Monochrome has more configurations than Chrome since it includes |
| 519 | # WebView (which supports more locales), but these should mostly be empty |
| 520 | # so ignore them here. |
| 521 | num_arsc_translations = num_translations |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 522 | normalized_apk_size += int(_NormalizeResourcesArsc( |
| 523 | apk_filename, arsc.GetNumEntries(), num_arsc_translations, out_dir)) |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 524 | |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 525 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 526 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 527 | 'normalized apk size', normalized_apk_size, 'bytes') |
| 528 | |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 529 | perf_tests_results_helper.ReportPerfResult(chartjson, |
| 530 | apk_basename + '_Specifics', |
agrieve | 07a7037 | 2016-11-23 22:04:31 | [diff] [blame] | 531 | 'file count', len(apk_contents), 'zip entries') |
| 532 | |
agrieve | 0c10a53 | 2017-01-03 19:41:00 | [diff] [blame] | 533 | for info in unknown.AllEntries(): |
| 534 | print 'Unknown entry:', info.filename, info.compress_size |
| 535 | |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 536 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 537 | def _AnnotatePakResources(out_dir): |
agrieve | 068272b | 2017-02-15 19:37:45 | [diff] [blame] | 538 | """Returns a pair of maps: id_name_map, id_header_map.""" |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 539 | print 'Looking at resources in: %s' % out_dir |
| 540 | |
| 541 | grit_headers = [] |
| 542 | for root, _, files in os.walk(out_dir): |
| 543 | if root.endswith('grit'): |
| 544 | grit_headers += [os.path.join(root, f) for f in files if f.endswith('.h')] |
| 545 | assert grit_headers, 'Failed to find grit headers in %s' % out_dir |
| 546 | |
| 547 | id_name_map = {} |
agrieve | 068272b | 2017-02-15 19:37:45 | [diff] [blame] | 548 | id_header_map = {} |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 549 | for header in grit_headers: |
| 550 | with open(header, 'r') as f: |
| 551 | for line in f.readlines(): |
| 552 | m = _RC_HEADER_RE.match(line.strip()) |
| 553 | if m: |
| 554 | i = int(m.group('id')) |
| 555 | name = m.group('name') |
| 556 | if i in id_name_map and name != id_name_map[i]: |
| 557 | print 'WARNING: Resource ID conflict %s (%s vs %s)' % ( |
| 558 | i, id_name_map[i], name) |
| 559 | id_name_map[i] = name |
agrieve | 068272b | 2017-02-15 19:37:45 | [diff] [blame] | 560 | id_header_map[i] = os.path.relpath(header, out_dir) |
| 561 | return id_name_map, id_header_map |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 562 | |
| 563 | |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 564 | # This method also used by //build/android/gyp/assert_static_initializers.py |
Ian Vollick | cdb8e99 | 2018-05-08 12:06:42 | [diff] [blame] | 565 | def AnalyzeStaticInitializers(apk_filename, tool_prefix, dump_sis, out_dir, |
| 566 | ignored_libs): |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 567 | # Static initializer counting mostly copies logic in |
| 568 | # infra/scripts/legacy/scripts/slave/chromium/sizes.py. |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 569 | with zipfile.ZipFile(apk_filename) as z: |
estevenson | 7ad22b2 | 2017-04-10 16:14:02 | [diff] [blame] | 570 | so_files = [f for f in z.infolist() |
Ian Vollick | cdb8e99 | 2018-05-08 12:06:42 | [diff] [blame] | 571 | if f.filename.endswith('.so') and f.file_size > 0 |
| 572 | and os.path.basename(f.filename) not in ignored_libs] |
estevenson | 7ad22b2 | 2017-04-10 16:14:02 | [diff] [blame] | 573 | # Skip checking static initializers for 32 bit .so files when 64 bit .so files |
| 574 | # are present since the 32 bit versions will be checked by bots that only |
| 575 | # build the 32 bit version. This avoids the complexity of finding 32 bit .so |
| 576 | # files in the output directory in 64 bit builds. |
| 577 | has_64 = any('64' in f.filename for f in so_files) |
| 578 | files_to_check = [f for f in so_files if not has_64 or '64' in f.filename] |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 579 | |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 580 | si_count = 0 |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 581 | for f in files_to_check: |
| 582 | with Unzip(apk_filename, filename=f.filename) as unzipped_so: |
| 583 | si_count += CountStaticInitializers(unzipped_so, tool_prefix) |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 584 | if dump_sis: |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 585 | # Print count and list of SIs reported by dump-static-initializers.py. |
| 586 | # Doesn't work well on all archs (particularly arm), which is why |
| 587 | # the readelf method is used for tracking SI counts. |
| 588 | _PrintDumpSIsCount(f.filename, unzipped_so, out_dir, tool_prefix) |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 589 | return si_count |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 590 | |
| 591 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 592 | def _PrintDumpSIsCount(apk_so_name, unzipped_so, out_dir, tool_prefix): |
| 593 | lib_name = os.path.basename(apk_so_name).replace('crazy.', '') |
| 594 | so_with_symbols_path = os.path.join(out_dir, 'lib.unstripped', lib_name) |
| 595 | if os.path.exists(so_with_symbols_path): |
| 596 | _VerifyLibBuildIdsMatch(tool_prefix, unzipped_so, so_with_symbols_path) |
| 597 | sis, _ = GetStaticInitializers( |
| 598 | so_with_symbols_path, tool_prefix) |
| 599 | for si in sis: |
| 600 | print si |
| 601 | else: |
| 602 | raise Exception('Unstripped .so not found. Looked here: %s', |
| 603 | so_with_symbols_path) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 604 | |
Eric Stevenson | 7ab22ee | 2017-06-09 17:53:31 | [diff] [blame] | 605 | |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 606 | def _CalculateCompressedSize(file_path): |
| 607 | CHUNK_SIZE = 256 * 1024 |
| 608 | compressor = zlib.compressobj() |
| 609 | total_size = 0 |
| 610 | with open(file_path, 'rb') as f: |
| 611 | for chunk in iter(lambda: f.read(CHUNK_SIZE), ''): |
| 612 | total_size += len(compressor.compress(chunk)) |
| 613 | total_size += len(compressor.flush()) |
| 614 | return total_size |
| 615 | |
| 616 | |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 617 | def _PrintDexAnalysis(apk_filename, chartjson=None): |
Sam Maier | eb7233ce | 2018-05-29 13:36:55 | [diff] [blame] | 618 | sizes, total_size = method_count.ExtractSizesFromZip(apk_filename) |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 619 | |
| 620 | graph_title = os.path.basename(apk_filename) + '_Dex' |
| 621 | dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE |
Sam Maier | eb7233ce | 2018-05-29 13:36:55 | [diff] [blame] | 622 | cumulative_sizes = collections.defaultdict(int) |
| 623 | for classes_dex_sizes in sizes.values(): |
| 624 | for key in dex_metrics: |
| 625 | cumulative_sizes[key] += classes_dex_sizes[key] |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 626 | for key, label in dex_metrics.iteritems(): |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 627 | perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, label, |
Sam Maier | eb7233ce | 2018-05-29 13:36:55 | [diff] [blame] | 628 | cumulative_sizes[key], 'entries') |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 629 | |
| 630 | graph_title = '%sCache' % graph_title |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 631 | perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, 'DexCache', |
Sam Maier | eb7233ce | 2018-05-29 13:36:55 | [diff] [blame] | 632 | total_size, 'bytes') |
agrieve | ef2220f6 | 2016-09-01 18:42:34 | [diff] [blame] | 633 | |
| 634 | |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 635 | def _PrintPatchSizeEstimate(new_apk, builder, bucket, chartjson=None): |
| 636 | apk_name = os.path.basename(new_apk) |
| 637 | title = apk_name + '_PatchSizeEstimate' |
| 638 | # Reference APK paths have spaces replaced by underscores. |
| 639 | builder = builder.replace(' ', '_') |
| 640 | old_apk = apk_downloader.MaybeDownloadApk( |
| 641 | builder, apk_downloader.CURRENT_MILESTONE, apk_name, |
| 642 | apk_downloader.DEFAULT_DOWNLOAD_PATH, bucket) |
| 643 | if old_apk: |
| 644 | # Use a temp dir in case patch size functions fail to clean up temp files. |
| 645 | with build_utils.TempDir() as tmp: |
| 646 | tmp_name = os.path.join(tmp, 'patch.tmp') |
| 647 | bsdiff = apk_patch_size_estimator.calculate_bsdiff( |
| 648 | old_apk, new_apk, None, tmp_name) |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 649 | perf_tests_results_helper.ReportPerfResult(chartjson, title, |
| 650 | 'BSDiff (gzipped)', bsdiff, 'bytes') |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 651 | fbf = apk_patch_size_estimator.calculate_filebyfile( |
| 652 | old_apk, new_apk, None, tmp_name) |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 653 | perf_tests_results_helper.ReportPerfResult(chartjson, title, |
| 654 | 'FileByFile (gzipped)', fbf, 'bytes') |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 655 | |
| 656 | |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 657 | @contextmanager |
| 658 | def Unzip(zip_file, filename=None): |
| 659 | """Utility for temporary use of a single file in a zip archive.""" |
| 660 | with build_utils.TempDir() as unzipped_dir: |
| 661 | unzipped_files = build_utils.ExtractAll( |
| 662 | zip_file, unzipped_dir, True, pattern=filename) |
| 663 | if len(unzipped_files) == 0: |
| 664 | raise Exception( |
| 665 | '%s not found in %s' % (filename, zip_file)) |
| 666 | yield unzipped_files[0] |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 667 | |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 668 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 669 | def _VerifyLibBuildIdsMatch(tool_prefix, *so_files): |
| 670 | if len(set(_ParseLibBuildId(f, tool_prefix) for f in so_files)) > 1: |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 671 | raise Exception('Found differing build ids in output directory and apk. ' |
| 672 | 'Your output directory is likely stale.') |
| 673 | |
| 674 | |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 675 | def _ConfigOutDirAndToolsPrefix(out_dir): |
| 676 | if out_dir: |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 677 | constants.SetOutputDirectory(os.path.abspath(out_dir)) |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 678 | else: |
| 679 | try: |
| 680 | out_dir = constants.GetOutDirectory() |
| 681 | devil_chromium.Initialize() |
| 682 | except EnvironmentError: |
| 683 | pass |
| 684 | if out_dir: |
Andrew Grieve | 5cebabf3 | 2018-06-26 15:16:04 | [diff] [blame] | 685 | build_vars = build_utils.ReadBuildVars( |
| 686 | os.path.join(out_dir, "build_vars.txt")) |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 687 | tool_prefix = os.path.join(out_dir, build_vars['android_tool_prefix']) |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 688 | else: |
| 689 | tool_prefix = '' |
| 690 | return out_dir, tool_prefix |
| 691 | |
| 692 | |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 693 | def main(): |
| 694 | argparser = argparse.ArgumentParser(description='Print APK size metrics.') |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 695 | argparser.add_argument('--min-pak-resource-size', |
| 696 | type=int, |
| 697 | default=20*1024, |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 698 | help='Minimum byte size of displayed pak resources.') |
| 699 | argparser.add_argument('--chromium-output-directory', |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 700 | dest='out_dir', |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 701 | help='Location of the build artifacts.') |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 702 | argparser.add_argument('--chartjson', |
| 703 | action='store_true', |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 704 | help='Sets output mode to chartjson.') |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 705 | argparser.add_argument('--output-dir', |
| 706 | default='.', |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 707 | help='Directory to save chartjson to.') |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 708 | argparser.add_argument('--dump-static-initializers', |
| 709 | action='store_true', |
| 710 | dest='dump_sis', |
Eric Stevenson | 7ab22ee | 2017-06-09 17:53:31 | [diff] [blame] | 711 | help='Run dump-static-initializers.py to get the list' |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 712 | 'of static initializers (slow).') |
Ian Vollick | cdb8e99 | 2018-05-08 12:06:42 | [diff] [blame] | 713 | argparser.add_argument('--loadable_module', |
| 714 | action='append', |
| 715 | help='Use for libraries added via loadable_modules') |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 716 | argparser.add_argument('--estimate-patch-size', |
| 717 | action='store_true', |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 718 | help='Include patch size estimates. Useful for perf ' |
| 719 | 'builders where a reference APK is available but adds ' |
| 720 | '~3 mins to run time.') |
| 721 | argparser.add_argument('--reference-apk-builder', |
| 722 | default=apk_downloader.DEFAULT_BUILDER, |
| 723 | help='Builder name to use for reference APK for patch ' |
| 724 | 'size estimates.') |
| 725 | argparser.add_argument('--reference-apk-bucket', |
| 726 | default=apk_downloader.DEFAULT_BUCKET, |
| 727 | help='Storage bucket holding reference APKs.') |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 728 | argparser.add_argument('apk', help='APK file path.') |
| 729 | args = argparser.parse_args() |
| 730 | |
| 731 | chartjson = _BASE_CHART.copy() if args.chartjson else None |
Eric Stevenson | 2011d153 | 2017-07-21 15:40:53 | [diff] [blame] | 732 | out_dir, tool_prefix = _ConfigOutDirAndToolsPrefix(args.out_dir) |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 733 | if args.dump_sis and not out_dir: |
| 734 | argparser.error( |
| 735 | '--dump-static-initializers requires --chromium-output-directory') |
| 736 | |
Andrew Grieve | 2143a47 | 2017-09-26 21:09:11 | [diff] [blame] | 737 | # Do not add any new metrics without also documenting them in: |
| 738 | # //docs/speed/binary_size/metrics.md. |
| 739 | |
Eric Stevenson | 0cd4fc80 | 2017-09-15 20:20:05 | [diff] [blame] | 740 | PrintApkAnalysis(args.apk, tool_prefix, out_dir, chartjson=chartjson) |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 741 | _PrintDexAnalysis(args.apk, chartjson=chartjson) |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 742 | |
Ian Vollick | cdb8e99 | 2018-05-08 12:06:42 | [diff] [blame] | 743 | ignored_libs = args.loadable_module if args.loadable_module else [] |
| 744 | |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 745 | si_count = AnalyzeStaticInitializers( |
Ian Vollick | cdb8e99 | 2018-05-08 12:06:42 | [diff] [blame] | 746 | args.apk, tool_prefix, args.dump_sis, out_dir, ignored_libs) |
bsheedy | 20e696b | 2017-09-13 20:31:05 | [diff] [blame] | 747 | perf_tests_results_helper.ReportPerfResult( |
| 748 | chartjson, 'StaticInitializersCount', 'count', si_count, 'count') |
Andrew Grieve | 860b155 | 2017-09-06 14:50:06 | [diff] [blame] | 749 | |
estevenson | 0700eafcd | 2017-03-21 18:41:34 | [diff] [blame] | 750 | if args.estimate_patch_size: |
| 751 | _PrintPatchSizeEstimate(args.apk, args.reference_apk_builder, |
| 752 | args.reference_apk_bucket, chartjson=chartjson) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 753 | if chartjson: |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 754 | results_path = os.path.join(args.output_dir, 'results-chart.json') |
rnephew | 526a5c3 | 2016-04-28 21:37:05 | [diff] [blame] | 755 | logging.critical('Dumping json to %s', results_path) |
rnephew | f10ebab1 | 2015-12-17 23:06:59 | [diff] [blame] | 756 | with open(results_path, 'w') as json_file: |
| 757 | json.dump(chartjson, json_file) |
| 758 | |
| 759 | |
| 760 | if __name__ == '__main__': |
estevenson | 636c61da | 2017-02-28 21:01:37 | [diff] [blame] | 761 | sys.exit(main()) |