blob: 6287d9842e4395af32278712333dd0702e20525e [file] [log] [blame]
Ingemar Ã…dahl504c0762017-07-05 14:42:431#!/usr/bin/env python
rnephewf10ebab12015-12-17 23:06:592# 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 Grieve2143a472017-09-26 21:09:116"""Reports binary size and static initializer metrics for an APK.
7
8More information at //docs/speed/binary_size/metrics.md.
rnephewf10ebab12015-12-17 23:06:599"""
10
estevenson636c61da2017-02-28 21:01:3711import argparse
rnephewf10ebab12015-12-17 23:06:5912import collections
estevenson636c61da2017-02-28 21:01:3713from contextlib import contextmanager
rnephewf10ebab12015-12-17 23:06:5914import json
rnephew526a5c32016-04-28 21:37:0515import logging
rnephewf10ebab12015-12-17 23:06:5916import os
17import re
agrieve83011ed2016-05-12 19:56:4518import struct
rnephewf10ebab12015-12-17 23:06:5919import sys
rnephewf10ebab12015-12-17 23:06:5920import zipfile
21import zlib
22
estevenson0700eafcd2017-03-21 18:41:3423from binary_size import apk_downloader
jbudorickd28554a2016-01-11 16:22:5924import devil_chromium
estevensonf3202302017-02-02 17:24:0825from devil.android.sdk import build_tools
rnephewf10ebab12015-12-17 23:06:5926from devil.utils import cmd_helper
estevensonf3202302017-02-02 17:24:0827from devil.utils import lazy
agrieveef2220f62016-09-01 18:42:3428import method_count
agrieve05957d7e2016-04-05 21:06:0629from pylib import constants
jbudorickd28554a2016-01-11 16:22:5930from pylib.constants import host_paths
rnephewf10ebab12015-12-17 23:06:5931
estevensonf3202302017-02-02 17:24:0832_AAPT_PATH = lazy.WeakConstant(lambda: build_tools.GetPath('aapt'))
estevenson636c61da2017-02-28 21:01:3733_BUILD_UTILS_PATH = os.path.join(
34 host_paths.DIR_SOURCE_ROOT, 'build', 'android', 'gyp')
estevenson0700eafcd2017-03-21 18:41:3435_APK_PATCH_SIZE_ESTIMATOR_PATH = os.path.join(
36 host_paths.DIR_SOURCE_ROOT, 'third_party', 'apk-patch-size-estimator')
jbudorickd28554a2016-01-11 16:22:5937
jbudorickd28554a2016-01-11 16:22:5938with host_paths.SysPath(host_paths.BUILD_COMMON_PATH):
39 import perf_tests_results_helper # pylint: disable=import-error
rnephewf10ebab12015-12-17 23:06:5940
mikecase79ae4652017-05-19 17:29:2241with host_paths.SysPath(_BUILD_UTILS_PATH, 0):
estevenson636c61da2017-02-28 21:01:3742 from util import build_utils # pylint: disable=import-error
43
estevenson0700eafcd2017-03-21 18:41:3444with host_paths.SysPath(_APK_PATCH_SIZE_ESTIMATOR_PATH):
45 import apk_patch_size_estimator # pylint: disable=import-error
46
agrieveef2220f62016-09-01 18:42:3447
agrieve83011ed2016-05-12 19:56:4548# Python had a bug in zipinfo parsing that triggers on ChromeModern.apk
49# https://bugs.python.org/issue14315
50def _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
85zipfile.ZipInfo._decodeExtra = ( # pylint: disable=protected-access
86 _PatchedDecodeExtra)
rnephewf10ebab12015-12-17 23:06:5987
estevensonf3202302017-02-02 17:24:0888# 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)
rnephewf10ebab12015-12-17 23:06:5993_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}
rnephewb5fe0972016-02-10 17:45:13100_DUMP_STATIC_INITIALIZERS_PATH = os.path.join(
101 host_paths.DIR_SOURCE_ROOT, 'tools', 'linux', 'dump-static-initializers.py')
agrieve12ff4bb2016-05-19 15:57:34102# Pragma exists when enable_resource_whitelist_generation=true.
103_RC_HEADER_RE = re.compile(
104 r'^#define (?P<name>\w+) (?:_Pragma\(.*?\) )?(?P<id>\d+)$')
estevenson3311c022017-06-19 19:39:05105_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$')
estevensonb57a1342017-02-06 11:34:23109_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 Grievec3dd28e2018-04-04 17:17:23113 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr',
114 '.ARM.exidxsentinel_section_after_text'],
estevensonb57a1342017-02-06 11:34:23115 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt',
Andrew Grievec3dd28e2018-04-04 17:17:23116 '.got.plt', '.hash', '.gnu.hash'],
estevensonb57a1342017-02-06 11:34:23117 'bss': ['.bss'],
118 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version',
Andrew Grievec3dd28e2018-04-04 17:17:23119 '.note.crashpad.info', '.note.android.ident',
estevensonb57a1342017-02-06 11:34:23120 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version',
121 '.gnu.version_d', '.gnu.version_r', '.interp', '.gcc_except_table']
122}
123
124
Eric Stevenson2011d1532017-07-21 15:40:53125def _RunReadelf(so_path, options, tool_prefix=''):
estevenson636c61da2017-02-28 21:01:37126 return cmd_helper.GetCmdOutput(
Eric Stevenson2011d1532017-07-21 15:40:53127 [tool_prefix + 'readelf'] + options + [so_path])
estevensonb57a1342017-02-06 11:34:23128
129
Eric Stevenson2011d1532017-07-21 15:40:53130def _ExtractMainLibSectionSizesFromApk(apk_path, main_lib_path, tool_prefix):
estevenson636c61da2017-02-28 21:01:37131 with Unzip(apk_path, filename=main_lib_path) as extracted_lib_path:
132 grouped_section_sizes = collections.defaultdict(int)
Eric Stevenson2011d1532017-07-21 15:40:53133 section_sizes = _CreateSectionNameSizeMap(extracted_lib_path, tool_prefix)
estevenson636c61da2017-02-28 21:01:37134 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 Stevenson2011d1532017-07-21 15:40:53147def _CreateSectionNameSizeMap(so_path, tool_prefix):
148 stdout = _RunReadelf(so_path, ['-S', '--wide'], tool_prefix)
estevensonb57a1342017-02-06 11:34:23149 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
rnephewf10ebab12015-12-17 23:06:59156
157
Eric Stevenson2011d1532017-07-21 15:40:53158def _ParseLibBuildId(so_path, tool_prefix):
estevenson636c61da2017-02-28 21:01:37159 """Returns the Build ID of the given native library."""
Eric Stevenson2011d1532017-07-21 15:40:53160 stdout = _RunReadelf(so_path, ['-n'], tool_prefix)
estevenson636c61da2017-02-28 21:01:37161 match = re.search(r'Build ID: (\w+)', stdout)
162 return match.group(1) if match else None
163
164
estevenson3311c022017-06-19 19:39:05165def _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 Grievef8ff0e52017-08-17 21:43:03178 # L, M: ART - .odex file is compiled version of the dex file (~4x).
estevenson3311c022017-06-19 19:39:05179 # N: ART - Uses Dalvik-like JIT for normal apps (~1x), full compilation for
Andrew Grievef8ff0e52017-08-17 21:43:03180 # 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.
estevenson3311c022017-06-19 19:39:05184 if sdk_version < 21:
Andrew Grievef8ff0e52017-08-17 21:43:03185 dex_multiplier = 1.16
estevenson3311c022017-06-19 19:39:05186 elif sdk_version < 24:
Andrew Grievef8ff0e52017-08-17 21:43:03187 dex_multiplier = 4.04
estevenson3311c022017-06-19 19:39:05188 elif 'Monochrome' in apk_path or 'WebView' in apk_path:
Andrew Grievef8ff0e52017-08-17 21:43:03189 dex_multiplier = 4.04 # compilation_filter=speed
estevenson3311c022017-06-19 19:39:05190 else:
Andrew Grievef8ff0e52017-08-17 21:43:03191 dex_multiplier = 1.17 # compilation_filter=speed-profile
estevenson3311c022017-06-19 19:39:05192
193 return dex_multiplier, skip_extract_lib
194
195
Eric Stevenson2011d1532017-07-21 15:40:53196def CountStaticInitializers(so_path, tool_prefix):
197 # Mostly copied from //infra/scripts/legacy/scripts/slave/chromium/sizes.py.
rnephewb5fe0972016-02-10 17:45:13198 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 Stevenson2011d1532017-07-21 15:40:53209 stdout = _RunReadelf(so_path, ['-h'], tool_prefix)
rnephewb5fe0972016-02-10 17:45:13210 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 Stevenson2011d1532017-07-21 15:40:53221 stdout = _RunReadelf(so_path, ['-SW'], tool_prefix)
rnephewb5fe0972016-02-10 17:45:13222 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 Stevenson2011d1532017-07-21 15:40:53229def GetStaticInitializers(so_path, tool_prefix):
rnephewb5fe0972016-02-10 17:45:13230 output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d',
Eric Stevenson2011d1532017-07-21 15:40:53231 so_path, '-t', tool_prefix])
estevenson636c61da2017-02-28 21:01:37232 summary = re.search(r'Found \d+ static initializers in (\d+) files.', output)
233 return output.splitlines()[:-1], int(summary.group(1))
rnephewf10ebab12015-12-17 23:06:59234
235
estevenson3311c022017-06-19 19:39:05236def _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 Stevenson0cd4fc802017-09-15 20:20:05246def _NormalizeResourcesArsc(apk_path, num_arsc_files, num_translations,
247 out_dir):
estevensonf3202302017-02-02 17:24:08248 """Estimates the expected overhead of untranslated strings in resources.arsc.
249
250 See http://crbug.com/677966 for why this is necessary.
251 """
Eric Stevenson0cd4fc802017-09-15 20:20:05252 # 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
estevensonf3202302017-02-02 17:24:08262
Eric Stevenson0cd4fc802017-09-15 20:20:05263 aapt_output = _RunAaptDumpResources(apk_path)
estevensonf3202302017-02-02 17:24:08264 # 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 Stevenson62bfb2cb42017-09-08 21:14:19269 # en-US and en-GB will never be translated.
270 config_count = num_translations - 2
estevensonf3202302017-02-02 17:24:08271
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
285def _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
293def _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
agrieve07a70372016-11-23 22:04:31302class _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 = []
estevenson3311c022017-06-19 19:39:05308 self._extracted_multipliers = []
agrieve07a70372016-11-23 22:04:31309
estevenson3311c022017-06-19 19:39:05310 def AddZipInfo(self, zip_info, extracted_multiplier=0):
agrieve07a70372016-11-23 22:04:31311 self._zip_infos.append(zip_info)
estevenson3311c022017-06-19 19:39:05312 self._extracted_multipliers.append(extracted_multiplier)
agrieve07a70372016-11-23 22:04:31313
agrieve0c10a532017-01-03 19:41:00314 def AllEntries(self):
315 return iter(self._zip_infos)
316
agrieve07a70372016-11-23 22:04:31317 def GetNumEntries(self):
318 return len(self._zip_infos)
319
320 def FindByPattern(self, pattern):
agrievebc2287b02016-12-12 23:13:01321 return next((i for i in self._zip_infos if re.match(pattern, i.filename)),
322 None)
agrieve07a70372016-11-23 22:04:31323
324 def FindLargest(self):
jbudorick53e276792017-04-06 17:13:54325 if not self._zip_infos:
326 return None
agrieve07a70372016-11-23 22:04:31327 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
estevenson3311c022017-06-19 19:39:05337 for zi, multiplier in zip(self._zip_infos, self._extracted_multipliers):
338 ret += zi.file_size * multiplier
agrieve07a70372016-11-23 22:04:31339 return ret
340
341 def ComputeInstallSize(self):
342 return self.ComputeExtractedSize() + self.ComputeZippedSize()
343
344
Eric Stevenson0cd4fc802017-09-15 20:20:05345def PrintApkAnalysis(apk_filename, tool_prefix, out_dir, chartjson=None):
rnephewf10ebab12015-12-17 23:06:59346 """Analyse APK to determine size contributions of different file classes."""
agrieve07a70372016-11-23 22:04:31347 file_groups = []
rnephewf10ebab12015-12-17 23:06:59348
agrieve07a70372016-11-23 22:04:31349 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)')
estevenson3311c022017-06-19 19:39:05358 stored_translations = make_group('Native resources stored (l10n)')
agrieve07a70372016-11-23 22:04:31359 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')
agrieve0c10a532017-01-03 19:41:00366 notices = make_group('licenses.notice file')
Andrew Grieve9b8952c82018-04-04 16:38:31367 unwind_cfi = make_group('unwind_cfi (dev and canary only)')
rnephewf10ebab12015-12-17 23:06:59368
369 apk = zipfile.ZipFile(apk_filename, 'r')
370 try:
371 apk_contents = apk.infolist()
372 finally:
373 apk.close()
374
estevenson3311c022017-06-19 19:39:05375 dex_multiplier, skip_extract_lib = _ParseManifestAttributes(apk_filename)
rnephewf10ebab12015-12-17 23:06:59376 total_apk_size = os.path.getsize(apk_filename)
377 apk_basename = os.path.basename(apk_filename)
rnephewf10ebab12015-12-17 23:06:59378 for member in apk_contents:
agrieve07a70372016-11-23 22:04:31379 filename = member.filename
380 if filename.endswith('/'):
381 continue
agrieve07a70372016-11-23 22:04:31382 if filename.endswith('.so'):
estevenson3311c022017-06-19 19:39:05383 should_extract_lib = not (skip_extract_lib or 'crazy' in filename)
384 native_code.AddZipInfo(
385 member, extracted_multiplier=int(should_extract_lib))
agrieve07a70372016-11-23 22:04:31386 elif filename.endswith('.dex'):
estevenson3311c022017-06-19 19:39:05387 java_code.AddZipInfo(member, extracted_multiplier=dex_multiplier)
388 elif re.search(_RE_NON_LANGUAGE_PAK, filename):
agrieve07a70372016-11-23 22:04:31389 native_resources_no_translations.AddZipInfo(member)
estevenson3311c022017-06-19 19:39:05390 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)
agrieve07a70372016-11-23 22:04:31396 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)
agrieve0c10a532017-01-03 19:41:00408 elif filename.endswith('.notice'):
409 notices.AddZipInfo(member)
Andrew Grieve9b8952c82018-04-04 16:38:31410 elif filename.startswith('assets/unwind_cfi'):
411 unwind_cfi.AddZipInfo(member)
rnephewf10ebab12015-12-17 23:06:59412 else:
agrieve07a70372016-11-23 22:04:31413 unknown.AddZipInfo(member)
rnephewf10ebab12015-12-17 23:06:59414
415 total_install_size = total_apk_size
Andrew Grieve767a6f52017-07-28 18:11:29416 zip_overhead = total_apk_size
rnephewf10ebab12015-12-17 23:06:59417
agrieve07a70372016-11-23 22:04:31418 for group in file_groups:
Andrew Grieve767a6f52017-07-28 18:11:29419 actual_size = group.ComputeZippedSize()
agrieve07a70372016-11-23 22:04:31420 install_size = group.ComputeInstallSize()
Andrew Grieve58f44702017-09-25 18:21:54421 uncompressed_size = group.ComputeUncompressedSize()
Andrew Grieve767a6f52017-07-28 18:11:29422
agrieve07a70372016-11-23 22:04:31423 total_install_size += group.ComputeExtractedSize()
Andrew Grieve767a6f52017-07-28 18:11:29424 zip_overhead -= actual_size
rnephewf10ebab12015-12-17 23:06:59425
bsheedy20e696b2017-09-13 20:31:05426 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',
rnephewf10ebab12015-12-17 23:06:59431 group.name + ' size', install_size, 'bytes')
Andrew Grieve58f44702017-09-25 18:21:54432 # 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')
rnephewf10ebab12015-12-17 23:06:59440
Andrew Grieve767a6f52017-07-28 18:11:29441 # 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.
bsheedy20e696b2017-09-13 20:31:05445 perf_tests_results_helper.ReportPerfResult(chartjson,
446 apk_basename + '_Breakdown', 'Zip Overhead',
Andrew Grieve767a6f52017-07-28 18:11:29447 zip_overhead, 'bytes')
bsheedy20e696b2017-09-13 20:31:05448 perf_tests_results_helper.ReportPerfResult(chartjson,
449 apk_basename + '_InstallSize', 'APK size',
rnephewf10ebab12015-12-17 23:06:59450 total_apk_size, 'bytes')
bsheedy20e696b2017-09-13 20:31:05451 perf_tests_results_helper.ReportPerfResult(chartjson,
452 apk_basename + '_InstallSize',
agrieve07a70372016-11-23 22:04:31453 'Estimated installed size', total_install_size, 'bytes')
454 transfer_size = _CalculateCompressedSize(apk_filename)
bsheedy20e696b2017-09-13 20:31:05455 perf_tests_results_helper.ReportPerfResult(chartjson,
456 apk_basename + '_TransferSize',
rnephewf10ebab12015-12-17 23:06:59457 'Transfer size (deflate)', transfer_size, 'bytes')
458
agrieve07a70372016-11-23 22:04:31459 # 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
bsheedy20e696b2017-09-13 20:31:05463 perf_tests_results_helper.ReportPerfResult(chartjson,
464 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31465 'main dex size', main_dex_size, 'bytes')
466 secondary_size = java_code.ComputeUncompressedSize() - main_dex_size
bsheedy20e696b2017-09-13 20:31:05467 perf_tests_results_helper.ReportPerfResult(chartjson,
468 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31469 '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
bsheedy20e696b2017-09-13 20:31:05475 perf_tests_results_helper.ReportPerfResult(chartjson,
476 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31477 'main lib size', main_lib_size, 'bytes')
478 secondary_size = native_code.ComputeUncompressedSize() - main_lib_size
bsheedy20e696b2017-09-13 20:31:05479 perf_tests_results_helper.ReportPerfResult(chartjson,
480 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31481 'other lib size', secondary_size, 'bytes')
482
estevensonb57a1342017-02-06 11:34:23483 main_lib_section_sizes = _ExtractMainLibSectionSizesFromApk(
Eric Stevenson2011d1532017-07-21 15:40:53484 apk_filename, main_lib_info.filename, tool_prefix)
estevensonb57a1342017-02-06 11:34:23485 for metric_name, size in main_lib_section_sizes.iteritems():
bsheedy20e696b2017-09-13 20:31:05486 perf_tests_results_helper.ReportPerfResult(chartjson,
487 apk_basename + '_MainLibInfo',
estevensonb57a1342017-02-06 11:34:23488 metric_name, size, 'bytes')
489
agrieve07a70372016-11-23 22:04:31490 # Main metric that we want to monitor for jumps.
491 normalized_apk_size = total_apk_size
Andrew Grieve9b8952c82018-04-04 16:38:31492 # unwind_cfi exists only in dev, canary, and non-channel builds.
493 normalized_apk_size -= unwind_cfi.ComputeZippedSize()
Andrew Grievef8ff0e52017-08-17 21:43:03494 # Always look at uncompressed .so.
agrieve07a70372016-11-23 22:04:31495 normalized_apk_size -= native_code.ComputeZippedSize()
496 normalized_apk_size += native_code.ComputeUncompressedSize()
Andrew Grieve35819062017-08-30 20:14:49497 # 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()
agrieve07a70372016-11-23 22:04:31500 # Avoid noise caused when strings change and translations haven't yet been
501 # updated.
estevensonf3202302017-02-02 17:24:08502 num_translations = translations.GetNumEntries()
Eric Stevenson62bfb2cb42017-09-08 21:14:19503 num_stored_translations = stored_translations.GetNumEntries()
504
estevenson3311c022017-06-19 19:39:05505 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 Stevenson62bfb2cb42017-09-08 21:14:19510 if num_stored_translations > 1:
estevenson3311c022017-06-19 19:39:05511 normalized_apk_size = _NormalizeLanguagePaks(
512 stored_translations, normalized_apk_size, 1.43)
Eric Stevenson62bfb2cb42017-09-08 21:14:19513 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 Stevenson0cd4fc802017-09-15 20:20:05522 normalized_apk_size += int(_NormalizeResourcesArsc(
523 apk_filename, arsc.GetNumEntries(), num_arsc_translations, out_dir))
agrieve07a70372016-11-23 22:04:31524
bsheedy20e696b2017-09-13 20:31:05525 perf_tests_results_helper.ReportPerfResult(chartjson,
526 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31527 'normalized apk size', normalized_apk_size, 'bytes')
528
bsheedy20e696b2017-09-13 20:31:05529 perf_tests_results_helper.ReportPerfResult(chartjson,
530 apk_basename + '_Specifics',
agrieve07a70372016-11-23 22:04:31531 'file count', len(apk_contents), 'zip entries')
532
agrieve0c10a532017-01-03 19:41:00533 for info in unknown.AllEntries():
534 print 'Unknown entry:', info.filename, info.compress_size
535
rnephewf10ebab12015-12-17 23:06:59536
Eric Stevenson2011d1532017-07-21 15:40:53537def _AnnotatePakResources(out_dir):
agrieve068272b2017-02-15 19:37:45538 """Returns a pair of maps: id_name_map, id_header_map."""
rnephewf10ebab12015-12-17 23:06:59539 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 = {}
agrieve068272b2017-02-15 19:37:45548 id_header_map = {}
rnephewf10ebab12015-12-17 23:06:59549 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
agrieve068272b2017-02-15 19:37:45560 id_header_map[i] = os.path.relpath(header, out_dir)
561 return id_name_map, id_header_map
rnephewf10ebab12015-12-17 23:06:59562
563
Andrew Grieve860b1552017-09-06 14:50:06564# This method also used by //build/android/gyp/assert_static_initializers.py
Ian Vollickcdb8e992018-05-08 12:06:42565def AnalyzeStaticInitializers(apk_filename, tool_prefix, dump_sis, out_dir,
566 ignored_libs):
Eric Stevenson2011d1532017-07-21 15:40:53567 # Static initializer counting mostly copies logic in
568 # infra/scripts/legacy/scripts/slave/chromium/sizes.py.
agrieveef2220f62016-09-01 18:42:34569 with zipfile.ZipFile(apk_filename) as z:
estevenson7ad22b22017-04-10 16:14:02570 so_files = [f for f in z.infolist()
Ian Vollickcdb8e992018-05-08 12:06:42571 if f.filename.endswith('.so') and f.file_size > 0
572 and os.path.basename(f.filename) not in ignored_libs]
estevenson7ad22b22017-04-10 16:14:02573 # 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 Stevenson2011d1532017-07-21 15:40:53579
agrieveef2220f62016-09-01 18:42:34580 si_count = 0
Eric Stevenson2011d1532017-07-21 15:40:53581 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 Grieve860b1552017-09-06 14:50:06584 if dump_sis:
Eric Stevenson2011d1532017-07-21 15:40:53585 # 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 Grieve860b1552017-09-06 14:50:06589 return si_count
agrieveef2220f62016-09-01 18:42:34590
591
Eric Stevenson2011d1532017-07-21 15:40:53592def _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)
rnephewf10ebab12015-12-17 23:06:59604
Eric Stevenson7ab22ee2017-06-09 17:53:31605
rnephewf10ebab12015-12-17 23:06:59606def _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
agrieveef2220f62016-09-01 18:42:34617def _PrintDexAnalysis(apk_filename, chartjson=None):
Sam Maiereb7233ce2018-05-29 13:36:55618 sizes, total_size = method_count.ExtractSizesFromZip(apk_filename)
agrieveef2220f62016-09-01 18:42:34619
620 graph_title = os.path.basename(apk_filename) + '_Dex'
621 dex_metrics = method_count.CONTRIBUTORS_TO_DEX_CACHE
Sam Maiereb7233ce2018-05-29 13:36:55622 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]
agrieveef2220f62016-09-01 18:42:34626 for key, label in dex_metrics.iteritems():
bsheedy20e696b2017-09-13 20:31:05627 perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, label,
Sam Maiereb7233ce2018-05-29 13:36:55628 cumulative_sizes[key], 'entries')
agrieveef2220f62016-09-01 18:42:34629
630 graph_title = '%sCache' % graph_title
bsheedy20e696b2017-09-13 20:31:05631 perf_tests_results_helper.ReportPerfResult(chartjson, graph_title, 'DexCache',
Sam Maiereb7233ce2018-05-29 13:36:55632 total_size, 'bytes')
agrieveef2220f62016-09-01 18:42:34633
634
estevenson0700eafcd2017-03-21 18:41:34635def _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)
bsheedy20e696b2017-09-13 20:31:05649 perf_tests_results_helper.ReportPerfResult(chartjson, title,
650 'BSDiff (gzipped)', bsdiff, 'bytes')
estevenson0700eafcd2017-03-21 18:41:34651 fbf = apk_patch_size_estimator.calculate_filebyfile(
652 old_apk, new_apk, None, tmp_name)
bsheedy20e696b2017-09-13 20:31:05653 perf_tests_results_helper.ReportPerfResult(chartjson, title,
654 'FileByFile (gzipped)', fbf, 'bytes')
estevenson0700eafcd2017-03-21 18:41:34655
656
estevenson636c61da2017-02-28 21:01:37657@contextmanager
658def 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]
rnephewf10ebab12015-12-17 23:06:59667
rnephewf10ebab12015-12-17 23:06:59668
Eric Stevenson2011d1532017-07-21 15:40:53669def _VerifyLibBuildIdsMatch(tool_prefix, *so_files):
670 if len(set(_ParseLibBuildId(f, tool_prefix) for f in so_files)) > 1:
estevenson636c61da2017-02-28 21:01:37671 raise Exception('Found differing build ids in output directory and apk. '
672 'Your output directory is likely stale.')
673
674
Eric Stevenson2011d1532017-07-21 15:40:53675def _ConfigOutDirAndToolsPrefix(out_dir):
676 if out_dir:
Andrew Grieve860b1552017-09-06 14:50:06677 constants.SetOutputDirectory(os.path.abspath(out_dir))
Eric Stevenson2011d1532017-07-21 15:40:53678 else:
679 try:
680 out_dir = constants.GetOutDirectory()
681 devil_chromium.Initialize()
682 except EnvironmentError:
683 pass
684 if out_dir:
Andrew Grieve5cebabf32018-06-26 15:16:04685 build_vars = build_utils.ReadBuildVars(
686 os.path.join(out_dir, "build_vars.txt"))
Andrew Grieve860b1552017-09-06 14:50:06687 tool_prefix = os.path.join(out_dir, build_vars['android_tool_prefix'])
Eric Stevenson2011d1532017-07-21 15:40:53688 else:
689 tool_prefix = ''
690 return out_dir, tool_prefix
691
692
estevenson636c61da2017-02-28 21:01:37693def main():
694 argparser = argparse.ArgumentParser(description='Print APK size metrics.')
Eric Stevenson2011d1532017-07-21 15:40:53695 argparser.add_argument('--min-pak-resource-size',
696 type=int,
697 default=20*1024,
estevenson636c61da2017-02-28 21:01:37698 help='Minimum byte size of displayed pak resources.')
699 argparser.add_argument('--chromium-output-directory',
Eric Stevenson2011d1532017-07-21 15:40:53700 dest='out_dir',
estevenson636c61da2017-02-28 21:01:37701 help='Location of the build artifacts.')
Eric Stevenson2011d1532017-07-21 15:40:53702 argparser.add_argument('--chartjson',
703 action='store_true',
estevenson636c61da2017-02-28 21:01:37704 help='Sets output mode to chartjson.')
Eric Stevenson2011d1532017-07-21 15:40:53705 argparser.add_argument('--output-dir',
706 default='.',
estevenson636c61da2017-02-28 21:01:37707 help='Directory to save chartjson to.')
Eric Stevenson2011d1532017-07-21 15:40:53708 argparser.add_argument('--dump-static-initializers',
709 action='store_true',
710 dest='dump_sis',
Eric Stevenson7ab22ee2017-06-09 17:53:31711 help='Run dump-static-initializers.py to get the list'
Andrew Grieve860b1552017-09-06 14:50:06712 'of static initializers (slow).')
Ian Vollickcdb8e992018-05-08 12:06:42713 argparser.add_argument('--loadable_module',
714 action='append',
715 help='Use for libraries added via loadable_modules')
Eric Stevenson2011d1532017-07-21 15:40:53716 argparser.add_argument('--estimate-patch-size',
717 action='store_true',
estevenson0700eafcd2017-03-21 18:41:34718 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.')
estevenson636c61da2017-02-28 21:01:37728 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 Stevenson2011d1532017-07-21 15:40:53732 out_dir, tool_prefix = _ConfigOutDirAndToolsPrefix(args.out_dir)
Andrew Grieve860b1552017-09-06 14:50:06733 if args.dump_sis and not out_dir:
734 argparser.error(
735 '--dump-static-initializers requires --chromium-output-directory')
736
Andrew Grieve2143a472017-09-26 21:09:11737 # Do not add any new metrics without also documenting them in:
738 # //docs/speed/binary_size/metrics.md.
739
Eric Stevenson0cd4fc802017-09-15 20:20:05740 PrintApkAnalysis(args.apk, tool_prefix, out_dir, chartjson=chartjson)
estevenson636c61da2017-02-28 21:01:37741 _PrintDexAnalysis(args.apk, chartjson=chartjson)
Andrew Grieve860b1552017-09-06 14:50:06742
Ian Vollickcdb8e992018-05-08 12:06:42743 ignored_libs = args.loadable_module if args.loadable_module else []
744
Andrew Grieve860b1552017-09-06 14:50:06745 si_count = AnalyzeStaticInitializers(
Ian Vollickcdb8e992018-05-08 12:06:42746 args.apk, tool_prefix, args.dump_sis, out_dir, ignored_libs)
bsheedy20e696b2017-09-13 20:31:05747 perf_tests_results_helper.ReportPerfResult(
748 chartjson, 'StaticInitializersCount', 'count', si_count, 'count')
Andrew Grieve860b1552017-09-06 14:50:06749
estevenson0700eafcd2017-03-21 18:41:34750 if args.estimate_patch_size:
751 _PrintPatchSizeEstimate(args.apk, args.reference_apk_builder,
752 args.reference_apk_bucket, chartjson=chartjson)
rnephewf10ebab12015-12-17 23:06:59753 if chartjson:
estevenson636c61da2017-02-28 21:01:37754 results_path = os.path.join(args.output_dir, 'results-chart.json')
rnephew526a5c32016-04-28 21:37:05755 logging.critical('Dumping json to %s', results_path)
rnephewf10ebab12015-12-17 23:06:59756 with open(results_path, 'w') as json_file:
757 json.dump(chartjson, json_file)
758
759
760if __name__ == '__main__':
estevenson636c61da2017-02-28 21:01:37761 sys.exit(main())