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