| OLD | NEW |
| 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 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 161 """Prints the sizes of each given file. | 161 """Prints the sizes of each given file. |
| 162 | 162 |
| 163 Args: | 163 Args: |
| 164 files: List of files to print sizes for. | 164 files: List of files to print sizes for. |
| 165 """ | 165 """ |
| 166 for f in files: | 166 for f in files: |
| 167 ReportPerfResult(chartjson, 'ResourceSizes', os.path.basename(f) + ' size', | 167 ReportPerfResult(chartjson, 'ResourceSizes', os.path.basename(f) + ' size', |
| 168 os.path.getsize(f), 'bytes') | 168 os.path.getsize(f), 'bytes') |
| 169 | 169 |
| 170 | 170 |
| 171 class _FileGroup(object): |
| 172 """Represents a category that apk files can fall into.""" |
| 173 |
| 174 def __init__(self, name): |
| 175 self.name = name |
| 176 self._zip_infos = [] |
| 177 self._extracted = [] |
| 178 |
| 179 def AddZipInfo(self, zip_info, extracted=False): |
| 180 self._zip_infos.append(zip_info) |
| 181 self._extracted.append(extracted) |
| 182 |
| 183 def GetNumEntries(self): |
| 184 return len(self._zip_infos) |
| 185 |
| 186 def FindByPattern(self, pattern): |
| 187 return next(i for i in self._zip_infos if re.match(pattern, i.filename)) |
| 188 |
| 189 def FindLargest(self): |
| 190 return max(self._zip_infos, key=lambda i: i.file_size) |
| 191 |
| 192 def ComputeZippedSize(self): |
| 193 return sum(i.compress_size for i in self._zip_infos) |
| 194 |
| 195 def ComputeUncompressedSize(self): |
| 196 return sum(i.file_size for i in self._zip_infos) |
| 197 |
| 198 def ComputeExtractedSize(self): |
| 199 ret = 0 |
| 200 for zi, extracted in zip(self._zip_infos, self._extracted): |
| 201 if extracted: |
| 202 ret += zi.file_size |
| 203 return ret |
| 204 |
| 205 def ComputeInstallSize(self): |
| 206 return self.ComputeExtractedSize() + self.ComputeZippedSize() |
| 207 |
| 208 |
| 171 def PrintApkAnalysis(apk_filename, chartjson=None): | 209 def PrintApkAnalysis(apk_filename, chartjson=None): |
| 172 """Analyse APK to determine size contributions of different file classes.""" | 210 """Analyse APK to determine size contributions of different file classes.""" |
| 173 # Define a named tuple type for file grouping. | 211 file_groups = [] |
| 174 # name: Human readable name for this file group | |
| 175 # regex: Regular expression to match filename | |
| 176 # extracted: Function that takes a file name and returns whether the file is | |
| 177 # extracted from the apk at install/runtime. | |
| 178 FileGroup = collections.namedtuple('FileGroup', | |
| 179 ['name', 'regex', 'extracted']) | |
| 180 | 212 |
| 181 # File groups are checked in sequence, so more specific regexes should be | 213 def make_group(name): |
| 182 # earlier in the list. | 214 group = _FileGroup(name) |
| 183 YES = lambda _: True | 215 file_groups.append(group) |
| 184 NO = lambda _: False | 216 return group |
| 185 FILE_GROUPS = ( | 217 |
| 186 FileGroup('Native code', r'\.so$', lambda f: 'crazy' not in f), | 218 native_code = make_group('Native code') |
| 187 FileGroup('Java code', r'\.dex$', YES), | 219 java_code = make_group('Java code') |
| 188 FileGroup('Native resources (no l10n)', | 220 native_resources_no_translations = make_group('Native resources (no l10n)') |
| 189 r'^assets/.*(resources|percent)\.pak$', NO), | 221 translations = make_group('Native resources (l10n)') |
| 190 # For locale paks, assume only english paks are extracted. | 222 icu_data = make_group('ICU (i18n library) data') |
| 191 # Handles locale paks as bother resources or assets (.lpak or .pak). | 223 v8_snapshots = make_group('V8 Snapshots') |
| 192 FileGroup('Native resources (l10n)', | 224 png_drawables = make_group('PNG drawables') |
| 193 r'\.lpak$|^assets/.*(?!resources|percent)\.pak$', | 225 res_directory = make_group('Non-compiled Android resources') |
| 194 lambda f: 'en_' in f or 'en-' in f), | 226 arsc = make_group('Compiled Android resources') |
| 195 FileGroup('ICU (i18n library) data', r'^assets/icudtl\.dat$', NO), | 227 metadata = make_group('Package metadata') |
| 196 FileGroup('V8 Snapshots', r'^assets/.*\.bin$', NO), | 228 unknown = make_group('Unknown files') |
| 197 FileGroup('PNG drawables', r'\.png$', NO), | |
| 198 FileGroup('Non-compiled Android resources', r'^res/', NO), | |
| 199 FileGroup('Compiled Android resources', r'\.arsc$', NO), | |
| 200 FileGroup('Package metadata', r'^(META-INF/|AndroidManifest\.xml$)', NO), | |
| 201 FileGroup('Unknown files', r'.', NO), | |
| 202 ) | |
| 203 | 229 |
| 204 apk = zipfile.ZipFile(apk_filename, 'r') | 230 apk = zipfile.ZipFile(apk_filename, 'r') |
| 205 try: | 231 try: |
| 206 apk_contents = apk.infolist() | 232 apk_contents = apk.infolist() |
| 207 finally: | 233 finally: |
| 208 apk.close() | 234 apk.close() |
| 209 | 235 |
| 210 total_apk_size = os.path.getsize(apk_filename) | 236 total_apk_size = os.path.getsize(apk_filename) |
| 211 apk_basename = os.path.basename(apk_filename) | 237 apk_basename = os.path.basename(apk_filename) |
| 212 | 238 |
| 213 found_files = {} | 239 for member in apk_contents: |
| 214 for group in FILE_GROUPS: | 240 filename = member.filename |
| 215 found_files[group] = [] | 241 if filename.endswith('/'): |
| 242 continue |
| 216 | 243 |
| 217 for member in apk_contents: | 244 if filename.endswith('.so'): |
| 218 for group in FILE_GROUPS: | 245 native_code.AddZipInfo(member, 'crazy' not in filename) |
| 219 if re.search(group.regex, member.filename): | 246 elif filename.endswith('.dex'): |
| 220 found_files[group].append(member) | 247 java_code.AddZipInfo(member, True) |
| 221 break | 248 elif re.search(r'^assets/.*(resources|percent)\.pak$', filename): |
| 249 native_resources_no_translations.AddZipInfo(member) |
| 250 elif re.search(r'\.lpak$|^assets/.*(?!resources|percent)\.pak$', filename): |
| 251 translations.AddZipInfo(member, 'en_' in filename or 'en-' in filename) |
| 252 elif filename == 'assets/icudtl.dat': |
| 253 icu_data.AddZipInfo(member) |
| 254 elif filename.endswith('.bin'): |
| 255 v8_snapshots.AddZipInfo(member) |
| 256 elif filename.endswith('.png') or filename.endswith('.webp'): |
| 257 png_drawables.AddZipInfo(member) |
| 258 elif filename.startswith('res/'): |
| 259 res_directory.AddZipInfo(member) |
| 260 elif filename.endswith('.arsc'): |
| 261 arsc.AddZipInfo(member) |
| 262 elif filename.startswith('META-INF') or filename == 'AndroidManifest.xml': |
| 263 metadata.AddZipInfo(member) |
| 222 else: | 264 else: |
| 223 raise KeyError('No group found for file "%s"' % member.filename) | 265 unknown.AddZipInfo(member) |
| 224 | 266 |
| 225 total_install_size = total_apk_size | 267 total_install_size = total_apk_size |
| 226 | 268 |
| 227 for group in FILE_GROUPS: | 269 for group in file_groups: |
| 228 uncompressed_size = 0 | 270 install_size = group.ComputeInstallSize() |
| 229 packed_size = 0 | 271 total_install_size += group.ComputeExtractedSize() |
| 230 extracted_size = 0 | |
| 231 for member in found_files[group]: | |
| 232 uncompressed_size += member.file_size | |
| 233 packed_size += member.compress_size | |
| 234 # Assume that if a file is not compressed, then it is not extracted. | |
| 235 is_compressed = member.compress_type != zipfile.ZIP_STORED | |
| 236 if is_compressed and group.extracted(member.filename): | |
| 237 extracted_size += member.file_size | |
| 238 install_size = packed_size + extracted_size | |
| 239 total_install_size += extracted_size | |
| 240 | 272 |
| 241 ReportPerfResult(chartjson, apk_basename + '_Breakdown', | 273 ReportPerfResult(chartjson, apk_basename + '_Breakdown', |
| 242 group.name + ' size', packed_size, 'bytes') | 274 group.name + ' size', group.ComputeZippedSize(), 'bytes') |
| 243 ReportPerfResult(chartjson, apk_basename + '_InstallBreakdown', | 275 ReportPerfResult(chartjson, apk_basename + '_InstallBreakdown', |
| 244 group.name + ' size', install_size, 'bytes') | 276 group.name + ' size', install_size, 'bytes') |
| 245 ReportPerfResult(chartjson, apk_basename + '_Uncompressed', | 277 ReportPerfResult(chartjson, apk_basename + '_Uncompressed', |
| 246 group.name + ' size', uncompressed_size, 'bytes') | 278 group.name + ' size', group.ComputeUncompressedSize(), |
| 279 'bytes') |
| 247 | 280 |
| 248 transfer_size = _CalculateCompressedSize(apk_filename) | 281 ReportPerfResult(chartjson, apk_basename + '_InstallSize', 'APK size', |
| 282 total_apk_size, 'bytes') |
| 249 ReportPerfResult(chartjson, apk_basename + '_InstallSize', | 283 ReportPerfResult(chartjson, apk_basename + '_InstallSize', |
| 250 'Estimated installed size', total_install_size, 'bytes') | 284 'Estimated installed size', total_install_size, 'bytes') |
| 251 ReportPerfResult(chartjson, apk_basename + '_InstallSize', 'APK size', | 285 transfer_size = _CalculateCompressedSize(apk_filename) |
| 252 total_apk_size, 'bytes') | |
| 253 ReportPerfResult(chartjson, apk_basename + '_TransferSize', | 286 ReportPerfResult(chartjson, apk_basename + '_TransferSize', |
| 254 'Transfer size (deflate)', transfer_size, 'bytes') | 287 'Transfer size (deflate)', transfer_size, 'bytes') |
| 255 | 288 |
| 289 # Size of main dex vs remaining. |
| 290 main_dex_info = java_code.FindByPattern('classes.dex') |
| 291 if main_dex_info: |
| 292 main_dex_size = main_dex_info.file_size |
| 293 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 294 'main dex size', main_dex_size, 'bytes') |
| 295 secondary_size = java_code.ComputeUncompressedSize() - main_dex_size |
| 296 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 297 'secondary dex size', secondary_size, 'bytes') |
| 298 |
| 299 # Size of main .so vs remaining. |
| 300 main_lib_info = native_code.FindLargest() |
| 301 if main_lib_info: |
| 302 main_lib_size = main_lib_info.file_size |
| 303 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 304 'main lib size', main_lib_size, 'bytes') |
| 305 secondary_size = native_code.ComputeUncompressedSize() - main_lib_size |
| 306 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 307 'other lib size', secondary_size, 'bytes') |
| 308 |
| 309 # Main metric that we want to monitor for jumps. |
| 310 normalized_apk_size = total_apk_size |
| 311 # Always look at uncompressed .dex & .so. |
| 312 normalized_apk_size -= java_code.ComputeZippedSize() |
| 313 normalized_apk_size += java_code.ComputeUncompressedSize() |
| 314 normalized_apk_size -= native_code.ComputeZippedSize() |
| 315 normalized_apk_size += native_code.ComputeUncompressedSize() |
| 316 # Avoid noise caused when strings change and translations haven't yet been |
| 317 # updated. |
| 318 english_pak = translations.FindByPattern(r'.*/en[-_][Uu][Ss]\.l?pak') |
| 319 if english_pak: |
| 320 normalized_apk_size -= translations.ComputeZippedSize() |
| 321 # 1.17 found by looking at Chrome.apk and seeing how much smaller en-US.pak |
| 322 # is relative to the average locale .pak. |
| 323 normalized_apk_size += int( |
| 324 english_pak.compress_size * translations.GetNumEntries() * 1.17) |
| 325 |
| 326 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 327 'normalized apk size', normalized_apk_size, 'bytes') |
| 328 |
| 329 ReportPerfResult(chartjson, apk_basename + '_Specifics', |
| 330 'file count', len(apk_contents), 'zip entries') |
| 331 |
| 256 | 332 |
| 257 def IsPakFileName(file_name): | 333 def IsPakFileName(file_name): |
| 258 """Returns whether the given file name ends with .pak or .lpak.""" | 334 """Returns whether the given file name ends with .pak or .lpak.""" |
| 259 return file_name.endswith('.pak') or file_name.endswith('.lpak') | 335 return file_name.endswith('.pak') or file_name.endswith('.lpak') |
| 260 | 336 |
| 261 | 337 |
| 262 def PrintPakAnalysis(apk_filename, min_pak_resource_size): | 338 def PrintPakAnalysis(apk_filename, min_pak_resource_size): |
| 263 """Print sizes of all resources in all pak files in |apk_filename|.""" | 339 """Print sizes of all resources in all pak files in |apk_filename|.""" |
| 264 print | 340 print |
| 265 print 'Analyzing pak files in %s...' % apk_filename | 341 print 'Analyzing pak files in %s...' % apk_filename |
| (...skipping 193 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 459 option_parser.add_option('--so-with-symbols-path', | 535 option_parser.add_option('--so-with-symbols-path', |
| 460 help='Mostly obsolete. Use .so within .apk instead.') | 536 help='Mostly obsolete. Use .so within .apk instead.') |
| 461 option_parser.add_option('--min-pak-resource-size', type='int', | 537 option_parser.add_option('--min-pak-resource-size', type='int', |
| 462 default=20*1024, | 538 default=20*1024, |
| 463 help='Minimum byte size of displayed pak resources.') | 539 help='Minimum byte size of displayed pak resources.') |
| 464 option_parser.add_option('--build_type', dest='build_type', default='Debug', | 540 option_parser.add_option('--build_type', dest='build_type', default='Debug', |
| 465 help='Obsoleted by --chromium-output-directory.') | 541 help='Obsoleted by --chromium-output-directory.') |
| 466 option_parser.add_option('--chromium-output-directory', | 542 option_parser.add_option('--chromium-output-directory', |
| 467 help='Location of the build artifacts. ' | 543 help='Location of the build artifacts. ' |
| 468 'Takes precidence over --build_type.') | 544 'Takes precidence over --build_type.') |
| 469 option_parser.add_option('--chartjson', action="store_true", | 545 option_parser.add_option('--chartjson', action='store_true', |
| 470 help='Sets output mode to chartjson.') | 546 help='Sets output mode to chartjson.') |
| 471 option_parser.add_option('--output-dir', default='.', | 547 option_parser.add_option('--output-dir', default='.', |
| 472 help='Directory to save chartjson to.') | 548 help='Directory to save chartjson to.') |
| 549 option_parser.add_option('--no-output-dir', action='store_true', |
| 550 help='Skip all measurements that rely on having ' |
| 551 'output-dir') |
| 473 option_parser.add_option('-d', '--device', | 552 option_parser.add_option('-d', '--device', |
| 474 help='Dummy option for perf runner.') | 553 help='Dummy option for perf runner.') |
| 475 options, args = option_parser.parse_args(argv) | 554 options, args = option_parser.parse_args(argv) |
| 476 files = args[1:] | 555 files = args[1:] |
| 477 chartjson = _BASE_CHART.copy() if options.chartjson else None | 556 chartjson = _BASE_CHART.copy() if options.chartjson else None |
| 478 | 557 |
| 479 constants.SetBuildType(options.build_type) | 558 constants.SetBuildType(options.build_type) |
| 480 if options.chromium_output_directory: | 559 if options.chromium_output_directory: |
| 481 constants.SetOutputDirectory(options.chromium_output_directory) | 560 constants.SetOutputDirectory(options.chromium_output_directory) |
| 482 constants.CheckOutputDirectory() | 561 if not options.no_output_dir: |
| 562 constants.CheckOutputDirectory() |
| 563 devil_chromium.Initialize() |
| 483 | 564 |
| 484 # For backward compatibilty with buildbot scripts, treat --so-path as just | 565 # For backward compatibilty with buildbot scripts, treat --so-path as just |
| 485 # another file to print the size of. We don't need it for anything special any | 566 # another file to print the size of. We don't need it for anything special any |
| 486 # more. | 567 # more. |
| 487 if options.so_path: | 568 if options.so_path: |
| 488 files.append(options.so_path) | 569 files.append(options.so_path) |
| 489 | 570 |
| 490 if not files: | 571 if not files: |
| 491 option_parser.error('Must specify a file') | 572 option_parser.error('Must specify a file') |
| 492 | 573 |
| 493 devil_chromium.Initialize() | |
| 494 | |
| 495 if options.so_with_symbols_path: | 574 if options.so_with_symbols_path: |
| 496 si_count = _PrintStaticInitializersCount(options.so_with_symbols_path) | 575 si_count = _PrintStaticInitializersCount(options.so_with_symbols_path) |
| 497 ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, | 576 ReportPerfResult(chartjson, 'StaticInitializersCount', 'count', si_count, |
| 498 'count') | 577 'count') |
| 499 | 578 |
| 500 PrintResourceSizes(files, chartjson=chartjson) | 579 PrintResourceSizes(files, chartjson=chartjson) |
| 501 | 580 |
| 502 for f in files: | 581 for f in files: |
| 503 if f.endswith('.apk'): | 582 if f.endswith('.apk'): |
| 504 PrintApkAnalysis(f, chartjson=chartjson) | 583 PrintApkAnalysis(f, chartjson=chartjson) |
| 505 PrintPakAnalysis(f, options.min_pak_resource_size) | |
| 506 _PrintDexAnalysis(f, chartjson=chartjson) | 584 _PrintDexAnalysis(f, chartjson=chartjson) |
| 507 if not options.so_with_symbols_path: | 585 if not options.no_output_dir: |
| 508 _PrintStaticInitializersCountFromApk(f, chartjson=chartjson) | 586 PrintPakAnalysis(f, options.min_pak_resource_size) |
| 587 if not options.so_with_symbols_path: |
| 588 _PrintStaticInitializersCountFromApk(f, chartjson=chartjson) |
| 509 | 589 |
| 510 if chartjson: | 590 if chartjson: |
| 511 results_path = os.path.join(options.output_dir, 'results-chart.json') | 591 results_path = os.path.join(options.output_dir, 'results-chart.json') |
| 512 logging.critical('Dumping json to %s', results_path) | 592 logging.critical('Dumping json to %s', results_path) |
| 513 with open(results_path, 'w') as json_file: | 593 with open(results_path, 'w') as json_file: |
| 514 json.dump(chartjson, json_file) | 594 json.dump(chartjson, json_file) |
| 515 | 595 |
| 516 | 596 |
| 517 if __name__ == '__main__': | 597 if __name__ == '__main__': |
| 518 sys.exit(main(sys.argv)) | 598 sys.exit(main(sys.argv)) |
| OLD | NEW |