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

Side by Side Diff: build/android/resource_sizes.py

Issue 2930313002: resource_sizes.py: Better pakfile and dex normalization. (Closed)
Patch Set: comment typo Created 3 years, 6 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 unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Prints the size of each given file and optionally computes the size of 6 """Prints the size of each given file and optionally computes the size of
7 libchrome.so without the dependencies added for building with android NDK. 7 libchrome.so without the dependencies added for building with android NDK.
8 Also breaks down the contents of the APK to determine the installed size 8 Also breaks down the contents of the APK to determine the installed size
9 and assign size contributions to different classes of file. 9 and assign size contributions to different classes of file.
10 """ 10 """
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after
104 'benchmark_name': 'resource_sizes', 104 'benchmark_name': 'resource_sizes',
105 'benchmark_description': 'APK resource size information.', 105 'benchmark_description': 'APK resource size information.',
106 'trace_rerun_options': [], 106 'trace_rerun_options': [],
107 'charts': {} 107 'charts': {}
108 } 108 }
109 _DUMP_STATIC_INITIALIZERS_PATH = os.path.join( 109 _DUMP_STATIC_INITIALIZERS_PATH = os.path.join(
110 host_paths.DIR_SOURCE_ROOT, 'tools', 'linux', 'dump-static-initializers.py') 110 host_paths.DIR_SOURCE_ROOT, 'tools', 'linux', 'dump-static-initializers.py')
111 # Pragma exists when enable_resource_whitelist_generation=true. 111 # Pragma exists when enable_resource_whitelist_generation=true.
112 _RC_HEADER_RE = re.compile( 112 _RC_HEADER_RE = re.compile(
113 r'^#define (?P<name>\w+) (?:_Pragma\(.*?\) )?(?P<id>\d+)$') 113 r'^#define (?P<name>\w+) (?:_Pragma\(.*?\) )?(?P<id>\d+)$')
114 _RE_NON_LANGUAGE_PAK = re.compile(r'^assets/.*(resources|percent)\.pak$')
115 _RE_COMPRESSED_LANGUAGE_PAK = re.compile(
116 r'\.lpak$|^assets/(?!stored-locales/).*(?!resources|percent)\.pak$')
117 _RE_STORED_LANGUAGE_PAK = re.compile(r'^assets/stored-locales/.*\.pak$')
114 _READELF_SIZES_METRICS = { 118 _READELF_SIZES_METRICS = {
115 'text': ['.text'], 119 'text': ['.text'],
116 'data': ['.data', '.rodata', '.data.rel.ro', '.data.rel.ro.local'], 120 'data': ['.data', '.rodata', '.data.rel.ro', '.data.rel.ro.local'],
117 'relocations': ['.rel.dyn', '.rel.plt', '.rela.dyn', '.rela.plt'], 121 'relocations': ['.rel.dyn', '.rel.plt', '.rela.dyn', '.rela.plt'],
118 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr',], 122 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr',],
119 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt', 123 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt',
120 '.got.plt', '.hash'], 124 '.got.plt', '.hash'],
121 'bss': ['.bss'], 125 'bss': ['.bss'],
122 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version', 126 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version',
123 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version', 127 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version',
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
158 return section_sizes 162 return section_sizes
159 163
160 164
161 def _ParseLibBuildId(so_path, tools_prefix): 165 def _ParseLibBuildId(so_path, tools_prefix):
162 """Returns the Build ID of the given native library.""" 166 """Returns the Build ID of the given native library."""
163 stdout = _RunReadelf(so_path, ['-n'], tools_prefix) 167 stdout = _RunReadelf(so_path, ['-n'], tools_prefix)
164 match = re.search(r'Build ID: (\w+)', stdout) 168 match = re.search(r'Build ID: (\w+)', stdout)
165 return match.group(1) if match else None 169 return match.group(1) if match else None
166 170
167 171
172 def _ParseManifestAttributes(apk_path):
173 # Check if the manifest specifies whether or not to extract native libs.
174 skip_extract_lib = False
175 output = cmd_helper.GetCmdOutput([
176 _AAPT_PATH.read(), 'd', 'xmltree', apk_path, 'AndroidManifest.xml'])
177 m = re.search(r'extractNativeLibs\(.*\)=\(.*\)(\w)', output)
178 if m:
179 skip_extract_lib = not bool(int(m.group(1)))
180
181 # Dex decompression overhead varies by Android version.
182 m = re.search(r'android:minSdkVersion\(\w+\)=\(type \w+\)(\w+)\n', output)
183 sdk_version = int(m.group(1), 16)
184 # Pre-L: Dalvik - .odex file is simply decompressed/optimized dex file (~1x).
185 # L, M: ART - .odex file is compiled version of the dex file (~3x).
186 # N: ART - Uses Dalvik-like JIT for normal apps (~1x), full compilation for
187 # shared apps (~3x).
188 if sdk_version < 21:
189 dex_multiplier = 1
190 elif sdk_version < 24:
191 dex_multiplier = 3
192 elif 'Monochrome' in apk_path or 'WebView' in apk_path:
193 dex_multiplier = 3
194 else:
195 dex_multiplier = 1
196
197 return dex_multiplier, skip_extract_lib
198
199
168 def CountStaticInitializers(so_path, tools_prefix): 200 def CountStaticInitializers(so_path, tools_prefix):
169 # Static initializers expected in official builds. Note that this list is 201 # Static initializers expected in official builds. Note that this list is
170 # built using 'nm' on libchrome.so which results from a GCC official build 202 # built using 'nm' on libchrome.so which results from a GCC official build
171 # (i.e. Clang is not supported currently). 203 # (i.e. Clang is not supported currently).
172 def get_elf_section_size(readelf_stdout, section_name): 204 def get_elf_section_size(readelf_stdout, section_name):
173 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8 205 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8
174 match = re.search(r'\.%s.*$' % re.escape(section_name), 206 match = re.search(r'\.%s.*$' % re.escape(section_name),
175 readelf_stdout, re.MULTILINE) 207 readelf_stdout, re.MULTILINE)
176 if not match: 208 if not match:
177 return (False, -1) 209 return (False, -1)
(...skipping 22 matching lines...) Expand all
200 return si_count 232 return si_count
201 233
202 234
203 def GetStaticInitializers(so_path, tools_prefix): 235 def GetStaticInitializers(so_path, tools_prefix):
204 output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d', 236 output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d',
205 so_path, '-t', tools_prefix]) 237 so_path, '-t', tools_prefix])
206 summary = re.search(r'Found \d+ static initializers in (\d+) files.', output) 238 summary = re.search(r'Found \d+ static initializers in (\d+) files.', output)
207 return output.splitlines()[:-1], int(summary.group(1)) 239 return output.splitlines()[:-1], int(summary.group(1))
208 240
209 241
242 def _NormalizeLanguagePaks(translations, normalized_apk_size, factor):
243 english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak')
244 num_translations = translations.GetNumEntries()
245 if english_pak:
246 normalized_apk_size -= translations.ComputeZippedSize()
247 normalized_apk_size += int(
248 english_pak.compress_size * num_translations * factor)
249 return normalized_apk_size
250
251
210 def _NormalizeResourcesArsc(apk_path): 252 def _NormalizeResourcesArsc(apk_path):
211 """Estimates the expected overhead of untranslated strings in resources.arsc. 253 """Estimates the expected overhead of untranslated strings in resources.arsc.
212 254
213 See http://crbug.com/677966 for why this is necessary. 255 See http://crbug.com/677966 for why this is necessary.
214 """ 256 """
215 aapt_output = _RunAaptDumpResources(apk_path) 257 aapt_output = _RunAaptDumpResources(apk_path)
216 258
217 # en-rUS is in the default config and may be cluttered with non-translatable 259 # en-rUS is in the default config and may be cluttered with non-translatable
218 # strings, so en-rGB is a better baseline for finding missing translations. 260 # strings, so en-rGB is a better baseline for finding missing translations.
219 en_strings = _CreateResourceIdValueMap(aapt_output, 'en-rGB') 261 en_strings = _CreateResourceIdValueMap(aapt_output, 'en-rGB')
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after
275 perf_tests_results_helper.PrintPerfResult( 317 perf_tests_results_helper.PrintPerfResult(
276 graph_title, trace_title, [value], units) 318 graph_title, trace_title, [value], units)
277 319
278 320
279 class _FileGroup(object): 321 class _FileGroup(object):
280 """Represents a category that apk files can fall into.""" 322 """Represents a category that apk files can fall into."""
281 323
282 def __init__(self, name): 324 def __init__(self, name):
283 self.name = name 325 self.name = name
284 self._zip_infos = [] 326 self._zip_infos = []
285 self._extracted = [] 327 self._extracted_multipliers = []
286 328
287 def AddZipInfo(self, zip_info, extracted=False): 329 def AddZipInfo(self, zip_info, extracted_multiplier=0):
288 self._zip_infos.append(zip_info) 330 self._zip_infos.append(zip_info)
289 self._extracted.append(extracted) 331 self._extracted_multipliers.append(extracted_multiplier)
290 332
291 def AllEntries(self): 333 def AllEntries(self):
292 return iter(self._zip_infos) 334 return iter(self._zip_infos)
293 335
294 def GetNumEntries(self): 336 def GetNumEntries(self):
295 return len(self._zip_infos) 337 return len(self._zip_infos)
296 338
297 def FindByPattern(self, pattern): 339 def FindByPattern(self, pattern):
298 return next((i for i in self._zip_infos if re.match(pattern, i.filename)), 340 return next((i for i in self._zip_infos if re.match(pattern, i.filename)),
299 None) 341 None)
300 342
301 def FindLargest(self): 343 def FindLargest(self):
302 if not self._zip_infos: 344 if not self._zip_infos:
303 return None 345 return None
304 return max(self._zip_infos, key=lambda i: i.file_size) 346 return max(self._zip_infos, key=lambda i: i.file_size)
305 347
306 def ComputeZippedSize(self): 348 def ComputeZippedSize(self):
307 return sum(i.compress_size for i in self._zip_infos) 349 return sum(i.compress_size for i in self._zip_infos)
308 350
309 def ComputeUncompressedSize(self): 351 def ComputeUncompressedSize(self):
310 return sum(i.file_size for i in self._zip_infos) 352 return sum(i.file_size for i in self._zip_infos)
311 353
312 def ComputeExtractedSize(self): 354 def ComputeExtractedSize(self):
313 ret = 0 355 ret = 0
314 for zi, extracted in zip(self._zip_infos, self._extracted): 356 for zi, multiplier in zip(self._zip_infos, self._extracted_multipliers):
315 if extracted: 357 ret += zi.file_size * multiplier
316 ret += zi.file_size
317 return ret 358 return ret
318 359
319 def ComputeInstallSize(self): 360 def ComputeInstallSize(self):
320 return self.ComputeExtractedSize() + self.ComputeZippedSize() 361 return self.ComputeExtractedSize() + self.ComputeZippedSize()
321 362
322 363
323 def PrintApkAnalysis(apk_filename, tools_prefix, chartjson=None): 364 def PrintApkAnalysis(apk_filename, tools_prefix, chartjson=None):
324 """Analyse APK to determine size contributions of different file classes.""" 365 """Analyse APK to determine size contributions of different file classes."""
325 file_groups = [] 366 file_groups = []
326 367
327 def make_group(name): 368 def make_group(name):
328 group = _FileGroup(name) 369 group = _FileGroup(name)
329 file_groups.append(group) 370 file_groups.append(group)
330 return group 371 return group
331 372
332 native_code = make_group('Native code') 373 native_code = make_group('Native code')
333 java_code = make_group('Java code') 374 java_code = make_group('Java code')
334 native_resources_no_translations = make_group('Native resources (no l10n)') 375 native_resources_no_translations = make_group('Native resources (no l10n)')
335 translations = make_group('Native resources (l10n)') 376 translations = make_group('Native resources (l10n)')
377 stored_translations = make_group('Native resources stored (l10n)')
336 icu_data = make_group('ICU (i18n library) data') 378 icu_data = make_group('ICU (i18n library) data')
337 v8_snapshots = make_group('V8 Snapshots') 379 v8_snapshots = make_group('V8 Snapshots')
338 png_drawables = make_group('PNG drawables') 380 png_drawables = make_group('PNG drawables')
339 res_directory = make_group('Non-compiled Android resources') 381 res_directory = make_group('Non-compiled Android resources')
340 arsc = make_group('Compiled Android resources') 382 arsc = make_group('Compiled Android resources')
341 metadata = make_group('Package metadata') 383 metadata = make_group('Package metadata')
342 unknown = make_group('Unknown files') 384 unknown = make_group('Unknown files')
343 notices = make_group('licenses.notice file') 385 notices = make_group('licenses.notice file')
344 386
345 apk = zipfile.ZipFile(apk_filename, 'r') 387 apk = zipfile.ZipFile(apk_filename, 'r')
346 try: 388 try:
347 apk_contents = apk.infolist() 389 apk_contents = apk.infolist()
348 finally: 390 finally:
349 apk.close() 391 apk.close()
350 392
393 dex_multiplier, skip_extract_lib = _ParseManifestAttributes(apk_filename)
351 total_apk_size = os.path.getsize(apk_filename) 394 total_apk_size = os.path.getsize(apk_filename)
352 apk_basename = os.path.basename(apk_filename) 395 apk_basename = os.path.basename(apk_filename)
353
354 for member in apk_contents: 396 for member in apk_contents:
355 filename = member.filename 397 filename = member.filename
356 if filename.endswith('/'): 398 if filename.endswith('/'):
357 continue 399 continue
358
359 if filename.endswith('.so'): 400 if filename.endswith('.so'):
360 native_code.AddZipInfo(member, 'crazy' not in filename) 401 should_extract_lib = not (skip_extract_lib or 'crazy' in filename)
402 native_code.AddZipInfo(
403 member, extracted_multiplier=int(should_extract_lib))
361 elif filename.endswith('.dex'): 404 elif filename.endswith('.dex'):
362 java_code.AddZipInfo(member, True) 405 java_code.AddZipInfo(member, extracted_multiplier=dex_multiplier)
363 elif re.search(r'^assets/.*(resources|percent)\.pak$', filename): 406 elif re.search(_RE_NON_LANGUAGE_PAK, filename):
364 native_resources_no_translations.AddZipInfo(member) 407 native_resources_no_translations.AddZipInfo(member)
365 elif re.search(r'\.lpak$|^assets/.*(?!resources|percent)\.pak$', filename): 408 elif re.search(_RE_COMPRESSED_LANGUAGE_PAK, filename):
366 translations.AddZipInfo(member, 'en_' in filename or 'en-' in filename) 409 translations.AddZipInfo(
410 member,
411 extracted_multiplier=int('en_' in filename or 'en-' in filename))
412 elif re.search(_RE_STORED_LANGUAGE_PAK, filename):
413 stored_translations.AddZipInfo(member)
367 elif filename == 'assets/icudtl.dat': 414 elif filename == 'assets/icudtl.dat':
368 icu_data.AddZipInfo(member) 415 icu_data.AddZipInfo(member)
369 elif filename.endswith('.bin'): 416 elif filename.endswith('.bin'):
370 v8_snapshots.AddZipInfo(member) 417 v8_snapshots.AddZipInfo(member)
371 elif filename.endswith('.png') or filename.endswith('.webp'): 418 elif filename.endswith('.png') or filename.endswith('.webp'):
372 png_drawables.AddZipInfo(member) 419 png_drawables.AddZipInfo(member)
373 elif filename.startswith('res/'): 420 elif filename.startswith('res/'):
374 res_directory.AddZipInfo(member) 421 res_directory.AddZipInfo(member)
375 elif filename.endswith('.arsc'): 422 elif filename.endswith('.arsc'):
376 arsc.AddZipInfo(member) 423 arsc.AddZipInfo(member)
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after
431 478
432 # Main metric that we want to monitor for jumps. 479 # Main metric that we want to monitor for jumps.
433 normalized_apk_size = total_apk_size 480 normalized_apk_size = total_apk_size
434 # Always look at uncompressed .dex & .so. 481 # Always look at uncompressed .dex & .so.
435 normalized_apk_size -= java_code.ComputeZippedSize() 482 normalized_apk_size -= java_code.ComputeZippedSize()
436 normalized_apk_size += java_code.ComputeUncompressedSize() 483 normalized_apk_size += java_code.ComputeUncompressedSize()
437 normalized_apk_size -= native_code.ComputeZippedSize() 484 normalized_apk_size -= native_code.ComputeZippedSize()
438 normalized_apk_size += native_code.ComputeUncompressedSize() 485 normalized_apk_size += native_code.ComputeUncompressedSize()
439 # Avoid noise caused when strings change and translations haven't yet been 486 # Avoid noise caused when strings change and translations haven't yet been
440 # updated. 487 # updated.
441 english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak')
442 num_translations = translations.GetNumEntries() 488 num_translations = translations.GetNumEntries()
443 if english_pak and num_translations > 1: 489 if num_translations > 1:
444 normalized_apk_size -= translations.ComputeZippedSize() 490 # Multipliers found by looking at MonochromePublic.apk and seeing how much
445 # 1.17 found by looking at Chrome.apk and seeing how much smaller en-US.pak 491 # smaller en-US.pak is relative to the average locale.pak.
446 # is relative to the average locale .pak. 492 normalized_apk_size = _NormalizeLanguagePaks(
447 normalized_apk_size += int( 493 translations, normalized_apk_size, 1.17)
448 english_pak.compress_size * num_translations * 1.17) 494 normalized_apk_size = _NormalizeLanguagePaks(
495 stored_translations, normalized_apk_size, 1.43)
449 normalized_apk_size += int(_NormalizeResourcesArsc(apk_filename)) 496 normalized_apk_size += int(_NormalizeResourcesArsc(apk_filename))
450 497
451 ReportPerfResult(chartjson, apk_basename + '_Specifics', 498 ReportPerfResult(chartjson, apk_basename + '_Specifics',
452 'normalized apk size', normalized_apk_size, 'bytes') 499 'normalized apk size', normalized_apk_size, 'bytes')
453 500
454 ReportPerfResult(chartjson, apk_basename + '_Specifics', 501 ReportPerfResult(chartjson, apk_basename + '_Specifics',
455 'file count', len(apk_contents), 'zip entries') 502 'file count', len(apk_contents), 'zip entries')
456 503
457 for info in unknown.AllEntries(): 504 for info in unknown.AllEntries():
458 print 'Unknown entry:', info.filename, info.compress_size 505 print 'Unknown entry:', info.filename, info.compress_size
(...skipping 296 matching lines...) Expand 10 before | Expand all | Expand 10 after
755 default=apk_downloader.DEFAULT_BUILDER, 802 default=apk_downloader.DEFAULT_BUILDER,
756 help='Builder name to use for reference APK for patch ' 803 help='Builder name to use for reference APK for patch '
757 'size estimates.') 804 'size estimates.')
758 argparser.add_argument('--reference-apk-bucket', 805 argparser.add_argument('--reference-apk-bucket',
759 default=apk_downloader.DEFAULT_BUCKET, 806 default=apk_downloader.DEFAULT_BUCKET,
760 help='Storage bucket holding reference APKs.') 807 help='Storage bucket holding reference APKs.')
761 argparser.add_argument('apk', help='APK file path.') 808 argparser.add_argument('apk', help='APK file path.')
762 args = argparser.parse_args() 809 args = argparser.parse_args()
763 810
764 chartjson = _BASE_CHART.copy() if args.chartjson else None 811 chartjson = _BASE_CHART.copy() if args.chartjson else None
765
766 if args.chromium_output_directory: 812 if args.chromium_output_directory:
767 constants.SetOutputDirectory(args.chromium_output_directory) 813 constants.SetOutputDirectory(args.chromium_output_directory)
768 if not args.no_output_dir: 814 if not args.no_output_dir:
769 constants.CheckOutputDirectory() 815 constants.CheckOutputDirectory()
770 devil_chromium.Initialize() 816 devil_chromium.Initialize()
771 build_vars = _ReadBuildVars(constants.GetOutDirectory()) 817 build_vars = _ReadBuildVars(constants.GetOutDirectory())
772 tools_prefix = os.path.join(constants.GetOutDirectory(), 818 tools_prefix = os.path.join(constants.GetOutDirectory(),
773 build_vars['android_tool_prefix']) 819 build_vars['android_tool_prefix'])
774 else: 820 else:
775 tools_prefix = '' 821 tools_prefix = ''
(...skipping 10 matching lines...) Expand all
786 chartjson=chartjson) 832 chartjson=chartjson)
787 if chartjson: 833 if chartjson:
788 results_path = os.path.join(args.output_dir, 'results-chart.json') 834 results_path = os.path.join(args.output_dir, 'results-chart.json')
789 logging.critical('Dumping json to %s', results_path) 835 logging.critical('Dumping json to %s', results_path)
790 with open(results_path, 'w') as json_file: 836 with open(results_path, 'w') as json_file:
791 json.dump(chartjson, json_file) 837 json.dump(chartjson, json_file)
792 838
793 839
794 if __name__ == '__main__': 840 if __name__ == '__main__':
795 sys.exit(main()) 841 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698