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

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

Issue 1445633002: [Android] Upstream resource_sizes and check_static_initializers and add chartjson support (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: move typechecks to begining of functions Created 5 years, 1 month 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
(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))
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