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

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: 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,
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))
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