Added perf.py a wrapper around Linux's perf tool for perf sampling

This provides 2 things:

1. perf integration with the bench/test runners - This is a bit tricky
   with perf as it doesn't have its own way to combine perf measurements
   across multiple processes. perf.py works around this by writing
   everything to a zip file, using flock to synchronize. As a plus, free
   compression!

2. Parsing and presentation of perf results in a format consistent with
   the other CSV-based tools. This actually ran into a surprising number of
   issues:

   - We need to process raw events to get the information we want, this
     ends up being a lot of data (~16MiB at 100Hz uncompressed), so we
     paralellize the parsing of each decompressed perf file.

   - perf reports raw addresses post-ASLR. It does provide sym+off which
     is very useful, but to find the source of static functions we need to
     reverse the ASLR by finding the delta the produces the best
     symbol<->addr matches.

   - This isn't related to perf, but decoding dwarf line-numbers is
     really complicated. You basically need to write a tiny VM.

This also turns on perf measurement by default for the bench-runner, but at a
low frequency (100 Hz). This can be decreased or removed in the future
if it causes any slowdown.
This commit is contained in:
Christopher Haster
2022-10-02 18:35:46 -05:00
parent ca66993812
commit 490e1c4616
15 changed files with 2104 additions and 283 deletions

View File

@@ -27,9 +27,13 @@ import time
import toml
RUNNER_PATH = 'runners/test_runner'
RUNNER_PATH = './runners/test_runner'
HEADER_PATH = 'runners/test_runner.h'
GDB_TOOL = ['gdb']
VALGRIND_TOOL = ['valgrind']
PERF_SCRIPT = ['./scripts/perf.py']
def openio(path, mode='r', buffering=-1, nb=False):
if path == '-':
@@ -516,12 +520,25 @@ def find_runner(runner, **args):
# run under valgrind?
if args.get('valgrind'):
cmd[:0] = filter(None, [
'valgrind',
cmd[:0] = args['valgrind_tool'] + [
'--leak-check=full',
'--track-origins=yes',
'--error-exitcode=4',
'-q'])
'-q']
# run under perf?
if args.get('perf'):
cmd[:0] = args['perf_script'] + list(filter(None, [
'-R',
'--perf-freq=%s' % args['perf_freq']
if args.get('perf_freq') else None,
'--perf-period=%s' % args['perf_period']
if args.get('perf_period') else None,
'--perf-events=%s' % args['perf_events']
if args.get('perf_events') else None,
'--perf-tool=%s' % args['perf_tool']
if args.get('perf_tool') else None,
'-o%s' % args['perf']]))
# other context
if args.get('geometry'):
@@ -799,9 +816,9 @@ def run_stage(name, runner_, ids, output_, **args):
try:
line = mpty.readline()
except OSError as e:
if e.errno == errno.EIO:
break
raise
if e.errno != errno.EIO:
raise
break
if not line:
break
last_stdout.append(line)
@@ -1126,24 +1143,24 @@ def run(runner, test_ids=[], **args):
cmd = runner_ + [failure.id]
if args.get('gdb_main'):
cmd[:0] = ['gdb',
cmd[:0] = args['gdb_tool'] + [
'-ex', 'break main',
'-ex', 'run',
'--args']
elif args.get('gdb_case'):
path, lineno = find_path(runner_, failure.id, **args)
cmd[:0] = ['gdb',
cmd[:0] = args['gdb_tool'] + [
'-ex', 'break %s:%d' % (path, lineno),
'-ex', 'run',
'--args']
elif failure.assert_ is not None:
cmd[:0] = ['gdb',
cmd[:0] = args['gdb_tool'] + [
'-ex', 'run',
'-ex', 'frame function raise',
'-ex', 'up 2',
'--args']
else:
cmd[:0] = ['gdb',
cmd[:0] = args['gdb_tool'] + [
'-ex', 'run',
'--args']
@@ -1188,6 +1205,7 @@ if __name__ == "__main__":
argparse._ArgumentGroup._handle_conflict_ignore = lambda *_: None
parser = argparse.ArgumentParser(
description="Build and run tests.",
allow_abbrev=False,
conflict_handler='ignore')
parser.add_argument(
'-v', '--verbose',
@@ -1323,6 +1341,11 @@ if __name__ == "__main__":
action='store_true',
help="Drop into gdb on test failure but stop at the beginning "
"of main.")
test_parser.add_argument(
'--gdb-tool',
type=lambda x: x.split(),
default=GDB_TOOL,
help="Path to gdb tool to use. Defaults to %r." % GDB_TOOL)
test_parser.add_argument(
'--exec',
type=lambda e: e.split(),
@@ -1332,6 +1355,37 @@ if __name__ == "__main__":
action='store_true',
help="Run under Valgrind to find memory errors. Implicitly sets "
"--isolate.")
test_parser.add_argument(
'--valgrind-tool',
type=lambda x: x.split(),
default=VALGRIND_TOOL,
help="Path to Valgrind tool to use. Defaults to %r." % VALGRIND_TOOL)
test_parser.add_argument(
'--perf',
help="Run under Linux's perf to sample performance counters, writing "
"samples to this file.")
test_parser.add_argument(
'--perf-freq',
help="perf sampling frequency. This is passed directly to the perf "
"script.")
test_parser.add_argument(
'--perf-period',
help="perf sampling period. This is passed directly to the perf "
"script.")
test_parser.add_argument(
'--perf-events',
help="perf events to record. This is passed directly to the perf "
"script.")
test_parser.add_argument(
'--perf-script',
type=lambda x: x.split(),
default=PERF_SCRIPT,
help="Path to the perf script to use. Defaults to %r." % PERF_SCRIPT)
test_parser.add_argument(
'--perf-tool',
type=lambda x: x.split(),
help="Path to the perf tool to use. This is passed directly to the "
"perf script")
# compilation flags
comp_parser = parser.add_argument_group('compilation options')
@@ -1356,7 +1410,7 @@ if __name__ == "__main__":
'-o', '--output',
help="Output file.")
# runner + test_ids overlaps test_paths, so we need to do some munging here
# runner/test_paths overlap, so need to do some munging here
args = parser.parse_intermixed_args()
args.test_paths = [' '.join(args.runner or [])] + args.test_ids
args.runner = args.runner or [RUNNER_PATH]