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

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

Issue 2930313002: resource_sizes.py: Better pakfile and dex normalization. (Closed)
Patch Set: 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_LANGUAGE_PAK = re.compile(
agrieve 2017/06/14 14:11:12 nit: RE_LANGUAGE_PAK -> RE_COMPRESSED_LANGUAGE_PAK
estevenson 2017/06/15 02:56:44 Done.
116 r'\.lpak$|^assets/(?!stored-locales/).*(?!resources|percent)\.pak$')
117 _RE_STORED_LANGUAGE_PAK = re.compile(
118 r'\.lpak$|^assets/stored-locales/.*(?!resources|percent)\.pak$')
agrieve 2017/06/14 14:11:13 nit: can remove .lpak from this list. .lpak is his
estevenson 2017/06/15 02:56:43 Done.
114 _READELF_SIZES_METRICS = { 119 _READELF_SIZES_METRICS = {
115 'text': ['.text'], 120 'text': ['.text'],
116 'data': ['.data', '.rodata', '.data.rel.ro', '.data.rel.ro.local'], 121 'data': ['.data', '.rodata', '.data.rel.ro', '.data.rel.ro.local'],
117 'relocations': ['.rel.dyn', '.rel.plt', '.rela.dyn', '.rela.plt'], 122 'relocations': ['.rel.dyn', '.rel.plt', '.rela.dyn', '.rela.plt'],
118 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr',], 123 'unwind': ['.ARM.extab', '.ARM.exidx', '.eh_frame', '.eh_frame_hdr',],
119 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt', 124 'symbols': ['.dynsym', '.dynstr', '.dynamic', '.shstrtab', '.got', '.plt',
120 '.got.plt', '.hash'], 125 '.got.plt', '.hash'],
121 'bss': ['.bss'], 126 'bss': ['.bss'],
122 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version', 127 'other': ['.init_array', '.fini_array', '.comment', '.note.gnu.gold-version',
123 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version', 128 '.ARM.attributes', '.note.gnu.build-id', '.gnu.version',
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
158 return section_sizes 163 return section_sizes
159 164
160 165
161 def _ParseLibBuildId(so_path, tools_prefix): 166 def _ParseLibBuildId(so_path, tools_prefix):
162 """Returns the Build ID of the given native library.""" 167 """Returns the Build ID of the given native library."""
163 stdout = _RunReadelf(so_path, ['-n'], tools_prefix) 168 stdout = _RunReadelf(so_path, ['-n'], tools_prefix)
164 match = re.search(r'Build ID: (\w+)', stdout) 169 match = re.search(r'Build ID: (\w+)', stdout)
165 return match.group(1) if match else None 170 return match.group(1) if match else None
166 171
167 172
173 def _ParseManifestAttributes(apk_path):
174 # Check if the manifest specifies whether or not to extract native libs.
175 skip_extract_lib = False
176 output = cmd_helper.GetCmdOutput([
177 'aapt', 'd', 'xmltree', apk_path, 'AndroidManifest.xml'])
agrieve 2017/06/14 14:11:12 I don't think we can assume aapt will be in the PA
estevenson 2017/06/15 02:56:43 Forgot that we already have AAPT_PATH via devil.an
178 m = re.search(r'extractNativeLibs\(.*\)=\(.*\)(\w)', output)
179 if m:
180 skip_extract_lib = not bool(int(m.group(1)))
181
182 # Dex decompression overhead varies by Android version.
183 output = cmd_helper.GetCmdOutput(['aapt', 'd', 'badging', apk_path])
agrieve 2017/06/14 14:11:12 nit: no need to run both badging and xmltree. The
estevenson 2017/06/15 02:56:43 Ahh missed that. Done!
184 sdk_version = int(re.search(r'sdkVersion:\'(\d+)\'', output).group(1))
185 if sdk_version < 21:
186 dex_multiplier = 1
187 elif sdk_version < 24:
188 dex_multiplier = 3
189 elif 'monochrome' in apk_path.lower(): # Extracted for WebView and Chrome.
agrieve 2017/06/14 14:11:13 Worth a comment saying why this is the case.
estevenson 2017/06/15 02:56:44 Done. Can you confirm that it should be a multipli
190 dex_multiplier = 3
estevenson 2017/06/12 16:56:28 Should this be 2?
191 else:
192 dex_multiplier = 1
193
194 return dex_multiplier, skip_extract_lib
195
196
168 def CountStaticInitializers(so_path, tools_prefix): 197 def CountStaticInitializers(so_path, tools_prefix):
169 # Static initializers expected in official builds. Note that this list is 198 # 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 199 # built using 'nm' on libchrome.so which results from a GCC official build
171 # (i.e. Clang is not supported currently). 200 # (i.e. Clang is not supported currently).
172 def get_elf_section_size(readelf_stdout, section_name): 201 def get_elf_section_size(readelf_stdout, section_name):
173 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8 202 # Matches: .ctors PROGBITS 000000000516add0 5169dd0 000010 00 WA 0 0 8
174 match = re.search(r'\.%s.*$' % re.escape(section_name), 203 match = re.search(r'\.%s.*$' % re.escape(section_name),
175 readelf_stdout, re.MULTILINE) 204 readelf_stdout, re.MULTILINE)
176 if not match: 205 if not match:
177 return (False, -1) 206 return (False, -1)
(...skipping 22 matching lines...) Expand all
200 return si_count 229 return si_count
201 230
202 231
203 def GetStaticInitializers(so_path, tools_prefix): 232 def GetStaticInitializers(so_path, tools_prefix):
204 output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d', 233 output = cmd_helper.GetCmdOutput([_DUMP_STATIC_INITIALIZERS_PATH, '-d',
205 so_path, '-t', tools_prefix]) 234 so_path, '-t', tools_prefix])
206 summary = re.search(r'Found \d+ static initializers in (\d+) files.', output) 235 summary = re.search(r'Found \d+ static initializers in (\d+) files.', output)
207 return output.splitlines()[:-1], int(summary.group(1)) 236 return output.splitlines()[:-1], int(summary.group(1))
208 237
209 238
239 def _NormalizeLanguagePaks(translations, normalized_apk_size, factor):
240 english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak')
241 num_translations = translations.GetNumEntries()
242 if english_pak:
243 normalized_apk_size -= translations.ComputeZippedSize()
244 normalized_apk_size += int(
245 english_pak.compress_size * num_translations * factor)
246 return normalized_apk_size
247
248
210 def _NormalizeResourcesArsc(apk_path): 249 def _NormalizeResourcesArsc(apk_path):
211 """Estimates the expected overhead of untranslated strings in resources.arsc. 250 """Estimates the expected overhead of untranslated strings in resources.arsc.
212 251
213 See http://crbug.com/677966 for why this is necessary. 252 See http://crbug.com/677966 for why this is necessary.
214 """ 253 """
215 aapt_output = _RunAaptDumpResources(apk_path) 254 aapt_output = _RunAaptDumpResources(apk_path)
216 255
217 # en-rUS is in the default config and may be cluttered with non-translatable 256 # 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. 257 # strings, so en-rGB is a better baseline for finding missing translations.
219 en_strings = _CreateResourceIdValueMap(aapt_output, 'en-rGB') 258 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( 314 perf_tests_results_helper.PrintPerfResult(
276 graph_title, trace_title, [value], units) 315 graph_title, trace_title, [value], units)
277 316
278 317
279 class _FileGroup(object): 318 class _FileGroup(object):
280 """Represents a category that apk files can fall into.""" 319 """Represents a category that apk files can fall into."""
281 320
282 def __init__(self, name): 321 def __init__(self, name):
283 self.name = name 322 self.name = name
284 self._zip_infos = [] 323 self._zip_infos = []
285 self._extracted = [] 324 self._extracted_multipliers = []
286 325
287 def AddZipInfo(self, zip_info, extracted=False): 326 def AddZipInfo(self, zip_info, extracted_multiplier=None):
agrieve 2017/06/14 14:11:13 nit: maybe make the default 0, so that you don't h
estevenson 2017/06/15 02:56:44 Done.
288 self._zip_infos.append(zip_info) 327 self._zip_infos.append(zip_info)
289 self._extracted.append(extracted) 328 self._extracted_multipliers.append(extracted_multiplier)
290 329
291 def AllEntries(self): 330 def AllEntries(self):
292 return iter(self._zip_infos) 331 return iter(self._zip_infos)
293 332
294 def GetNumEntries(self): 333 def GetNumEntries(self):
295 return len(self._zip_infos) 334 return len(self._zip_infos)
296 335
297 def FindByPattern(self, pattern): 336 def FindByPattern(self, pattern):
298 return next((i for i in self._zip_infos if re.match(pattern, i.filename)), 337 return next((i for i in self._zip_infos if re.match(pattern, i.filename)),
299 None) 338 None)
300 339
301 def FindLargest(self): 340 def FindLargest(self):
302 if not self._zip_infos: 341 if not self._zip_infos:
303 return None 342 return None
304 return max(self._zip_infos, key=lambda i: i.file_size) 343 return max(self._zip_infos, key=lambda i: i.file_size)
305 344
306 def ComputeZippedSize(self): 345 def ComputeZippedSize(self):
307 return sum(i.compress_size for i in self._zip_infos) 346 return sum(i.compress_size for i in self._zip_infos)
308 347
309 def ComputeUncompressedSize(self): 348 def ComputeUncompressedSize(self):
310 return sum(i.file_size for i in self._zip_infos) 349 return sum(i.file_size for i in self._zip_infos)
311 350
312 def ComputeExtractedSize(self): 351 def ComputeExtractedSize(self):
313 ret = 0 352 ret = 0
314 for zi, extracted in zip(self._zip_infos, self._extracted): 353 for zi, multiplier in zip(self._zip_infos, self._extracted_multipliers):
315 if extracted: 354 if multiplier:
316 ret += zi.file_size 355 ret += zi.file_size * int(multiplier)
317 return ret 356 return ret
318 357
319 def ComputeInstallSize(self): 358 def ComputeInstallSize(self):
320 return self.ComputeExtractedSize() + self.ComputeZippedSize() 359 return self.ComputeExtractedSize() + self.ComputeZippedSize()
321 360
322 361
323 def PrintApkAnalysis(apk_filename, tools_prefix, chartjson=None): 362 def PrintApkAnalysis(apk_filename, tools_prefix, chartjson=None):
324 """Analyse APK to determine size contributions of different file classes.""" 363 """Analyse APK to determine size contributions of different file classes."""
325 file_groups = [] 364 file_groups = []
326 365
327 def make_group(name): 366 def make_group(name):
328 group = _FileGroup(name) 367 group = _FileGroup(name)
329 file_groups.append(group) 368 file_groups.append(group)
330 return group 369 return group
331 370
332 native_code = make_group('Native code') 371 native_code = make_group('Native code')
333 java_code = make_group('Java code') 372 java_code = make_group('Java code')
334 native_resources_no_translations = make_group('Native resources (no l10n)') 373 native_resources_no_translations = make_group('Native resources (no l10n)')
335 translations = make_group('Native resources (l10n)') 374 translations = make_group('Native resources (l10n)')
375 stored_translations = make_group('Native resources stored (l10n)')
336 icu_data = make_group('ICU (i18n library) data') 376 icu_data = make_group('ICU (i18n library) data')
337 v8_snapshots = make_group('V8 Snapshots') 377 v8_snapshots = make_group('V8 Snapshots')
338 png_drawables = make_group('PNG drawables') 378 png_drawables = make_group('PNG drawables')
339 res_directory = make_group('Non-compiled Android resources') 379 res_directory = make_group('Non-compiled Android resources')
340 arsc = make_group('Compiled Android resources') 380 arsc = make_group('Compiled Android resources')
341 metadata = make_group('Package metadata') 381 metadata = make_group('Package metadata')
342 unknown = make_group('Unknown files') 382 unknown = make_group('Unknown files')
343 notices = make_group('licenses.notice file') 383 notices = make_group('licenses.notice file')
344 384
345 apk = zipfile.ZipFile(apk_filename, 'r') 385 apk = zipfile.ZipFile(apk_filename, 'r')
346 try: 386 try:
347 apk_contents = apk.infolist() 387 apk_contents = apk.infolist()
348 finally: 388 finally:
349 apk.close() 389 apk.close()
350 390
391 dex_multiplier, skip_extract_lib = _ParseManifestAttributes(apk_filename)
351 total_apk_size = os.path.getsize(apk_filename) 392 total_apk_size = os.path.getsize(apk_filename)
352 apk_basename = os.path.basename(apk_filename) 393 apk_basename = os.path.basename(apk_filename)
353
354 for member in apk_contents: 394 for member in apk_contents:
355 filename = member.filename 395 filename = member.filename
356 if filename.endswith('/'): 396 if filename.endswith('/'):
357 continue 397 continue
358
359 if filename.endswith('.so'): 398 if filename.endswith('.so'):
360 native_code.AddZipInfo(member, 'crazy' not in filename) 399 should_extract_lib = not (skip_extract_lib or 'crazy' in filename)
400 native_code.AddZipInfo(member, should_extract_lib)
agrieve 2017/06/14 14:11:12 nit: change to int(should_extract_lib)
estevenson 2017/06/15 02:56:43 Done.
361 elif filename.endswith('.dex'): 401 elif filename.endswith('.dex'):
362 java_code.AddZipInfo(member, True) 402 java_code.AddZipInfo(member, dex_multiplier)
363 elif re.search(r'^assets/.*(resources|percent)\.pak$', filename): 403 elif re.search(_RE_NON_LANGUAGE_PAK, filename):
364 native_resources_no_translations.AddZipInfo(member) 404 native_resources_no_translations.AddZipInfo(member)
365 elif re.search(r'\.lpak$|^assets/.*(?!resources|percent)\.pak$', filename): 405 elif re.search(_RE_LANGUAGE_PAK, filename):
366 translations.AddZipInfo(member, 'en_' in filename or 'en-' in filename) 406 translations.AddZipInfo(member, 'en_' in filename or 'en-' in filename)
agrieve 2017/06/14 14:11:13 nit: add int() to 2nd param
estevenson 2017/06/15 02:56:44 Done.
407 elif re.search(_RE_STORED_LANGUAGE_PAK, filename):
408 stored_translations.AddZipInfo(
409 member, 'en_' in filename or 'en-' in filename)
agrieve 2017/06/14 14:11:13 I think 2nd param here should always be 0 (never e
estevenson 2017/06/15 02:56:43 Definitely. Done.
367 elif filename == 'assets/icudtl.dat': 410 elif filename == 'assets/icudtl.dat':
368 icu_data.AddZipInfo(member) 411 icu_data.AddZipInfo(member)
369 elif filename.endswith('.bin'): 412 elif filename.endswith('.bin'):
370 v8_snapshots.AddZipInfo(member) 413 v8_snapshots.AddZipInfo(member)
371 elif filename.endswith('.png') or filename.endswith('.webp'): 414 elif filename.endswith('.png') or filename.endswith('.webp'):
372 png_drawables.AddZipInfo(member) 415 png_drawables.AddZipInfo(member)
373 elif filename.startswith('res/'): 416 elif filename.startswith('res/'):
374 res_directory.AddZipInfo(member) 417 res_directory.AddZipInfo(member)
375 elif filename.endswith('.arsc'): 418 elif filename.endswith('.arsc'):
376 arsc.AddZipInfo(member) 419 arsc.AddZipInfo(member)
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after
431 474
432 # Main metric that we want to monitor for jumps. 475 # Main metric that we want to monitor for jumps.
433 normalized_apk_size = total_apk_size 476 normalized_apk_size = total_apk_size
434 # Always look at uncompressed .dex & .so. 477 # Always look at uncompressed .dex & .so.
435 normalized_apk_size -= java_code.ComputeZippedSize() 478 normalized_apk_size -= java_code.ComputeZippedSize()
436 normalized_apk_size += java_code.ComputeUncompressedSize() 479 normalized_apk_size += java_code.ComputeUncompressedSize()
437 normalized_apk_size -= native_code.ComputeZippedSize() 480 normalized_apk_size -= native_code.ComputeZippedSize()
438 normalized_apk_size += native_code.ComputeUncompressedSize() 481 normalized_apk_size += native_code.ComputeUncompressedSize()
439 # Avoid noise caused when strings change and translations haven't yet been 482 # Avoid noise caused when strings change and translations haven't yet been
440 # updated. 483 # updated.
441 english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak')
442 num_translations = translations.GetNumEntries() 484 num_translations = translations.GetNumEntries()
443 if english_pak and num_translations > 1: 485 if num_translations > 1:
444 normalized_apk_size -= translations.ComputeZippedSize() 486 normalized_apk_size = _NormalizeLanguagePaks(
445 # 1.17 found by looking at Chrome.apk and seeing how much smaller en-US.pak 487 translations, normalized_apk_size, 1.17)
agrieve 2017/06/14 14:11:13 can we keep this comment?
estevenson 2017/06/15 02:56:44 Done.
446 # is relative to the average locale .pak. 488 normalized_apk_size = _NormalizeLanguagePaks(
447 normalized_apk_size += int( 489 stored_translations, normalized_apk_size, 1.43)
448 english_pak.compress_size * num_translations * 1.17)
449 normalized_apk_size += int(_NormalizeResourcesArsc(apk_filename)) 490 normalized_apk_size += int(_NormalizeResourcesArsc(apk_filename))
450 491
451 ReportPerfResult(chartjson, apk_basename + '_Specifics', 492 ReportPerfResult(chartjson, apk_basename + '_Specifics',
452 'normalized apk size', normalized_apk_size, 'bytes') 493 'normalized apk size', normalized_apk_size, 'bytes')
453 494
454 ReportPerfResult(chartjson, apk_basename + '_Specifics', 495 ReportPerfResult(chartjson, apk_basename + '_Specifics',
455 'file count', len(apk_contents), 'zip entries') 496 'file count', len(apk_contents), 'zip entries')
456 497
457 for info in unknown.AllEntries(): 498 for info in unknown.AllEntries():
458 print 'Unknown entry:', info.filename, info.compress_size 499 print 'Unknown entry:', info.filename, info.compress_size
(...skipping 327 matching lines...) Expand 10 before | Expand all | Expand 10 after
786 chartjson=chartjson) 827 chartjson=chartjson)
787 if chartjson: 828 if chartjson:
788 results_path = os.path.join(args.output_dir, 'results-chart.json') 829 results_path = os.path.join(args.output_dir, 'results-chart.json')
789 logging.critical('Dumping json to %s', results_path) 830 logging.critical('Dumping json to %s', results_path)
790 with open(results_path, 'w') as json_file: 831 with open(results_path, 'w') as json_file:
791 json.dump(chartjson, json_file) 832 json.dump(chartjson, json_file)
792 833
793 834
794 if __name__ == '__main__': 835 if __name__ == '__main__':
795 sys.exit(main()) 836 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