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 |