xref: /openbsd-src/gnu/llvm/clang/utils/perf-training/perf-helper.py (revision 12c855180aad702bbcca06e0398d774beeafb155)
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