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

Side by Side Diff: tools/binary_size/run_binary_size_analysis.py

Issue 119083006: Add tool to help analyze binary size (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: remove extraneous comment from buildfile Created 6 years, 11 months 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
OLDNEW
(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'
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698