Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python | |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
| 3 # Use of this source code is governed by a BSD-style license that can be | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 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. | |
| 8 Also breaks down the contents of the APK to determine the installed size | |
| 9 and assign size contributions to different classes of file. | |
| 10 """ | |
| 11 | |
| 12 import collections | |
| 13 import json | |
| 14 import operator | |
| 15 import optparse | |
| 16 import os | |
| 17 import re | |
| 18 import subprocess | |
| 19 import sys | |
| 20 import tempfile | |
| 21 import zipfile | |
| 22 import zlib | |
| 23 | |
| 24 sys.path.append( | |
| 25 os.path.join(sys.path[0], os.pardir, os.pardir, 'tools', 'grit')) | |
| 26 from grit.format import data_pack # pylint: disable=import-error | |
| 27 sys.path.append(os.path.join( | |
| 28 sys.path[0], os.pardir, os.pardir, 'build', 'util', 'lib', 'common')) | |
| 29 import perf_tests_results_helper # pylint: disable=import-error | |
| 30 | |
| 31 | |
| 32 # Static initializers expected in official builds. Note that this list is built | |
| 33 # using 'nm' on libchrome.so which results from a GCC official build (i.e. | |
| 34 # Clang is not supported currently). | |
| 35 | |
| 36 STATIC_INITIALIZER_SYMBOL_PREFIX = '_GLOBAL__I_' | |
| 37 | |
| 38 EXPECTED_STATIC_INITIALIZERS = frozenset([ | |
| 39 'allocators.cpp', | |
| 40 'common.pb.cc', | |
| 41 'defaults.cc', | |
| 42 'generated_message_util.cc', | |
| 43 'locale_impl.cpp', | |
| 44 'timeutils.cc', | |
| 45 'watchdog.cc', | |
| 46 # http://b/6354040 | |
| 47 'SkFontHost_android.cpp', | |
| 48 # http://b/6354040 | |
| 49 'isolate.cc', | |
| 50 'assembler_arm.cc', | |
| 51 'isolate.cc', | |
| 52 ]) | |
| 53 | |
| 54 _BASE_CHART = { | |
| 55 'format_version': '0.1', | |
| 56 'benchmark_name': 'resource_sizes', | |
| 57 'benchmark_description': 'APK resource size information.', | |
| 58 'trace_rerun_options': [], | |
| 59 'charts': {} | |
| 60 } | |
| 61 | |
| 62 | |
| 63 def GetStaticInitializers(so_path): | |
| 64 """Returns a list of static initializers found in the non-stripped library | |
| 65 located at the provided path. Note that this function assumes that the | |
| 66 library was compiled with GCC. | |
| 67 """ | |
| 68 handle = subprocess.Popen(['nm', so_path], stdout=subprocess.PIPE) | |
| 69 static_initializers = [] | |
| 70 for line in handle.stdout: | |
| 71 symbol_name = line.split(' ').pop().rstrip() | |
| 72 if STATIC_INITIALIZER_SYMBOL_PREFIX in symbol_name: | |
| 73 static_initializers.append( | |
| 74 symbol_name.replace(STATIC_INITIALIZER_SYMBOL_PREFIX, '')) | |
| 75 return static_initializers | |
| 76 | |
| 77 | |
| 78 def add_value(chart_data, graph_title, trace_title, value, units, | |
| 79 improvement_direction='down', important=True): | |
| 80 chart_data['charts'].setdefault(graph_title, {}) | |
| 81 chart_data['charts'][graph_title][trace_title] = { | |
| 82 'type': 'scalar', | |
| 83 'value': value, | |
| 84 'units': units, | |
| 85 'imporvement_direction': improvement_direction, | |
| 86 'important': important | |
| 87 } | |
| 88 | |
| 89 | |
| 90 def PrintResourceSizes(files, chartjson=None): | |
| 91 """Prints the sizes of each given file. | |
| 92 | |
| 93 Args: | |
| 94 files: List of files to print sizes for. | |
| 95 """ | |
| 96 for f in files: | |
| 97 if chartjson: | |
| 98 if not isinstance(chartjson, dict): | |
| 99 raise TypeError('Chartjson must be a dictionary.') | |
| 100 add_value(chartjson, 'ResourceSizes', os.path.basename(f) + ' size', | |
| 101 os.path.getsize(f), 'bytes') | |
| 102 else: | |
| 103 perf_tests_results_helper.PrintPerfResult('ResourceSizes', | |
| 104 os.path.basename(f) + ' size', | |
| 105 [os.path.getsize(f)], 'bytes') | |
| 106 | |
| 107 | |
| 108 def PrintApkAnalysis(apk_filename, chartjson=None): | |
| 109 """Analyse APK to determine size contributions of different file classes.""" | |
| 110 | |
| 111 # Define a named tuple type for file grouping. | |
| 112 # name: Human readable name for this file group | |
| 113 # regex: Regular expression to match filename | |
| 114 # extracted: Function that takes a file name and returns whether the file is | |
| 115 # extracted from the apk at install/runtime. | |
| 116 FileGroup = collections.namedtuple('FileGroup', | |
| 117 ['name', 'regex', 'extracted']) | |
| 118 | |
| 119 # File groups are checked in sequence, so more specific regexes should be | |
| 120 # earlier in the list. | |
| 121 YES = lambda _: True | |
| 122 NO = lambda _: False | |
| 123 FILE_GROUPS = ( | |
| 124 FileGroup('Native code', r'\.so$', lambda f: 'crazy' not in f), | |
| 125 FileGroup('Java code', r'\.dex$', YES), | |
| 126 FileGroup('Native resources (no l10n)', r'\.pak$', NO), | |
| 127 # For locale paks, assume only english paks are extracted. | |
| 128 FileGroup('Native resources (l10n)', r'\.lpak$', lambda f: 'en_' in f), | |
| 129 FileGroup('ICU (i18n library) data', r'assets/icudtl\.dat$', NO), | |
| 130 FileGroup('V8 Snapshots', r'\.bin$', NO), | |
| 131 FileGroup('PNG drawables', r'\.png$', NO), | |
| 132 FileGroup('Non-compiled Android resources', r'^res/', NO), | |
| 133 FileGroup('Compiled Android resources', r'\.arsc$', NO), | |
| 134 FileGroup('Package metadata', r'^(META-INF/|AndroidManifest\.xml$)', NO), | |
| 135 FileGroup('Unknown files', r'.', NO), | |
| 136 ) | |
| 137 | |
| 138 apk = zipfile.ZipFile(apk_filename, 'r') | |
| 139 try: | |
| 140 apk_contents = apk.infolist() | |
| 141 finally: | |
| 142 apk.close() | |
| 143 | |
| 144 total_apk_size = os.path.getsize(apk_filename) | |
| 145 apk_basename = os.path.basename(apk_filename) | |
| 146 | |
| 147 found_files = {} | |
| 148 for group in FILE_GROUPS: | |
| 149 found_files[group] = [] | |
| 150 | |
| 151 for member in apk_contents: | |
| 152 for group in FILE_GROUPS: | |
| 153 if re.search(group.regex, member.filename): | |
| 154 found_files[group].append(member) | |
| 155 break | |
| 156 else: | |
| 157 raise KeyError('No group found for file "%s"' % member.filename) | |
| 158 | |
| 159 total_install_size = total_apk_size | |
| 160 | |
| 161 for group in FILE_GROUPS: | |
| 162 apk_size = sum(member.compress_size for member in found_files[group]) | |
| 163 install_size = apk_size | |
| 164 install_bytes = sum(f.file_size for f in found_files[group] | |
| 165 if group.extracted(f.filename)) | |
| 166 install_size += install_bytes | |
| 167 total_install_size += install_bytes | |
| 168 | |
| 169 if chartjson: | |
| 170 if not isinstance(chartjson, dict): | |
|
agrieve
2015/11/13 19:54:41
nit: either remove this assertion or put it at the
rnephew (Reviews Here)
2015/11/13 20:53:17
Done.
| |
| 171 raise TypeError('Chartjson must be a dictionary.') | |
| 172 add_value(chartjson, apk_basename + '_Breakdown', group.name + ' size', | |
| 173 apk_size, 'bytes') | |
| 174 add_value(chartjson, apk_basename + '_InstallBreakdown', | |
| 175 group.name + ' size', install_size, 'bytes') | |
| 176 else: | |
| 177 perf_tests_results_helper.PrintPerfResult(apk_basename + '_Breakdown', | |
| 178 group.name + ' size', | |
| 179 [apk_size], 'bytes') | |
| 180 perf_tests_results_helper.PrintPerfResult(apk_basename + | |
| 181 '_InstallBreakdown', | |
| 182 group.name + ' size', | |
| 183 [install_size], 'bytes') | |
| 184 | |
| 185 transfer_size = _CalculateCompressedSize(apk_filename) | |
| 186 if chartjson: | |
| 187 add_value(chartjson, apk_basename + '_InstallSize', | |
| 188 'Estimated installed size', total_install_size, 'bytes') | |
| 189 add_value(chartjson, apk_basename + '_InstallSize', 'APK size', | |
| 190 total_apk_size, 'bytes') | |
| 191 add_value(chartjson, apk_basename + '_TransferSize', | |
| 192 'Transfer size (deflate)', transfer_size, 'bytes') | |
| 193 | |
| 194 else: | |
| 195 perf_tests_results_helper.PrintPerfResult(apk_basename + '_InstallSize', | |
| 196 'Estimated installed size', | |
| 197 [total_install_size], 'bytes') | |
| 198 perf_tests_results_helper.PrintPerfResult(apk_basename + '_InstallSize', | |
| 199 'APK size', | |
| 200 [total_apk_size], 'bytes') | |
| 201 perf_tests_results_helper.PrintPerfResult(apk_basename + '_TransferSize', | |
| 202 'Transfer size (deflate)', | |
| 203 [transfer_size], 'bytes') | |
| 204 | |
| 205 | |
| 206 def IsPakFileName(file_name): | |
| 207 """Returns whether the given file name ends with .pak or .lpak.""" | |
| 208 return file_name.endswith('.pak') or file_name.endswith('.lpak') | |
| 209 | |
| 210 | |
| 211 def PrintPakAnalysis(apk_filename, min_pak_resource_size, build_type): | |
| 212 """Print sizes of all resources in all pak files in |apk_filename|.""" | |
| 213 | |
| 214 print | |
| 215 print 'Analyzing pak files in %s...' % apk_filename | |
| 216 | |
| 217 # A structure for holding details about a pak file. | |
| 218 Pak = collections.namedtuple( | |
| 219 'Pak', ['filename', 'compress_size', 'file_size', 'resources']) | |
| 220 | |
| 221 # Build a list of Pak objets for each pak file. | |
| 222 paks = [] | |
| 223 apk = zipfile.ZipFile(apk_filename, 'r') | |
| 224 try: | |
| 225 for i in apk.infolist(): | |
| 226 if not IsPakFileName(i.filename): | |
| 227 continue | |
| 228 with tempfile.NamedTemporaryFile() as f: | |
| 229 f.write(apk.read(i.filename)) | |
| 230 f.flush() | |
| 231 paks.append(Pak(i.filename, i.compress_size, i.file_size, | |
| 232 data_pack.DataPack.ReadDataPack(f.name).resources)) | |
| 233 finally: | |
| 234 apk.close() | |
| 235 | |
| 236 # Output the overall pak file summary. | |
| 237 total_files = len(paks) | |
| 238 total_compress_size = sum([pak.compress_size for pak in paks]) | |
| 239 total_file_size = sum([pak.file_size for pak in paks]) | |
| 240 print 'Total pak files: %d' % total_files | |
| 241 print 'Total compressed size: %s' % _FormatBytes(total_compress_size) | |
| 242 print 'Total uncompressed size: %s' % _FormatBytes(total_file_size) | |
| 243 print | |
| 244 | |
| 245 # Output the table of details about all pak files. | |
| 246 print '%25s%11s%21s%21s' % ( | |
| 247 'FILENAME', 'RESOURCES', 'COMPRESSED SIZE', 'UNCOMPRESSED SIZE') | |
| 248 for pak in sorted(paks, key=operator.attrgetter('file_size'), reverse=True): | |
| 249 print '%25s %10s %12s %6.2f%% %12s %6.2f%%' % ( | |
| 250 pak.filename, | |
| 251 len(pak.resources), | |
| 252 _FormatBytes(pak.compress_size), | |
| 253 100.0 * pak.compress_size / total_compress_size, | |
| 254 _FormatBytes(pak.file_size), | |
| 255 100.0 * pak.file_size / total_file_size) | |
| 256 | |
| 257 print | |
| 258 print 'Analyzing pak resources in %s...' % apk_filename | |
| 259 | |
| 260 # Calculate aggregate stats about resources across pak files. | |
| 261 resource_count_map = collections.defaultdict(int) | |
| 262 resource_size_map = collections.defaultdict(int) | |
| 263 resource_overhead_bytes = 6 | |
| 264 for pak in paks: | |
| 265 for r in pak.resources: | |
| 266 resource_count_map[r] += 1 | |
| 267 resource_size_map[r] += len(pak.resources[r]) + resource_overhead_bytes | |
| 268 | |
| 269 # Output the overall resource summary. | |
| 270 total_resource_size = sum(resource_size_map.values()) | |
| 271 total_resource_count = len(resource_count_map) | |
| 272 assert total_resource_size <= total_file_size | |
| 273 print 'Total pak resources: %s' % total_resource_count | |
| 274 print 'Total uncompressed resource size: %s' % _FormatBytes( | |
| 275 total_resource_size) | |
| 276 print | |
| 277 | |
| 278 resource_id_name_map = _GetResourceIdNameMap(build_type) | |
| 279 | |
| 280 # Output the table of details about all resources across pak files. | |
| 281 print | |
| 282 print '%56s %5s %17s' % ('RESOURCE', 'COUNT', 'UNCOMPRESSED SIZE') | |
| 283 for i in sorted(resource_size_map, key=resource_size_map.get, | |
| 284 reverse=True): | |
| 285 if resource_size_map[i] >= min_pak_resource_size: | |
| 286 print '%56s %5s %9s %6.2f%%' % ( | |
| 287 i in resource_id_name_map and resource_id_name_map[i] or i, | |
| 288 resource_count_map[i], | |
| 289 _FormatBytes(resource_size_map[i]), | |
| 290 100.0 * resource_size_map[i] / total_resource_size) | |
| 291 | |
| 292 | |
| 293 def _GetResourceIdNameMap(build_type): | |
| 294 """Returns a map of {resource_id: resource_name}.""" | |
| 295 out_dir = os.path.join(sys.path[0], '..', '..', 'out', build_type) | |
| 296 assert os.path.isdir(out_dir), 'Failed to locate out dir at %s' % out_dir | |
| 297 print 'Looking at resources in: %s' % out_dir | |
| 298 | |
| 299 grit_headers = [] | |
| 300 for root, _, files in os.walk(out_dir): | |
| 301 if root.endswith('grit'): | |
| 302 grit_headers += [os.path.join(root, f) for f in files if f.endswith('.h')] | |
| 303 assert grit_headers, 'Failed to find grit headers in %s' % out_dir | |
| 304 | |
| 305 rc_header_re = re.compile(r'^#define (?P<name>\w+) (?P<id>\d+)$') | |
| 306 id_name_map = {} | |
| 307 for header in grit_headers: | |
| 308 with open(header, 'r') as f: | |
| 309 for line in f.readlines(): | |
| 310 m = rc_header_re.match(line.strip()) | |
| 311 if m: | |
| 312 i = int(m.group('id')) | |
| 313 name = m.group('name') | |
| 314 if i in id_name_map and name != id_name_map[i]: | |
| 315 print 'WARNING: Resource ID conflict %s (%s vs %s)' % ( | |
| 316 i, id_name_map[i], name) | |
| 317 id_name_map[i] = name | |
| 318 return id_name_map | |
| 319 | |
| 320 | |
| 321 def PrintStaticInitializersCount(so_with_symbols_path, chartjson=None): | |
| 322 """Emits the performance result for static initializers found in the provided | |
| 323 shared library. Additionally, files for which static initializers were | |
| 324 found are printed on the standard output. | |
| 325 | |
| 326 Args: | |
| 327 so_with_symbols_path: Path to the unstripped libchrome.so file. | |
| 328 """ | |
| 329 print 'Files with static initializers:' | |
| 330 static_initializers = GetStaticInitializers(so_with_symbols_path) | |
| 331 print '\n'.join(static_initializers) | |
| 332 | |
| 333 if chartjson: | |
| 334 if not isinstance(chartjson, dict): | |
| 335 raise TypeError('Chartjson must be a dictionary.') | |
| 336 add_value(chartjson, 'StaticInitializersCount', 'count', | |
| 337 len(static_initializers), 'count') | |
| 338 else: | |
| 339 perf_tests_results_helper.PrintPerfResult( | |
| 340 'StaticInitializersCount', 'count', [len(static_initializers)], 'count') | |
| 341 | |
| 342 | |
| 343 def _FormatBytes(byts): | |
| 344 """Pretty-print a number of bytes.""" | |
| 345 if byts > 2**20.0: | |
| 346 byts /= 2**20.0 | |
| 347 return '%.2fm' % byts | |
| 348 if byts > 2**10.0: | |
| 349 byts /= 2**10.0 | |
| 350 return '%.2fk' % byts | |
| 351 return str(byts) | |
| 352 | |
| 353 | |
| 354 def _CalculateCompressedSize(file_path): | |
| 355 CHUNK_SIZE = 256 * 1024 | |
| 356 compressor = zlib.compressobj() | |
| 357 total_size = 0 | |
| 358 with open(file_path, 'rb') as f: | |
| 359 for chunk in iter(lambda: f.read(CHUNK_SIZE), ''): | |
| 360 total_size += len(compressor.compress(chunk)) | |
| 361 total_size += len(compressor.flush()) | |
| 362 return total_size | |
| 363 | |
| 364 | |
| 365 def main(argv): | |
| 366 usage = """Usage: %prog [options] file1 file2 ... | |
| 367 | |
| 368 Pass any number of files to graph their sizes. Any files with the extension | |
| 369 '.apk' will be broken down into their components on a separate graph.""" | |
| 370 option_parser = optparse.OptionParser(usage=usage) | |
| 371 option_parser.add_option('--so-path', help='Path to libchrome.so') | |
| 372 option_parser.add_option('--so-with-symbols-path', | |
| 373 help='Path to libchrome.so with symbols') | |
| 374 option_parser.add_option('--min-pak-resource-size', type='int', | |
| 375 default=20*1024, | |
| 376 help='Minimum byte size of displayed pak resources') | |
| 377 option_parser.add_option('--build_type', dest='build_type', default='Debug', | |
| 378 help='Sets the build type, default is Debug') | |
| 379 option_parser.add_option('--chartjson', action="store_true", | |
| 380 help='Sets output mode to chartjson') | |
| 381 option_parser.add_option('--output-dir', | |
| 382 help='Directory to save chartjson to.') | |
| 383 option_parser.add_option('-d', '--device', | |
| 384 help='Dummy option for perf runner.') | |
| 385 options, args = option_parser.parse_args(argv) | |
| 386 files = args[1:] | |
| 387 | |
| 388 if options.chartjson: | |
| 389 if not options.output_dir: | |
| 390 option_parser.error('Must set --output-dir if using chartjson format') | |
| 391 chartjson = _BASE_CHART.copy() | |
| 392 else: | |
| 393 if options.output_dir: | |
| 394 option_parser.error('Must set --chartjson if you set --output-dir') | |
| 395 chartjson = None | |
| 396 | |
| 397 # For backward compatibilty with buildbot scripts, treat --so-path as just | |
| 398 # another file to print the size of. We don't need it for anything special any | |
| 399 # more. | |
| 400 if options.so_path: | |
| 401 files.append(options.so_path) | |
| 402 | |
| 403 if not files: | |
| 404 option_parser.error('Must specify a file') | |
| 405 | |
| 406 if options.so_with_symbols_path: | |
| 407 PrintStaticInitializersCount( | |
| 408 options.so_with_symbols_path, chartjson=chartjson) | |
| 409 | |
| 410 PrintResourceSizes(files, chartjson=chartjson) | |
| 411 | |
| 412 for f in files: | |
| 413 if f.endswith('.apk'): | |
| 414 PrintApkAnalysis(f, chartjson=chartjson) | |
| 415 PrintPakAnalysis(f, options.min_pak_resource_size, options.build_type) | |
| 416 | |
| 417 if chartjson: | |
| 418 with open(os.path.join( | |
| 419 options.output_dir, 'results-chart.json'), 'w') as json_file: | |
| 420 json.dump(chartjson, json_file) | |
| 421 | |
| 422 | |
| 423 if __name__ == '__main__': | |
| 424 sys.exit(main(sys.argv)) | |
| OLD | NEW |