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