OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/python | |
2 # Copyright 2014 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 """Generate a spatial analysis against an arbitrary library. | |
7 | |
8 To use, build the 'binary_size_java' target. Then run this tool, passing | |
9 in the location of the library to be analyzed along with any other options | |
10 you desire. | |
11 """ | |
12 | |
13 import fileinput | |
14 import optparse | |
15 import os | |
16 import pprint | |
17 import re | |
18 import shutil | |
19 import subprocess | |
20 import sys | |
21 import tempfile | |
22 import json | |
23 | |
24 def format_bytes(bytes): | |
25 """Pretty-print a number of bytes.""" | |
26 if bytes > 1e6: | |
27 bytes = bytes / 1.0e6 | |
28 return '%.1fm' % bytes | |
29 if bytes > 1e3: | |
30 bytes = bytes / 1.0e3 | |
31 return '%.1fk' % bytes | |
32 return str(bytes) | |
33 | |
34 | |
35 def parse_nm(input): | |
bulach
2014/01/07 19:38:30
not sure, is this needed?
Andrew Hayden (chromium.org)
2014/01/08 00:56:45
Yes, the bloat script parses nm to convert the str
bulach
2014/01/08 15:04:00
got it... there's some hidden irony in that whilst
| |
36 """Parse nm output. | |
37 | |
38 Argument: an iterable over lines of nm output. | |
39 | |
40 Yields: (symbol name, symbol type, symbol size, source file path). | |
41 Path may be None if nm couldn't figure out the source file. | |
42 """ | |
43 | |
44 # Match lines with size, symbol, optional location, optional discriminator | |
45 sym_re = re.compile(r'^[0-9a-f]{8} ([0-9a-f]{8}) (.) ([^\t]+)(?:\t(.*):[\d\? ]+)?.*$') | |
46 | |
47 # Match lines with addr but no size. | |
48 addr_re = re.compile(r'^[0-9a-f]{8} (.) ([^\t]+)(?:\t.*)?$') | |
49 # Match lines that don't have an address at all -- typically external symbol s. | |
50 noaddr_re = re.compile(r'^ {8} (.) (.*)$') | |
51 | |
52 for line in input: | |
53 line = line.rstrip() | |
54 match = sym_re.match(line) | |
55 if match: | |
56 size, type, sym = match.groups()[0:3] | |
57 size = int(size, 16) | |
58 type = type.lower() | |
59 if type == 'v': | |
60 type = 'w' # just call them all weak | |
61 if type == 'b': | |
62 continue # skip all BSS for now | |
63 path = match.group(4) | |
64 yield sym, type, size, path | |
65 continue | |
66 match = addr_re.match(line) | |
67 if match: | |
68 type, sym = match.groups()[0:2] | |
69 # No size == we don't care. | |
70 continue | |
71 match = noaddr_re.match(line) | |
72 if match: | |
73 type, sym = match.groups() | |
74 if type in ('U', 'w'): | |
75 # external or weak symbol | |
76 continue | |
77 | |
78 print >>sys.stderr, 'unparsed:', repr(line) | |
79 | |
80 | |
81 def treeify_syms(symbols): | |
82 dirs = {} | |
83 for sym, type, size, path in symbols: | |
84 if path: | |
85 path = os.path.normpath(path) | |
86 if path.startswith('/usr/include'): | |
87 path = path.replace('/usr/include', 'usrinclude') | |
88 elif path.startswith('/'): | |
89 path = path[1:] | |
90 | |
91 parts = None | |
92 # TODO: make segmenting by namespace work. | |
93 if False and '::' in sym: | |
94 if sym.startswith('vtable for '): | |
95 sym = sym[len('vtable for '):] | |
96 parts = sym.split('::') | |
97 parts.append('[vtable]') | |
98 else: | |
99 parts = sym.split('::') | |
100 parts[0] = '::' + parts[0] | |
101 elif path and '/' in path: | |
102 parts = path.split('/') | |
103 | |
104 if parts: | |
105 key = parts.pop() | |
106 tree = dirs | |
107 try: | |
108 for part in parts: | |
109 assert part != '', path | |
110 if part not in tree: | |
111 tree[part] = {} | |
112 tree = tree[part] | |
113 tree[key] = tree.get(key, 0) + size | |
114 except: | |
115 print >>sys.stderr, sym, parts, key | |
116 raise | |
117 else: | |
118 key = 'symbols without paths' | |
119 if key not in dirs: | |
120 dirs[key] = {} | |
121 tree = dirs[key] | |
122 subkey = 'misc' | |
123 if (sym.endswith('::__FUNCTION__') or | |
124 sym.endswith('::__PRETTY_FUNCTION__')): | |
125 subkey = '__FUNCTION__' | |
126 elif sym.startswith('CSWTCH.'): | |
127 subkey = 'CSWTCH' | |
128 elif '::' in sym: | |
129 subkey = sym[0:sym.find('::') + 2] | |
130 #else: | |
131 # print >>sys.stderr, 'unbucketed (no path?):', sym, type, size, path | |
132 tree[subkey] = tree.get(subkey, 0) + size | |
133 return dirs | |
134 | |
135 | |
136 def jsonify_tree(tree, name): | |
137 children = [] | |
138 total = 0 | |
139 files = 0 | |
140 | |
141 for key, val in tree.iteritems(): | |
142 if isinstance(val, dict): | |
143 subtree = jsonify_tree(val, key) | |
144 total += subtree['data']['$area'] | |
145 children.append(subtree) | |
146 else: | |
147 total += val | |
148 children.append({ | |
149 'name': key + ' ' + format_bytes(val), | |
150 'data': { '$area': val } | |
151 }) | |
152 | |
153 children.sort(key=lambda child: -child['data']['$area']) | |
154 | |
155 return { | |
156 'name': name + ' ' + format_bytes(total), | |
157 'data': { | |
158 '$area': total, | |
159 }, | |
160 'children': children, | |
161 } | |
162 | |
163 | |
164 def dump_nm(infile, outfile): | |
165 dirs = treeify_syms(parse_nm(infile)) | |
166 out = sys.stdout | |
167 if outfile is not None: | |
168 out = open(outfile, 'w') | |
169 out.write('var kTree = ' + json.dumps(jsonify_tree(dirs, '/'), indent=2)) | |
170 out.flush() | |
171 if outfile is not None: | |
172 out.close() | |
173 | |
174 | |
175 def run_pa2l(outfile, library, arch, threads, verbose=False): | |
176 """Run a parallel addr2line processing engine to dump and resolve symbols""" | |
177 out_dir = os.getenv('CHROMIUM_OUT_DIR', 'out') | |
178 buildtype = os.getenv('BUILDTYPE', 'Release') | |
179 classpath = out_dir + '/' + buildtype + '/lib.java/binary_size_java.jar' | |
180 cmd = ['java', | |
181 '-classpath', classpath, | |
182 'org.chromium.tools.binary_size.ParallelAddress2Line', | |
183 '--disambiguate', | |
184 '--outfile', outfile, | |
185 '--library', library, | |
186 '--threads', threads] | |
187 if verbose is True: | |
188 cmd.append('--verbose') | |
189 if arch == 'android-arm': | |
190 cmd.extend([ | |
191 '--nm', 'third_party/android_tools/ndk/toolchains/arm-linux- androideabi-4.7/prebuilt/linux-x86_64/bin/arm-linux-androideabi-nm', | |
192 '--addr2line', 'third_party/android_tools/ndk/toolchains/arm -linux-androideabi-4.7/prebuilt/linux-x86_64/bin/arm-linux-androideabi-addr2line ', | |
193 ]) | |
194 elif arch == 'android-mips': | |
195 cmd.extend([ | |
196 '--nm', 'third_party/android_tools/ndk/toolchains/mipsel-lin ux-android-4.7/prebuilt/linux-x86_64/bin/mipsel-linux-android-nm', | |
197 '--addr2line', 'third_party/android_tools/ndk/toolchains/mip sel-linux-android-4.7/prebuilt/linux-x86_64/bin/mipsel-linux-android-addr2line', | |
198 ]) | |
199 elif arch == 'android-x86': | |
200 cmd.extend([ | |
201 '--nm', 'third_party/android_tools/ndk/toolchains/x86-4.7/pr ebuilt/linux-x86_64/bin/i686-linux-android-nm' | |
202 '--addr2line', 'third_party/android_tools/ndk/toolchains/x86 -4.7/prebuilt/linux-x86_64/bin/i686-linux-android-addr2line', | |
203 ]) | |
204 # else, use whatever is in PATH (don't pass --nm or --addr2line) | |
205 | |
206 if verbose: | |
207 print cmd | |
208 | |
209 return_code = subprocess.call(cmd) | |
210 if return_code: | |
211 raise RuntimeError('Failed to run ParallelAddress2Line: returned ' + str (return_code)) | |
212 | |
213 usage="""%prog [options] | |
214 | |
215 Runs a spatial analysis on a given library, looking up the source locations of | |
216 its symbols and calculating how much space each directory, source file, and so | |
217 on is taking. The result is a report that can be used to pinpoint sources of | |
218 large portions of the binary, etceteras. | |
219 | |
220 Under normal circumstances, you only need to pass two arguments, thusly: | |
221 | |
222 %prog --library /path/to/library --destdir /path/to/output | |
223 | |
224 In this mode, the program will dump the symbols from the specified library and | |
225 map those symbols back to source locations, producing a web-based report in the | |
226 specified output directory. | |
227 | |
228 Other options are available via '--help'. | |
229 """ | |
230 parser = optparse.OptionParser(usage=usage) | |
231 parser.add_option('--nm-in', dest='nm_in', metavar='PATH', | |
232 help='if specified, use nm input from <path> instead of ' | |
233 'generating it. Note that source locations should be present ' | |
234 'in the file; i.e., no addr2line symbol lookups will be ' | |
235 'performed when this option is specified. Mutually exclusive ' | |
236 'with --library.') | |
237 parser.add_option('--destdir', metavar='PATH', | |
238 help='write output to the specified directory. An HTML ' | |
239 'report is generated here along with supporting files; any ' | |
240 'existing report will be overwritten.') | |
241 parser.add_option('--library', metavar='PATH', | |
242 help='if specified, process symbols in the library at the ' | |
243 'specified path. Mutually exclusive with --nm-in.') | |
244 parser.add_option('--arch', | |
245 help='the architecture that the library is targeted to. ' | |
246 'Currently supports the following: ' | |
247 'host-native, android-arm, android-mips, android-x86.' | |
248 'the default is host-native. This determines ' | |
249 'what nm/addr2line binaries are used. When host-native is ' | |
250 'chosen (the default), the program will use whichever ' | |
251 'nm/addr2line binaries are on the PATH. This is appropriate ' | |
252 'when you are analyzing a binary by and for your computer. ' | |
253 'This argument is only valid when using --library.') | |
254 parser.add_option('--pa2l-threads', dest='threads', | |
255 help='number of threads to use for the parallel addr2line ' | |
256 'processing pool; defaults to 1. More threads greatly ' | |
257 'improve throughput but eat RAM like popcorn, and take ' | |
258 'several gigabytes each. Start low and ramp this number up ' | |
259 'until your machine begins to struggle with RAM.' | |
260 'This argument is only valid when using --library.') | |
261 parser.add_option('-v', dest='verbose', action='store_true', | |
262 help='be verbose, printing lots of status information.') | |
263 parser.add_option('--nm-out', dest='nm_out', | |
264 help='keep the nm output file, and store it at the specified ' | |
265 'path. This is useful if you want to see the fully processed ' | |
266 'nm output after the symbols have been mapped to source ' | |
267 'locations. By default, a tempfile is used and is deleted ' | |
268 'when the program terminates.' | |
269 'This argument is only valid when using --library.') | |
270 opts, args = parser.parse_args() | |
271 | |
272 if ((not opts.library) and (not opts.nm_in)) or (opts.library and opts.nm_in): | |
273 parser.error('exactly one of --library or --nm-in is required') | |
274 if (opts.nm_in): | |
275 if opts.threads: | |
276 print >> sys.stderr, ('WARNING: --pa2l-threads has no effect ' | |
277 'when used with --nm-in') | |
278 if opts.arch: | |
279 print >> sys.stderr, ('WARNING: --arch has no effect ' | |
280 'when used with --nm-in') | |
281 if not opts.destdir: | |
282 parser.error('--destdir is required argument') | |
283 if not opts.threads: | |
284 opts.threads = 1 | |
285 if not opts.arch: | |
286 opts.arch = 'host-native' | |
287 | |
288 if opts.arch not in ['host-native', 'android-arm', | |
289 'android-mips', 'android-x86']: | |
290 parser.error('arch must be one of ' | |
291 '[host-native,android-arm,android-mips,android-x86]') | |
292 | |
293 nm_in = opts.nm_in | |
294 temp_file = None | |
295 if nm_in is None: | |
296 if opts.nm_out is None: | |
297 temp_file = tempfile.NamedTemporaryFile(prefix='binary_size_nm', delete= False) | |
298 nm_in = temp_file.name | |
299 else: | |
300 nm_in = opts.nm_out | |
301 | |
302 if opts.verbose: | |
303 print 'Running parallel addr2line, dumping symbols to ' + nm_in; | |
304 run_pa2l(outfile=nm_in, | |
305 library=opts.library, | |
306 arch=opts.arch, | |
307 threads=opts.threads, | |
308 verbose=(opts.verbose is True)) | |
309 elif opts.verbose: | |
310 print 'Using nm input from ' + nm_in | |
311 | |
312 if not os.path.exists(opts.destdir): | |
313 os.makedirs(opts.destdir, 0755) | |
314 | |
315 jspath = opts.destdir + '/treemap-dump.js' | |
316 nmfile = open(nm_in, 'r') | |
317 dump_nm(nmfile, jspath) | |
318 if not os.path.exists(opts.destdir + '/webtreemap.js'): | |
319 url = 'https://github.com/martine/webtreemap/archive/gh-pages.zip' | |
320 tmpdir = tempfile.mkdtemp('binary_size') | |
321 try: | |
322 cmd = ['wget', '-O', tmpdir + '/webtreemap.zip', url] | |
323 return_code = subprocess.call(cmd) | |
324 if return_code: | |
325 raise RuntimeError('Failed to download: returned ' + str(return_code )) | |
326 cmd = ['unzip', '-o', tmpdir + '/webtreemap.zip', '-d', tmpdir] | |
327 return_code = subprocess.call(cmd) | |
328 if return_code: | |
329 raise RuntimeError('Failed to unzip: returned ' + str(return_code)) | |
330 | |
331 shutil.move(tmpdir + '/webtreemap-gh-pages/COPYING', opts.destdir) | |
332 shutil.move(tmpdir + '/webtreemap-gh-pages/webtreemap.js', opts.destdir) | |
333 shutil.move(tmpdir + '/webtreemap-gh-pages/webtreemap.css', opts.destdir ) | |
334 finally: | |
335 shutil.rmtree(tmpdir, ignore_errors=True) | |
336 shutil.copy('tools/binary_size/template/index.html', opts.destdir) | |
337 if opts.verbose: | |
338 print 'Report saved to ' + opts.destdir + '/index.html' | |
OLD | NEW |