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, | |
perezju
2015/11/16 10:22:34
style: it's no longer a private function, so shoul
rnephew (Reviews Here)
2015/11/16 16:22:01
Done.
| |
79 improvement_direction='down', important=True): | |
80 """Adds data to chartjson dict in proper format.""" | |
81 chart_data['charts'].setdefault(graph_title, {}) | |
82 chart_data['charts'][graph_title][trace_title] = { | |
83 'type': 'scalar', | |
84 'value': value, | |
85 'units': units, | |
86 'imporvement_direction': improvement_direction, | |
87 'important': important | |
88 } | |
89 | |
90 | |
91 def PrintResourceSizes(files, chartjson=None): | |
92 """Prints the sizes of each given file. | |
93 | |
94 Args: | |
95 files: List of files to print sizes for. | |
96 """ | |
97 if chartjson and not isinstance(chartjson, dict): | |
98 raise TypeError('Chartjson must be a dictionary.') | |
99 for f in files: | |
100 if chartjson: | |
101 add_value(chartjson, 'ResourceSizes', os.path.basename(f) + ' size', | |
102 os.path.getsize(f), 'bytes') | |
103 else: | |
104 perf_tests_results_helper.PrintPerfResult('ResourceSizes', | |
105 os.path.basename(f) + ' size', | |
106 [os.path.getsize(f)], 'bytes') | |
107 | |
108 | |
109 def PrintApkAnalysis(apk_filename, chartjson=None): | |
110 """Analyse APK to determine size contributions of different file classes.""" | |
111 if chartjson and not isinstance(chartjson, dict): | |
112 raise TypeError('Chartjson must be a dictionary.') | |
113 # Define a named tuple type for file grouping. | |
114 # name: Human readable name for this file group | |
115 # regex: Regular expression to match filename | |
116 # extracted: Function that takes a file name and returns whether the file is | |
117 # extracted from the apk at install/runtime. | |
118 FileGroup = collections.namedtuple('FileGroup', | |
119 ['name', 'regex', 'extracted']) | |
120 | |
121 # File groups are checked in sequence, so more specific regexes should be | |
122 # earlier in the list. | |
123 YES = lambda _: True | |
124 NO = lambda _: False | |
125 FILE_GROUPS = ( | |
126 FileGroup('Native code', r'\.so$', lambda f: 'crazy' not in f), | |
127 FileGroup('Java code', r'\.dex$', YES), | |
128 FileGroup('Native resources (no l10n)', r'\.pak$', NO), | |
129 # For locale paks, assume only english paks are extracted. | |
130 FileGroup('Native resources (l10n)', r'\.lpak$', lambda f: 'en_' in f), | |
131 FileGroup('ICU (i18n library) data', r'assets/icudtl\.dat$', NO), | |
132 FileGroup('V8 Snapshots', r'\.bin$', NO), | |
133 FileGroup('PNG drawables', r'\.png$', NO), | |
134 FileGroup('Non-compiled Android resources', r'^res/', NO), | |
135 FileGroup('Compiled Android resources', r'\.arsc$', NO), | |
136 FileGroup('Package metadata', r'^(META-INF/|AndroidManifest\.xml$)', NO), | |
137 FileGroup('Unknown files', r'.', NO), | |
138 ) | |
139 | |
140 apk = zipfile.ZipFile(apk_filename, 'r') | |
141 try: | |
142 apk_contents = apk.infolist() | |
143 finally: | |
144 apk.close() | |
145 | |
146 total_apk_size = os.path.getsize(apk_filename) | |
147 apk_basename = os.path.basename(apk_filename) | |
148 | |
149 found_files = {} | |
150 for group in FILE_GROUPS: | |
151 found_files[group] = [] | |
152 | |
153 for member in apk_contents: | |
154 for group in FILE_GROUPS: | |
155 if re.search(group.regex, member.filename): | |
156 found_files[group].append(member) | |
157 break | |
158 else: | |
159 raise KeyError('No group found for file "%s"' % member.filename) | |
160 | |
161 total_install_size = total_apk_size | |
162 | |
163 for group in FILE_GROUPS: | |
164 apk_size = sum(member.compress_size for member in found_files[group]) | |
165 install_size = apk_size | |
166 install_bytes = sum(f.file_size for f in found_files[group] | |
167 if group.extracted(f.filename)) | |
168 install_size += install_bytes | |
169 total_install_size += install_bytes | |
170 | |
171 if chartjson: | |
172 add_value(chartjson, apk_basename + '_Breakdown', group.name + ' size', | |
perezju
2015/11/16 10:22:34
There seems to be a lot of repetition everywhere w
rnephew (Reviews Here)
2015/11/16 16:22:01
Moved to ReportPerfResults.
| |
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 print | |
214 print 'Analyzing pak files in %s...' % apk_filename | |
215 | |
216 # A structure for holding details about a pak file. | |
217 Pak = collections.namedtuple( | |
218 'Pak', ['filename', 'compress_size', 'file_size', 'resources']) | |
219 | |
220 # Build a list of Pak objets for each pak file. | |
221 paks = [] | |
222 apk = zipfile.ZipFile(apk_filename, 'r') | |
223 try: | |
224 for i in apk.infolist(): | |
225 if not IsPakFileName(i.filename): | |
226 continue | |
227 with tempfile.NamedTemporaryFile() as f: | |
228 f.write(apk.read(i.filename)) | |
229 f.flush() | |
230 paks.append(Pak(i.filename, i.compress_size, i.file_size, | |
231 data_pack.DataPack.ReadDataPack(f.name).resources)) | |
232 finally: | |
233 apk.close() | |
234 | |
235 # Output the overall pak file summary. | |
236 total_files = len(paks) | |
237 total_compress_size = sum([pak.compress_size for pak in paks]) | |
238 total_file_size = sum([pak.file_size for pak in paks]) | |
239 print 'Total pak files: %d' % total_files | |
240 print 'Total compressed size: %s' % _FormatBytes(total_compress_size) | |
241 print 'Total uncompressed size: %s' % _FormatBytes(total_file_size) | |
242 print | |
243 | |
244 # Output the table of details about all pak files. | |
245 print '%25s%11s%21s%21s' % ( | |
246 'FILENAME', 'RESOURCES', 'COMPRESSED SIZE', 'UNCOMPRESSED SIZE') | |
247 for pak in sorted(paks, key=operator.attrgetter('file_size'), reverse=True): | |
248 print '%25s %10s %12s %6.2f%% %12s %6.2f%%' % ( | |
249 pak.filename, | |
250 len(pak.resources), | |
251 _FormatBytes(pak.compress_size), | |
252 100.0 * pak.compress_size / total_compress_size, | |
253 _FormatBytes(pak.file_size), | |
254 100.0 * pak.file_size / total_file_size) | |
255 | |
256 print | |
257 print 'Analyzing pak resources in %s...' % apk_filename | |
258 | |
259 # Calculate aggregate stats about resources across pak files. | |
260 resource_count_map = collections.defaultdict(int) | |
261 resource_size_map = collections.defaultdict(int) | |
262 resource_overhead_bytes = 6 | |
263 for pak in paks: | |
264 for r in pak.resources: | |
265 resource_count_map[r] += 1 | |
266 resource_size_map[r] += len(pak.resources[r]) + resource_overhead_bytes | |
267 | |
268 # Output the overall resource summary. | |
269 total_resource_size = sum(resource_size_map.values()) | |
270 total_resource_count = len(resource_count_map) | |
271 assert total_resource_size <= total_file_size | |
272 print 'Total pak resources: %s' % total_resource_count | |
273 print 'Total uncompressed resource size: %s' % _FormatBytes( | |
274 total_resource_size) | |
275 print | |
276 | |
277 resource_id_name_map = _GetResourceIdNameMap(build_type) | |
278 | |
279 # Output the table of details about all resources across pak files. | |
280 print | |
281 print '%56s %5s %17s' % ('RESOURCE', 'COUNT', 'UNCOMPRESSED SIZE') | |
282 for i in sorted(resource_size_map, key=resource_size_map.get, | |
283 reverse=True): | |
284 if resource_size_map[i] >= min_pak_resource_size: | |
285 print '%56s %5s %9s %6.2f%%' % ( | |
286 i in resource_id_name_map and resource_id_name_map[i] or i, | |
287 resource_count_map[i], | |
288 _FormatBytes(resource_size_map[i]), | |
289 100.0 * resource_size_map[i] / total_resource_size) | |
290 | |
291 | |
292 def _GetResourceIdNameMap(build_type): | |
293 """Returns a map of {resource_id: resource_name}.""" | |
294 out_dir = os.path.join(sys.path[0], '..', '..', 'out', build_type) | |
295 assert os.path.isdir(out_dir), 'Failed to locate out dir at %s' % out_dir | |
296 print 'Looking at resources in: %s' % out_dir | |
297 | |
298 grit_headers = [] | |
299 for root, _, files in os.walk(out_dir): | |
300 if root.endswith('grit'): | |
301 grit_headers += [os.path.join(root, f) for f in files if f.endswith('.h')] | |
302 assert grit_headers, 'Failed to find grit headers in %s' % out_dir | |
303 | |
304 rc_header_re = re.compile(r'^#define (?P<name>\w+) (?P<id>\d+)$') | |
305 id_name_map = {} | |
306 for header in grit_headers: | |
307 with open(header, 'r') as f: | |
308 for line in f.readlines(): | |
309 m = rc_header_re.match(line.strip()) | |
310 if m: | |
311 i = int(m.group('id')) | |
312 name = m.group('name') | |
313 if i in id_name_map and name != id_name_map[i]: | |
314 print 'WARNING: Resource ID conflict %s (%s vs %s)' % ( | |
315 i, id_name_map[i], name) | |
316 id_name_map[i] = name | |
317 return id_name_map | |
318 | |
319 | |
320 def PrintStaticInitializersCount(so_with_symbols_path, chartjson=None): | |
321 """Emits the performance result for static initializers found in the provided | |
322 shared library. Additionally, files for which static initializers were | |
323 found are printed on the standard output. | |
324 | |
325 Args: | |
326 so_with_symbols_path: Path to the unstripped libchrome.so file. | |
327 """ | |
328 if chartjson and not isinstance(chartjson, dict): | |
329 raise TypeError('Chartjson must be a dictionary.') | |
330 print 'Files with static initializers:' | |
331 static_initializers = GetStaticInitializers(so_with_symbols_path) | |
332 print '\n'.join(static_initializers) | |
333 | |
334 if chartjson: | |
335 add_value(chartjson, 'StaticInitializersCount', 'count', | |
336 len(static_initializers), 'count') | |
337 else: | |
338 perf_tests_results_helper.PrintPerfResult( | |
339 'StaticInitializersCount', 'count', [len(static_initializers)], 'count') | |
340 | |
341 | |
342 def _FormatBytes(byts): | |
343 """Pretty-print a number of bytes.""" | |
344 if byts > 2**20.0: | |
345 byts /= 2**20.0 | |
346 return '%.2fm' % byts | |
347 if byts > 2**10.0: | |
348 byts /= 2**10.0 | |
349 return '%.2fk' % byts | |
350 return str(byts) | |
351 | |
352 | |
353 def _CalculateCompressedSize(file_path): | |
354 CHUNK_SIZE = 256 * 1024 | |
355 compressor = zlib.compressobj() | |
356 total_size = 0 | |
357 with open(file_path, 'rb') as f: | |
358 for chunk in iter(lambda: f.read(CHUNK_SIZE), ''): | |
359 total_size += len(compressor.compress(chunk)) | |
360 total_size += len(compressor.flush()) | |
361 return total_size | |
362 | |
363 | |
364 def main(argv): | |
365 usage = """Usage: %prog [options] file1 file2 ... | |
366 | |
367 Pass any number of files to graph their sizes. Any files with the extension | |
368 '.apk' will be broken down into their components on a separate graph.""" | |
369 option_parser = optparse.OptionParser(usage=usage) | |
370 option_parser.add_option('--so-path', help='Path to libchrome.so') | |
371 option_parser.add_option('--so-with-symbols-path', | |
372 help='Path to libchrome.so with symbols') | |
perezju
2015/11/16 10:22:34
nit: end all help sentences with "."
rnephew (Reviews Here)
2015/11/16 16:22:01
Done.
| |
373 option_parser.add_option('--min-pak-resource-size', type='int', | |
374 default=20*1024, | |
375 help='Minimum byte size of displayed pak resources') | |
376 option_parser.add_option('--build_type', dest='build_type', default='Debug', | |
377 help='Sets the build type, default is Debug') | |
378 option_parser.add_option('--chartjson', action="store_true", | |
379 help='Sets output mode to chartjson') | |
380 option_parser.add_option('--output-dir', | |
381 help='Directory to save chartjson to.') | |
382 option_parser.add_option('-d', '--device', | |
383 help='Dummy option for perf runner.') | |
384 options, args = option_parser.parse_args(argv) | |
385 files = args[1:] | |
386 | |
387 if options.chartjson: | |
388 if not options.output_dir: | |
389 option_parser.error('Must set --output-dir if using chartjson format') | |
perezju
2015/11/16 10:22:34
maybe just drop to the current directory instead o
rnephew (Reviews Here)
2015/11/16 16:22:01
Done.
| |
390 chartjson = _BASE_CHART.copy() | |
391 else: | |
392 if options.output_dir: | |
393 option_parser.error('Must set --chartjson if you set --output-dir') | |
perezju
2015/11/16 10:22:34
Maybe it's OK to just ignore the value --output-di
rnephew (Reviews Here)
2015/11/16 16:22:01
Done.
| |
394 chartjson = None | |
395 | |
396 # For backward compatibilty with buildbot scripts, treat --so-path as just | |
397 # another file to print the size of. We don't need it for anything special any | |
398 # more. | |
399 if options.so_path: | |
400 files.append(options.so_path) | |
401 | |
402 if not files: | |
403 option_parser.error('Must specify a file') | |
404 | |
405 if options.so_with_symbols_path: | |
406 PrintStaticInitializersCount( | |
407 options.so_with_symbols_path, chartjson=chartjson) | |
408 | |
409 PrintResourceSizes(files, chartjson=chartjson) | |
410 | |
411 for f in files: | |
412 if f.endswith('.apk'): | |
413 PrintApkAnalysis(f, chartjson=chartjson) | |
414 PrintPakAnalysis(f, options.min_pak_resource_size, options.build_type) | |
415 | |
416 if chartjson: | |
417 with open(os.path.join( | |
418 options.output_dir, 'results-chart.json'), 'w') as json_file: | |
419 json.dump(chartjson, json_file) | |
420 | |
421 | |
422 if __name__ == '__main__': | |
423 sys.exit(main(sys.argv)) | |
OLD | NEW |