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

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