1 #===- perf-helper.py - Clang Python Bindings -----------------*- python -*--===# 2 # 3 # The LLVM Compiler Infrastructure 4 # 5 # This file is distributed under the University of Illinois Open Source 6 # License. See LICENSE.TXT for details. 7 # 8 #===------------------------------------------------------------------------===# 9 10 from __future__ import print_function 11 12 import sys 13 import os 14 import subprocess 15 import argparse 16 import time 17 import bisect 18 import shlex 19 import tempfile 20 21 test_env = { 'PATH' : os.environ['PATH'] } 22 23 def findFilesWithExtension(path, extension): 24 filenames = [] 25 for root, dirs, files in os.walk(path): 26 for filename in files: 27 if filename.endswith(extension): 28 filenames.append(os.path.join(root, filename)) 29 return filenames 30 31 def clean(args): 32 if len(args) != 2: 33 print('Usage: %s clean <path> <extension>\n' % __file__ + 34 '\tRemoves all files with extension from <path>.') 35 return 1 36 for filename in findFilesWithExtension(args[0], args[1]): 37 os.remove(filename) 38 return 0 39 40 def merge(args): 41 if len(args) != 3: 42 print('Usage: %s clean <llvm-profdata> <output> <path>\n' % __file__ + 43 '\tMerges all profraw files from path into output.') 44 return 1 45 cmd = [args[0], 'merge', '-o', args[1]] 46 cmd.extend(findFilesWithExtension(args[2], "profraw")) 47 subprocess.check_call(cmd) 48 return 0 49 50 def dtrace(args): 51 parser = argparse.ArgumentParser(prog='perf-helper dtrace', 52 description='dtrace wrapper for order file generation') 53 parser.add_argument('--buffer-size', metavar='size', type=int, required=False, 54 default=1, help='dtrace buffer size in MB (default 1)') 55 parser.add_argument('--use-oneshot', required=False, action='store_true', 56 help='Use dtrace\'s oneshot probes') 57 parser.add_argument('--use-ustack', required=False, action='store_true', 58 help='Use dtrace\'s ustack to print function names') 59 parser.add_argument('--cc1', required=False, action='store_true', 60 help='Execute cc1 directly (don\'t profile the driver)') 61 parser.add_argument('cmd', nargs='*', help='') 62 63 # Use python's arg parser to handle all leading option arguments, but pass 64 # everything else through to dtrace 65 first_cmd = next(arg for arg in args if not arg.startswith("--")) 66 last_arg_idx = args.index(first_cmd) 67 68 opts = parser.parse_args(args[:last_arg_idx]) 69 cmd = args[last_arg_idx:] 70 71 if opts.cc1: 72 cmd = get_cc1_command_for_args(cmd, test_env) 73 74 if opts.use_oneshot: 75 target = "oneshot$target:::entry" 76 else: 77 target = "pid$target:::entry" 78 predicate = '%s/probemod=="%s"/' % (target, os.path.basename(args[0])) 79 log_timestamp = 'printf("dtrace-TS: %d\\n", timestamp)' 80 if opts.use_ustack: 81 action = 'ustack(1);' 82 else: 83 action = 'printf("dtrace-Symbol: %s\\n", probefunc);' 84 dtrace_script = "%s { %s; %s }" % (predicate, log_timestamp, action) 85 86 dtrace_args = [] 87 if not os.geteuid() == 0: 88 print( 89 'Script must be run as root, or you must add the following to your sudoers:' 90 + '%%admin ALL=(ALL) NOPASSWD: /usr/sbin/dtrace') 91 dtrace_args.append("sudo") 92 93 dtrace_args.extend(( 94 'dtrace', '-xevaltime=exec', 95 '-xbufsize=%dm' % (opts.buffer_size), 96 '-q', '-n', dtrace_script, 97 '-c', ' '.join(cmd))) 98 99 if sys.platform == "darwin": 100 dtrace_args.append('-xmangled') 101 102 start_time = time.time() 103 104 with open("%d.dtrace" % os.getpid(), "w") as f: 105 subprocess.check_call(dtrace_args, stdout=f, stderr=subprocess.PIPE) 106 107 elapsed = time.time() - start_time 108 print("... data collection took %.4fs" % elapsed) 109 110 return 0 111 112 def get_cc1_command_for_args(cmd, env): 113 # Find the cc1 command used by the compiler. To do this we execute the 114 # compiler with '-###' to figure out what it wants to do. 115 cmd = cmd + ['-###'] 116 cc_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, env=env).strip() 117 cc_commands = [] 118 for ln in cc_output.split('\n'): 119 # Filter out known garbage. 120 if (ln == 'Using built-in specs.' or 121 ln.startswith('Configured with:') or 122 ln.startswith('Target:') or 123 ln.startswith('Thread model:') or 124 ln.startswith('InstalledDir:') or 125 ln.startswith('LLVM Profile Note') or 126 ' version ' in ln): 127 continue 128 cc_commands.append(ln) 129 130 if len(cc_commands) != 1: 131 print('Fatal error: unable to determine cc1 command: %r' % cc_output) 132 exit(1) 133 134 cc1_cmd = shlex.split(cc_commands[0]) 135 if not cc1_cmd: 136 print('Fatal error: unable to determine cc1 command: %r' % cc_output) 137 exit(1) 138 139 return cc1_cmd 140 141 def cc1(args): 142 parser = argparse.ArgumentParser(prog='perf-helper cc1', 143 description='cc1 wrapper for order file generation') 144 parser.add_argument('cmd', nargs='*', help='') 145 146 # Use python's arg parser to handle all leading option arguments, but pass 147 # everything else through to dtrace 148 first_cmd = next(arg for arg in args if not arg.startswith("--")) 149 last_arg_idx = args.index(first_cmd) 150 151 opts = parser.parse_args(args[:last_arg_idx]) 152 cmd = args[last_arg_idx:] 153 154 # clear the profile file env, so that we don't generate profdata 155 # when capturing the cc1 command 156 cc1_env = test_env 157 cc1_env["LLVM_PROFILE_FILE"] = os.devnull 158 cc1_cmd = get_cc1_command_for_args(cmd, cc1_env) 159 160 subprocess.check_call(cc1_cmd) 161 return 0 162 163 def parse_dtrace_symbol_file(path, all_symbols, all_symbols_set, 164 missing_symbols, opts): 165 def fix_mangling(symbol): 166 if sys.platform == "darwin": 167 if symbol[0] != '_' and symbol != 'start': 168 symbol = '_' + symbol 169 return symbol 170 171 def get_symbols_with_prefix(symbol): 172 start_index = bisect.bisect_left(all_symbols, symbol) 173 for s in all_symbols[start_index:]: 174 if not s.startswith(symbol): 175 break 176 yield s 177 178 # Extract the list of symbols from the given file, which is assumed to be 179 # the output of a dtrace run logging either probefunc or ustack(1) and 180 # nothing else. The dtrace -xdemangle option needs to be used. 181 # 182 # This is particular to OS X at the moment, because of the '_' handling. 183 with open(path) as f: 184 current_timestamp = None 185 for ln in f: 186 # Drop leading and trailing whitespace. 187 ln = ln.strip() 188 if not ln.startswith("dtrace-"): 189 continue 190 191 # If this is a timestamp specifier, extract it. 192 if ln.startswith("dtrace-TS: "): 193 _,data = ln.split(': ', 1) 194 if not data.isdigit(): 195 print("warning: unrecognized timestamp line %r, ignoring" % ln, 196 file=sys.stderr) 197 continue 198 current_timestamp = int(data) 199 continue 200 elif ln.startswith("dtrace-Symbol: "): 201 202 _,ln = ln.split(': ', 1) 203 if not ln: 204 continue 205 206 # If there is a '`' in the line, assume it is a ustack(1) entry in 207 # the form of <modulename>`<modulefunc>, where <modulefunc> is never 208 # truncated (but does need the mangling patched). 209 if '`' in ln: 210 yield (current_timestamp, fix_mangling(ln.split('`',1)[1])) 211 continue 212 213 # Otherwise, assume this is a probefunc printout. DTrace on OS X 214 # seems to have a bug where it prints the mangled version of symbols 215 # which aren't C++ mangled. We just add a '_' to anything but start 216 # which doesn't already have a '_'. 217 symbol = fix_mangling(ln) 218 219 # If we don't know all the symbols, or the symbol is one of them, 220 # just return it. 221 if not all_symbols_set or symbol in all_symbols_set: 222 yield (current_timestamp, symbol) 223 continue 224 225 # Otherwise, we have a symbol name which isn't present in the 226 # binary. We assume it is truncated, and try to extend it. 227 228 # Get all the symbols with this prefix. 229 possible_symbols = list(get_symbols_with_prefix(symbol)) 230 if not possible_symbols: 231 continue 232 233 # If we found too many possible symbols, ignore this as a prefix. 234 if len(possible_symbols) > 100: 235 print( "warning: ignoring symbol %r " % symbol + 236 "(no match and too many possible suffixes)", file=sys.stderr) 237 continue 238 239 # Report that we resolved a missing symbol. 240 if opts.show_missing_symbols and symbol not in missing_symbols: 241 print("warning: resolved missing symbol %r" % symbol, file=sys.stderr) 242 missing_symbols.add(symbol) 243 244 # Otherwise, treat all the possible matches as having occurred. This 245 # is an over-approximation, but it should be ok in practice. 246 for s in possible_symbols: 247 yield (current_timestamp, s) 248 249 def uniq(list): 250 seen = set() 251 for item in list: 252 if item not in seen: 253 yield item 254 seen.add(item) 255 256 def form_by_call_order(symbol_lists): 257 # Simply strategy, just return symbols in order of occurrence, even across 258 # multiple runs. 259 return uniq(s for symbols in symbol_lists for s in symbols) 260 261 def form_by_call_order_fair(symbol_lists): 262 # More complicated strategy that tries to respect the call order across all 263 # of the test cases, instead of giving a huge preference to the first test 264 # case. 265 266 # First, uniq all the lists. 267 uniq_lists = [list(uniq(symbols)) for symbols in symbol_lists] 268 269 # Compute the successors for each list. 270 succs = {} 271 for symbols in uniq_lists: 272 for a,b in zip(symbols[:-1], symbols[1:]): 273 succs[a] = items = succs.get(a, []) 274 if b not in items: 275 items.append(b) 276 277 # Emit all the symbols, but make sure to always emit all successors from any 278 # call list whenever we see a symbol. 279 # 280 # There isn't much science here, but this sometimes works better than the 281 # more naive strategy. Then again, sometimes it doesn't so more research is 282 # probably needed. 283 return uniq(s 284 for symbols in symbol_lists 285 for node in symbols 286 for s in ([node] + succs.get(node,[]))) 287 288 def form_by_frequency(symbol_lists): 289 # Form the order file by just putting the most commonly occurring symbols 290 # first. This assumes the data files didn't use the oneshot dtrace method. 291 292 counts = {} 293 for symbols in symbol_lists: 294 for a in symbols: 295 counts[a] = counts.get(a,0) + 1 296 297 by_count = counts.items() 298 by_count.sort(key = lambda (_,n): -n) 299 return [s for s,n in by_count] 300 301 def form_by_random(symbol_lists): 302 # Randomize the symbols. 303 merged_symbols = uniq(s for symbols in symbol_lists 304 for s in symbols) 305 random.shuffle(merged_symbols) 306 return merged_symbols 307 308 def form_by_alphabetical(symbol_lists): 309 # Alphabetize the symbols. 310 merged_symbols = list(set(s for symbols in symbol_lists for s in symbols)) 311 merged_symbols.sort() 312 return merged_symbols 313 314 methods = dict((name[len("form_by_"):],value) 315 for name,value in locals().items() if name.startswith("form_by_")) 316 317 def genOrderFile(args): 318 parser = argparse.ArgumentParser( 319 "%prog [options] <dtrace data file directories>]") 320 parser.add_argument('input', nargs='+', help='') 321 parser.add_argument("--binary", metavar="PATH", type=str, dest="binary_path", 322 help="Path to the binary being ordered (for getting all symbols)", 323 default=None) 324 parser.add_argument("--output", dest="output_path", 325 help="path to output order file to write", default=None, required=True, 326 metavar="PATH") 327 parser.add_argument("--show-missing-symbols", dest="show_missing_symbols", 328 help="show symbols which are 'fixed up' to a valid name (requires --binary)", 329 action="store_true", default=None) 330 parser.add_argument("--output-unordered-symbols", 331 dest="output_unordered_symbols_path", 332 help="write a list of the unordered symbols to PATH (requires --binary)", 333 default=None, metavar="PATH") 334 parser.add_argument("--method", dest="method", 335 help="order file generation method to use", choices=methods.keys(), 336 default='call_order') 337 opts = parser.parse_args(args) 338 339 # If the user gave us a binary, get all the symbols in the binary by 340 # snarfing 'nm' output. 341 if opts.binary_path is not None: 342 output = subprocess.check_output(['nm', '-P', opts.binary_path]) 343 lines = output.split("\n") 344 all_symbols = [ln.split(' ',1)[0] 345 for ln in lines 346 if ln.strip()] 347 print("found %d symbols in binary" % len(all_symbols)) 348 all_symbols.sort() 349 else: 350 all_symbols = [] 351 all_symbols_set = set(all_symbols) 352 353 # Compute the list of input files. 354 input_files = [] 355 for dirname in opts.input: 356 input_files.extend(findFilesWithExtension(dirname, "dtrace")) 357 358 # Load all of the input files. 359 print("loading from %d data files" % len(input_files)) 360 missing_symbols = set() 361 timestamped_symbol_lists = [ 362 list(parse_dtrace_symbol_file(path, all_symbols, all_symbols_set, 363 missing_symbols, opts)) 364 for path in input_files] 365 366 # Reorder each symbol list. 367 symbol_lists = [] 368 for timestamped_symbols_list in timestamped_symbol_lists: 369 timestamped_symbols_list.sort() 370 symbol_lists.append([symbol for _,symbol in timestamped_symbols_list]) 371 372 # Execute the desire order file generation method. 373 method = methods.get(opts.method) 374 result = list(method(symbol_lists)) 375 376 # Report to the user on what percentage of symbols are present in the order 377 # file. 378 num_ordered_symbols = len(result) 379 if all_symbols: 380 print("note: order file contains %d/%d symbols (%.2f%%)" % ( 381 num_ordered_symbols, len(all_symbols), 382 100.*num_ordered_symbols/len(all_symbols)), file=sys.stderr) 383 384 if opts.output_unordered_symbols_path: 385 ordered_symbols_set = set(result) 386 with open(opts.output_unordered_symbols_path, 'w') as f: 387 f.write("\n".join(s for s in all_symbols if s not in ordered_symbols_set)) 388 389 # Write the order file. 390 with open(opts.output_path, 'w') as f: 391 f.write("\n".join(result)) 392 f.write("\n") 393 394 return 0 395 396 commands = {'clean' : clean, 397 'merge' : merge, 398 'dtrace' : dtrace, 399 'cc1' : cc1, 400 'gen-order-file' : genOrderFile} 401 402 def main(): 403 f = commands[sys.argv[1]] 404 sys.exit(f(sys.argv[2:])) 405 406 if __name__ == '__main__': 407 main() 408