409 lines
14 KiB
Python
409 lines
14 KiB
Python
#===- perf-helper.py - Clang Python Bindings -----------------*- python -*--===#
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
#===------------------------------------------------------------------------===#
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
import sys
|
|
import os
|
|
import subprocess
|
|
import argparse
|
|
import time
|
|
import bisect
|
|
import shlex
|
|
import tempfile
|
|
|
|
test_env = { 'PATH' : os.environ['PATH'] }
|
|
|
|
def findFilesWithExtension(path, extension):
|
|
filenames = []
|
|
for root, dirs, files in os.walk(path):
|
|
for filename in files:
|
|
if filename.endswith(extension):
|
|
filenames.append(os.path.join(root, filename))
|
|
return filenames
|
|
|
|
def clean(args):
|
|
if len(args) != 2:
|
|
print('Usage: %s clean <path> <extension>\n' % __file__ +
|
|
'\tRemoves all files with extension from <path>.')
|
|
return 1
|
|
for filename in findFilesWithExtension(args[0], args[1]):
|
|
os.remove(filename)
|
|
return 0
|
|
|
|
def merge(args):
|
|
if len(args) != 3:
|
|
print('Usage: %s clean <llvm-profdata> <output> <path>\n' % __file__ +
|
|
'\tMerges all profraw files from path into output.')
|
|
return 1
|
|
cmd = [args[0], 'merge', '-o', args[1]]
|
|
cmd.extend(findFilesWithExtension(args[2], "profraw"))
|
|
subprocess.check_call(cmd)
|
|
return 0
|
|
|
|
def dtrace(args):
|
|
parser = argparse.ArgumentParser(prog='perf-helper dtrace',
|
|
description='dtrace wrapper for order file generation')
|
|
parser.add_argument('--buffer-size', metavar='size', type=int, required=False,
|
|
default=1, help='dtrace buffer size in MB (default 1)')
|
|
parser.add_argument('--use-oneshot', required=False, action='store_true',
|
|
help='Use dtrace\'s oneshot probes')
|
|
parser.add_argument('--use-ustack', required=False, action='store_true',
|
|
help='Use dtrace\'s ustack to print function names')
|
|
parser.add_argument('--cc1', required=False, action='store_true',
|
|
help='Execute cc1 directly (don\'t profile the driver)')
|
|
parser.add_argument('cmd', nargs='*', help='')
|
|
|
|
# Use python's arg parser to handle all leading option arguments, but pass
|
|
# everything else through to dtrace
|
|
first_cmd = next(arg for arg in args if not arg.startswith("--"))
|
|
last_arg_idx = args.index(first_cmd)
|
|
|
|
opts = parser.parse_args(args[:last_arg_idx])
|
|
cmd = args[last_arg_idx:]
|
|
|
|
if opts.cc1:
|
|
cmd = get_cc1_command_for_args(cmd, test_env)
|
|
|
|
if opts.use_oneshot:
|
|
target = "oneshot$target:::entry"
|
|
else:
|
|
target = "pid$target:::entry"
|
|
predicate = '%s/probemod=="%s"/' % (target, os.path.basename(cmd[0]))
|
|
log_timestamp = 'printf("dtrace-TS: %d\\n", timestamp)'
|
|
if opts.use_ustack:
|
|
action = 'ustack(1);'
|
|
else:
|
|
action = 'printf("dtrace-Symbol: %s\\n", probefunc);'
|
|
dtrace_script = "%s { %s; %s }" % (predicate, log_timestamp, action)
|
|
|
|
dtrace_args = []
|
|
if not os.geteuid() == 0:
|
|
print(
|
|
'Script must be run as root, or you must add the following to your sudoers:'
|
|
+ '%%admin ALL=(ALL) NOPASSWD: /usr/sbin/dtrace')
|
|
dtrace_args.append("sudo")
|
|
|
|
dtrace_args.extend((
|
|
'dtrace', '-xevaltime=exec',
|
|
'-xbufsize=%dm' % (opts.buffer_size),
|
|
'-q', '-n', dtrace_script,
|
|
'-c', ' '.join(cmd)))
|
|
|
|
if sys.platform == "darwin":
|
|
dtrace_args.append('-xmangled')
|
|
|
|
start_time = time.time()
|
|
|
|
with open("%d.dtrace" % os.getpid(), "w") as f:
|
|
f.write("### Command: %s" % dtrace_args)
|
|
subprocess.check_call(dtrace_args, stdout=f, stderr=subprocess.PIPE)
|
|
|
|
elapsed = time.time() - start_time
|
|
print("... data collection took %.4fs" % elapsed)
|
|
|
|
return 0
|
|
|
|
def get_cc1_command_for_args(cmd, env):
|
|
# Find the cc1 command used by the compiler. To do this we execute the
|
|
# compiler with '-###' to figure out what it wants to do.
|
|
cmd = cmd + ['-###']
|
|
cc_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, env=env, universal_newlines=True).strip()
|
|
cc_commands = []
|
|
for ln in cc_output.split('\n'):
|
|
# Filter out known garbage.
|
|
if (ln == 'Using built-in specs.' or
|
|
ln.startswith('Configured with:') or
|
|
ln.startswith('Target:') or
|
|
ln.startswith('Thread model:') or
|
|
ln.startswith('InstalledDir:') or
|
|
ln.startswith('LLVM Profile Note') or
|
|
ln.startswith(' (in-process)') or
|
|
' version ' in ln):
|
|
continue
|
|
cc_commands.append(ln)
|
|
|
|
if len(cc_commands) != 1:
|
|
print('Fatal error: unable to determine cc1 command: %r' % cc_output)
|
|
exit(1)
|
|
|
|
cc1_cmd = shlex.split(cc_commands[0])
|
|
if not cc1_cmd:
|
|
print('Fatal error: unable to determine cc1 command: %r' % cc_output)
|
|
exit(1)
|
|
|
|
return cc1_cmd
|
|
|
|
def cc1(args):
|
|
parser = argparse.ArgumentParser(prog='perf-helper cc1',
|
|
description='cc1 wrapper for order file generation')
|
|
parser.add_argument('cmd', nargs='*', help='')
|
|
|
|
# Use python's arg parser to handle all leading option arguments, but pass
|
|
# everything else through to dtrace
|
|
first_cmd = next(arg for arg in args if not arg.startswith("--"))
|
|
last_arg_idx = args.index(first_cmd)
|
|
|
|
opts = parser.parse_args(args[:last_arg_idx])
|
|
cmd = args[last_arg_idx:]
|
|
|
|
# clear the profile file env, so that we don't generate profdata
|
|
# when capturing the cc1 command
|
|
cc1_env = test_env
|
|
cc1_env["LLVM_PROFILE_FILE"] = os.devnull
|
|
cc1_cmd = get_cc1_command_for_args(cmd, cc1_env)
|
|
|
|
subprocess.check_call(cc1_cmd)
|
|
return 0
|
|
|
|
def parse_dtrace_symbol_file(path, all_symbols, all_symbols_set,
|
|
missing_symbols, opts):
|
|
def fix_mangling(symbol):
|
|
if sys.platform == "darwin":
|
|
if symbol[0] != '_' and symbol != 'start':
|
|
symbol = '_' + symbol
|
|
return symbol
|
|
|
|
def get_symbols_with_prefix(symbol):
|
|
start_index = bisect.bisect_left(all_symbols, symbol)
|
|
for s in all_symbols[start_index:]:
|
|
if not s.startswith(symbol):
|
|
break
|
|
yield s
|
|
|
|
# Extract the list of symbols from the given file, which is assumed to be
|
|
# the output of a dtrace run logging either probefunc or ustack(1) and
|
|
# nothing else. The dtrace -xdemangle option needs to be used.
|
|
#
|
|
# This is particular to OS X at the moment, because of the '_' handling.
|
|
with open(path) as f:
|
|
current_timestamp = None
|
|
for ln in f:
|
|
# Drop leading and trailing whitespace.
|
|
ln = ln.strip()
|
|
if not ln.startswith("dtrace-"):
|
|
continue
|
|
|
|
# If this is a timestamp specifier, extract it.
|
|
if ln.startswith("dtrace-TS: "):
|
|
_,data = ln.split(': ', 1)
|
|
if not data.isdigit():
|
|
print("warning: unrecognized timestamp line %r, ignoring" % ln,
|
|
file=sys.stderr)
|
|
continue
|
|
current_timestamp = int(data)
|
|
continue
|
|
elif ln.startswith("dtrace-Symbol: "):
|
|
|
|
_,ln = ln.split(': ', 1)
|
|
if not ln:
|
|
continue
|
|
|
|
# If there is a '`' in the line, assume it is a ustack(1) entry in
|
|
# the form of <modulename>`<modulefunc>, where <modulefunc> is never
|
|
# truncated (but does need the mangling patched).
|
|
if '`' in ln:
|
|
yield (current_timestamp, fix_mangling(ln.split('`',1)[1]))
|
|
continue
|
|
|
|
# Otherwise, assume this is a probefunc printout. DTrace on OS X
|
|
# seems to have a bug where it prints the mangled version of symbols
|
|
# which aren't C++ mangled. We just add a '_' to anything but start
|
|
# which doesn't already have a '_'.
|
|
symbol = fix_mangling(ln)
|
|
|
|
# If we don't know all the symbols, or the symbol is one of them,
|
|
# just return it.
|
|
if not all_symbols_set or symbol in all_symbols_set:
|
|
yield (current_timestamp, symbol)
|
|
continue
|
|
|
|
# Otherwise, we have a symbol name which isn't present in the
|
|
# binary. We assume it is truncated, and try to extend it.
|
|
|
|
# Get all the symbols with this prefix.
|
|
possible_symbols = list(get_symbols_with_prefix(symbol))
|
|
if not possible_symbols:
|
|
continue
|
|
|
|
# If we found too many possible symbols, ignore this as a prefix.
|
|
if len(possible_symbols) > 100:
|
|
print( "warning: ignoring symbol %r " % symbol +
|
|
"(no match and too many possible suffixes)", file=sys.stderr)
|
|
continue
|
|
|
|
# Report that we resolved a missing symbol.
|
|
if opts.show_missing_symbols and symbol not in missing_symbols:
|
|
print("warning: resolved missing symbol %r" % symbol, file=sys.stderr)
|
|
missing_symbols.add(symbol)
|
|
|
|
# Otherwise, treat all the possible matches as having occurred. This
|
|
# is an over-approximation, but it should be ok in practice.
|
|
for s in possible_symbols:
|
|
yield (current_timestamp, s)
|
|
|
|
def uniq(list):
|
|
seen = set()
|
|
for item in list:
|
|
if item not in seen:
|
|
yield item
|
|
seen.add(item)
|
|
|
|
def form_by_call_order(symbol_lists):
|
|
# Simply strategy, just return symbols in order of occurrence, even across
|
|
# multiple runs.
|
|
return uniq(s for symbols in symbol_lists for s in symbols)
|
|
|
|
def form_by_call_order_fair(symbol_lists):
|
|
# More complicated strategy that tries to respect the call order across all
|
|
# of the test cases, instead of giving a huge preference to the first test
|
|
# case.
|
|
|
|
# First, uniq all the lists.
|
|
uniq_lists = [list(uniq(symbols)) for symbols in symbol_lists]
|
|
|
|
# Compute the successors for each list.
|
|
succs = {}
|
|
for symbols in uniq_lists:
|
|
for a,b in zip(symbols[:-1], symbols[1:]):
|
|
succs[a] = items = succs.get(a, [])
|
|
if b not in items:
|
|
items.append(b)
|
|
|
|
# Emit all the symbols, but make sure to always emit all successors from any
|
|
# call list whenever we see a symbol.
|
|
#
|
|
# There isn't much science here, but this sometimes works better than the
|
|
# more naive strategy. Then again, sometimes it doesn't so more research is
|
|
# probably needed.
|
|
return uniq(s
|
|
for symbols in symbol_lists
|
|
for node in symbols
|
|
for s in ([node] + succs.get(node,[])))
|
|
|
|
def form_by_frequency(symbol_lists):
|
|
# Form the order file by just putting the most commonly occurring symbols
|
|
# first. This assumes the data files didn't use the oneshot dtrace method.
|
|
|
|
counts = {}
|
|
for symbols in symbol_lists:
|
|
for a in symbols:
|
|
counts[a] = counts.get(a,0) + 1
|
|
|
|
by_count = list(counts.items())
|
|
by_count.sort(key = lambda __n: -__n[1])
|
|
return [s for s,n in by_count]
|
|
|
|
def form_by_random(symbol_lists):
|
|
# Randomize the symbols.
|
|
merged_symbols = uniq(s for symbols in symbol_lists
|
|
for s in symbols)
|
|
random.shuffle(merged_symbols)
|
|
return merged_symbols
|
|
|
|
def form_by_alphabetical(symbol_lists):
|
|
# Alphabetize the symbols.
|
|
merged_symbols = list(set(s for symbols in symbol_lists for s in symbols))
|
|
merged_symbols.sort()
|
|
return merged_symbols
|
|
|
|
methods = dict((name[len("form_by_"):],value)
|
|
for name,value in locals().items() if name.startswith("form_by_"))
|
|
|
|
def genOrderFile(args):
|
|
parser = argparse.ArgumentParser(
|
|
"%prog [options] <dtrace data file directories>]")
|
|
parser.add_argument('input', nargs='+', help='')
|
|
parser.add_argument("--binary", metavar="PATH", type=str, dest="binary_path",
|
|
help="Path to the binary being ordered (for getting all symbols)",
|
|
default=None)
|
|
parser.add_argument("--output", dest="output_path",
|
|
help="path to output order file to write", default=None, required=True,
|
|
metavar="PATH")
|
|
parser.add_argument("--show-missing-symbols", dest="show_missing_symbols",
|
|
help="show symbols which are 'fixed up' to a valid name (requires --binary)",
|
|
action="store_true", default=None)
|
|
parser.add_argument("--output-unordered-symbols",
|
|
dest="output_unordered_symbols_path",
|
|
help="write a list of the unordered symbols to PATH (requires --binary)",
|
|
default=None, metavar="PATH")
|
|
parser.add_argument("--method", dest="method",
|
|
help="order file generation method to use", choices=list(methods.keys()),
|
|
default='call_order')
|
|
opts = parser.parse_args(args)
|
|
|
|
# If the user gave us a binary, get all the symbols in the binary by
|
|
# snarfing 'nm' output.
|
|
if opts.binary_path is not None:
|
|
output = subprocess.check_output(['nm', '-P', opts.binary_path], universal_newlines=True)
|
|
lines = output.split("\n")
|
|
all_symbols = [ln.split(' ',1)[0]
|
|
for ln in lines
|
|
if ln.strip()]
|
|
print("found %d symbols in binary" % len(all_symbols))
|
|
all_symbols.sort()
|
|
else:
|
|
all_symbols = []
|
|
all_symbols_set = set(all_symbols)
|
|
|
|
# Compute the list of input files.
|
|
input_files = []
|
|
for dirname in opts.input:
|
|
input_files.extend(findFilesWithExtension(dirname, "dtrace"))
|
|
|
|
# Load all of the input files.
|
|
print("loading from %d data files" % len(input_files))
|
|
missing_symbols = set()
|
|
timestamped_symbol_lists = [
|
|
list(parse_dtrace_symbol_file(path, all_symbols, all_symbols_set,
|
|
missing_symbols, opts))
|
|
for path in input_files]
|
|
|
|
# Reorder each symbol list.
|
|
symbol_lists = []
|
|
for timestamped_symbols_list in timestamped_symbol_lists:
|
|
timestamped_symbols_list.sort()
|
|
symbol_lists.append([symbol for _,symbol in timestamped_symbols_list])
|
|
|
|
# Execute the desire order file generation method.
|
|
method = methods.get(opts.method)
|
|
result = list(method(symbol_lists))
|
|
|
|
# Report to the user on what percentage of symbols are present in the order
|
|
# file.
|
|
num_ordered_symbols = len(result)
|
|
if all_symbols:
|
|
print("note: order file contains %d/%d symbols (%.2f%%)" % (
|
|
num_ordered_symbols, len(all_symbols),
|
|
100.*num_ordered_symbols/len(all_symbols)), file=sys.stderr)
|
|
|
|
if opts.output_unordered_symbols_path:
|
|
ordered_symbols_set = set(result)
|
|
with open(opts.output_unordered_symbols_path, 'w') as f:
|
|
f.write("\n".join(s for s in all_symbols if s not in ordered_symbols_set))
|
|
|
|
# Write the order file.
|
|
with open(opts.output_path, 'w') as f:
|
|
f.write("\n".join(result))
|
|
f.write("\n")
|
|
|
|
return 0
|
|
|
|
commands = {'clean' : clean,
|
|
'merge' : merge,
|
|
'dtrace' : dtrace,
|
|
'cc1' : cc1,
|
|
'gen-order-file' : genOrderFile}
|
|
|
|
def main():
|
|
f = commands[sys.argv[1]]
|
|
sys.exit(f(sys.argv[2:]))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|