119ea8026Sopenharmony_ci#!/usr/bin/env python3
219ea8026Sopenharmony_ci#
319ea8026Sopenharmony_ci# Script to aggregate and report Linux perf results.
419ea8026Sopenharmony_ci#
519ea8026Sopenharmony_ci# Example:
619ea8026Sopenharmony_ci# ./scripts/perf.py -R -obench.perf ./runners/bench_runner
719ea8026Sopenharmony_ci# ./scripts/perf.py bench.perf -j -Flfs.c -Flfs_util.c -Scycles
819ea8026Sopenharmony_ci#
919ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors.
1019ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause
1119ea8026Sopenharmony_ci#
1219ea8026Sopenharmony_ci
1319ea8026Sopenharmony_ciimport bisect
1419ea8026Sopenharmony_ciimport collections as co
1519ea8026Sopenharmony_ciimport csv
1619ea8026Sopenharmony_ciimport errno
1719ea8026Sopenharmony_ciimport fcntl
1819ea8026Sopenharmony_ciimport functools as ft
1919ea8026Sopenharmony_ciimport itertools as it
2019ea8026Sopenharmony_ciimport math as m
2119ea8026Sopenharmony_ciimport multiprocessing as mp
2219ea8026Sopenharmony_ciimport os
2319ea8026Sopenharmony_ciimport re
2419ea8026Sopenharmony_ciimport shlex
2519ea8026Sopenharmony_ciimport shutil
2619ea8026Sopenharmony_ciimport subprocess as sp
2719ea8026Sopenharmony_ciimport tempfile
2819ea8026Sopenharmony_ciimport zipfile
2919ea8026Sopenharmony_ci
3019ea8026Sopenharmony_ci# TODO support non-zip perf results?
3119ea8026Sopenharmony_ci
3219ea8026Sopenharmony_ci
3319ea8026Sopenharmony_ciPERF_PATH = ['perf']
3419ea8026Sopenharmony_ciPERF_EVENTS = 'cycles,branch-misses,branches,cache-misses,cache-references'
3519ea8026Sopenharmony_ciPERF_FREQ = 100
3619ea8026Sopenharmony_ciOBJDUMP_PATH = ['objdump']
3719ea8026Sopenharmony_ciTHRESHOLD = (0.5, 0.85)
3819ea8026Sopenharmony_ci
3919ea8026Sopenharmony_ci
4019ea8026Sopenharmony_ci# integer fields
4119ea8026Sopenharmony_ciclass Int(co.namedtuple('Int', 'x')):
4219ea8026Sopenharmony_ci    __slots__ = ()
4319ea8026Sopenharmony_ci    def __new__(cls, x=0):
4419ea8026Sopenharmony_ci        if isinstance(x, Int):
4519ea8026Sopenharmony_ci            return x
4619ea8026Sopenharmony_ci        if isinstance(x, str):
4719ea8026Sopenharmony_ci            try:
4819ea8026Sopenharmony_ci                x = int(x, 0)
4919ea8026Sopenharmony_ci            except ValueError:
5019ea8026Sopenharmony_ci                # also accept +-∞ and +-inf
5119ea8026Sopenharmony_ci                if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
5219ea8026Sopenharmony_ci                    x = m.inf
5319ea8026Sopenharmony_ci                elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
5419ea8026Sopenharmony_ci                    x = -m.inf
5519ea8026Sopenharmony_ci                else:
5619ea8026Sopenharmony_ci                    raise
5719ea8026Sopenharmony_ci        assert isinstance(x, int) or m.isinf(x), x
5819ea8026Sopenharmony_ci        return super().__new__(cls, x)
5919ea8026Sopenharmony_ci
6019ea8026Sopenharmony_ci    def __str__(self):
6119ea8026Sopenharmony_ci        if self.x == m.inf:
6219ea8026Sopenharmony_ci            return '∞'
6319ea8026Sopenharmony_ci        elif self.x == -m.inf:
6419ea8026Sopenharmony_ci            return '-∞'
6519ea8026Sopenharmony_ci        else:
6619ea8026Sopenharmony_ci            return str(self.x)
6719ea8026Sopenharmony_ci
6819ea8026Sopenharmony_ci    def __int__(self):
6919ea8026Sopenharmony_ci        assert not m.isinf(self.x)
7019ea8026Sopenharmony_ci        return self.x
7119ea8026Sopenharmony_ci
7219ea8026Sopenharmony_ci    def __float__(self):
7319ea8026Sopenharmony_ci        return float(self.x)
7419ea8026Sopenharmony_ci
7519ea8026Sopenharmony_ci    none = '%7s' % '-'
7619ea8026Sopenharmony_ci    def table(self):
7719ea8026Sopenharmony_ci        return '%7s' % (self,)
7819ea8026Sopenharmony_ci
7919ea8026Sopenharmony_ci    diff_none = '%7s' % '-'
8019ea8026Sopenharmony_ci    diff_table = table
8119ea8026Sopenharmony_ci
8219ea8026Sopenharmony_ci    def diff_diff(self, other):
8319ea8026Sopenharmony_ci        new = self.x if self else 0
8419ea8026Sopenharmony_ci        old = other.x if other else 0
8519ea8026Sopenharmony_ci        diff = new - old
8619ea8026Sopenharmony_ci        if diff == +m.inf:
8719ea8026Sopenharmony_ci            return '%7s' % '+∞'
8819ea8026Sopenharmony_ci        elif diff == -m.inf:
8919ea8026Sopenharmony_ci            return '%7s' % '-∞'
9019ea8026Sopenharmony_ci        else:
9119ea8026Sopenharmony_ci            return '%+7d' % diff
9219ea8026Sopenharmony_ci
9319ea8026Sopenharmony_ci    def ratio(self, other):
9419ea8026Sopenharmony_ci        new = self.x if self else 0
9519ea8026Sopenharmony_ci        old = other.x if other else 0
9619ea8026Sopenharmony_ci        if m.isinf(new) and m.isinf(old):
9719ea8026Sopenharmony_ci            return 0.0
9819ea8026Sopenharmony_ci        elif m.isinf(new):
9919ea8026Sopenharmony_ci            return +m.inf
10019ea8026Sopenharmony_ci        elif m.isinf(old):
10119ea8026Sopenharmony_ci            return -m.inf
10219ea8026Sopenharmony_ci        elif not old and not new:
10319ea8026Sopenharmony_ci            return 0.0
10419ea8026Sopenharmony_ci        elif not old:
10519ea8026Sopenharmony_ci            return 1.0
10619ea8026Sopenharmony_ci        else:
10719ea8026Sopenharmony_ci            return (new-old) / old
10819ea8026Sopenharmony_ci
10919ea8026Sopenharmony_ci    def __add__(self, other):
11019ea8026Sopenharmony_ci        return self.__class__(self.x + other.x)
11119ea8026Sopenharmony_ci
11219ea8026Sopenharmony_ci    def __sub__(self, other):
11319ea8026Sopenharmony_ci        return self.__class__(self.x - other.x)
11419ea8026Sopenharmony_ci
11519ea8026Sopenharmony_ci    def __mul__(self, other):
11619ea8026Sopenharmony_ci        return self.__class__(self.x * other.x)
11719ea8026Sopenharmony_ci
11819ea8026Sopenharmony_ci# perf results
11919ea8026Sopenharmony_ciclass PerfResult(co.namedtuple('PerfResult', [
12019ea8026Sopenharmony_ci        'file', 'function', 'line',
12119ea8026Sopenharmony_ci        'cycles', 'bmisses', 'branches', 'cmisses', 'caches',
12219ea8026Sopenharmony_ci        'children'])):
12319ea8026Sopenharmony_ci    _by = ['file', 'function', 'line']
12419ea8026Sopenharmony_ci    _fields = ['cycles', 'bmisses', 'branches', 'cmisses', 'caches']
12519ea8026Sopenharmony_ci    _sort = ['cycles', 'bmisses', 'cmisses', 'branches', 'caches']
12619ea8026Sopenharmony_ci    _types = {
12719ea8026Sopenharmony_ci        'cycles': Int,
12819ea8026Sopenharmony_ci        'bmisses': Int, 'branches': Int,
12919ea8026Sopenharmony_ci        'cmisses': Int, 'caches': Int}
13019ea8026Sopenharmony_ci
13119ea8026Sopenharmony_ci    __slots__ = ()
13219ea8026Sopenharmony_ci    def __new__(cls, file='', function='', line=0,
13319ea8026Sopenharmony_ci            cycles=0, bmisses=0, branches=0, cmisses=0, caches=0,
13419ea8026Sopenharmony_ci            children=[]):
13519ea8026Sopenharmony_ci        return super().__new__(cls, file, function, int(Int(line)),
13619ea8026Sopenharmony_ci            Int(cycles), Int(bmisses), Int(branches), Int(cmisses), Int(caches),
13719ea8026Sopenharmony_ci            children)
13819ea8026Sopenharmony_ci
13919ea8026Sopenharmony_ci    def __add__(self, other):
14019ea8026Sopenharmony_ci        return PerfResult(self.file, self.function, self.line,
14119ea8026Sopenharmony_ci            self.cycles + other.cycles,
14219ea8026Sopenharmony_ci            self.bmisses + other.bmisses,
14319ea8026Sopenharmony_ci            self.branches + other.branches,
14419ea8026Sopenharmony_ci            self.cmisses + other.cmisses,
14519ea8026Sopenharmony_ci            self.caches + other.caches,
14619ea8026Sopenharmony_ci            self.children + other.children)
14719ea8026Sopenharmony_ci
14819ea8026Sopenharmony_ci
14919ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1):
15019ea8026Sopenharmony_ci    # allow '-' for stdin/stdout
15119ea8026Sopenharmony_ci    if path == '-':
15219ea8026Sopenharmony_ci        if mode == 'r':
15319ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
15419ea8026Sopenharmony_ci        else:
15519ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
15619ea8026Sopenharmony_ci    else:
15719ea8026Sopenharmony_ci        return open(path, mode, buffering)
15819ea8026Sopenharmony_ci
15919ea8026Sopenharmony_ci# run perf as a subprocess, storing measurements into a zip file
16019ea8026Sopenharmony_cidef record(command, *,
16119ea8026Sopenharmony_ci        output=None,
16219ea8026Sopenharmony_ci        perf_freq=PERF_FREQ,
16319ea8026Sopenharmony_ci        perf_period=None,
16419ea8026Sopenharmony_ci        perf_events=PERF_EVENTS,
16519ea8026Sopenharmony_ci        perf_path=PERF_PATH,
16619ea8026Sopenharmony_ci        **args):
16719ea8026Sopenharmony_ci    # create a temporary file for perf to write to, as far as I can tell
16819ea8026Sopenharmony_ci    # this is strictly needed because perf's pipe-mode only works with stdout
16919ea8026Sopenharmony_ci    with tempfile.NamedTemporaryFile('rb') as f:
17019ea8026Sopenharmony_ci        # figure out our perf invocation
17119ea8026Sopenharmony_ci        perf = perf_path + list(filter(None, [
17219ea8026Sopenharmony_ci            'record',
17319ea8026Sopenharmony_ci            '-F%s' % perf_freq
17419ea8026Sopenharmony_ci                if perf_freq is not None
17519ea8026Sopenharmony_ci                and perf_period is None else None,
17619ea8026Sopenharmony_ci            '-c%s' % perf_period
17719ea8026Sopenharmony_ci                if perf_period is not None else None,
17819ea8026Sopenharmony_ci            '-B',
17919ea8026Sopenharmony_ci            '-g',
18019ea8026Sopenharmony_ci            '--all-user',
18119ea8026Sopenharmony_ci            '-e%s' % perf_events,
18219ea8026Sopenharmony_ci            '-o%s' % f.name]))
18319ea8026Sopenharmony_ci
18419ea8026Sopenharmony_ci        # run our command
18519ea8026Sopenharmony_ci        try:
18619ea8026Sopenharmony_ci            if args.get('verbose'):
18719ea8026Sopenharmony_ci                print(' '.join(shlex.quote(c) for c in perf + command))
18819ea8026Sopenharmony_ci            err = sp.call(perf + command, close_fds=False)
18919ea8026Sopenharmony_ci
19019ea8026Sopenharmony_ci        except KeyboardInterrupt:
19119ea8026Sopenharmony_ci            err = errno.EOWNERDEAD
19219ea8026Sopenharmony_ci
19319ea8026Sopenharmony_ci        # synchronize access
19419ea8026Sopenharmony_ci        z = os.open(output, os.O_RDWR | os.O_CREAT)
19519ea8026Sopenharmony_ci        fcntl.flock(z, fcntl.LOCK_EX)
19619ea8026Sopenharmony_ci
19719ea8026Sopenharmony_ci        # copy measurements into our zip file
19819ea8026Sopenharmony_ci        with os.fdopen(z, 'r+b') as z:
19919ea8026Sopenharmony_ci            with zipfile.ZipFile(z, 'a',
20019ea8026Sopenharmony_ci                    compression=zipfile.ZIP_DEFLATED,
20119ea8026Sopenharmony_ci                    compresslevel=1) as z:
20219ea8026Sopenharmony_ci                with z.open('perf.%d' % os.getpid(), 'w') as g:
20319ea8026Sopenharmony_ci                    shutil.copyfileobj(f, g)
20419ea8026Sopenharmony_ci
20519ea8026Sopenharmony_ci    # forward the return code
20619ea8026Sopenharmony_ci    return err
20719ea8026Sopenharmony_ci
20819ea8026Sopenharmony_ci
20919ea8026Sopenharmony_ci# try to only process each dso onceS
21019ea8026Sopenharmony_ci#
21119ea8026Sopenharmony_ci# note this only caches with the non-keyword arguments
21219ea8026Sopenharmony_cidef multiprocessing_cache(f):
21319ea8026Sopenharmony_ci    local_cache = {}
21419ea8026Sopenharmony_ci    manager = mp.Manager()
21519ea8026Sopenharmony_ci    global_cache = manager.dict()
21619ea8026Sopenharmony_ci    lock = mp.Lock()
21719ea8026Sopenharmony_ci
21819ea8026Sopenharmony_ci    def multiprocessing_cache(*args, **kwargs):
21919ea8026Sopenharmony_ci        # check local cache?
22019ea8026Sopenharmony_ci        if args in local_cache:
22119ea8026Sopenharmony_ci            return local_cache[args]
22219ea8026Sopenharmony_ci        # check global cache?
22319ea8026Sopenharmony_ci        with lock:
22419ea8026Sopenharmony_ci            if args in global_cache:
22519ea8026Sopenharmony_ci                v = global_cache[args]
22619ea8026Sopenharmony_ci                local_cache[args] = v
22719ea8026Sopenharmony_ci                return v
22819ea8026Sopenharmony_ci            # fall back to calling the function
22919ea8026Sopenharmony_ci            v = f(*args, **kwargs)
23019ea8026Sopenharmony_ci            global_cache[args] = v
23119ea8026Sopenharmony_ci            local_cache[args] = v
23219ea8026Sopenharmony_ci            return v
23319ea8026Sopenharmony_ci
23419ea8026Sopenharmony_ci    return multiprocessing_cache
23519ea8026Sopenharmony_ci
23619ea8026Sopenharmony_ci@multiprocessing_cache
23719ea8026Sopenharmony_cidef collect_syms_and_lines(obj_path, *,
23819ea8026Sopenharmony_ci        objdump_path=None,
23919ea8026Sopenharmony_ci        **args):
24019ea8026Sopenharmony_ci    symbol_pattern = re.compile(
24119ea8026Sopenharmony_ci        '^(?P<addr>[0-9a-fA-F]+)'
24219ea8026Sopenharmony_ci            '\s+.*'
24319ea8026Sopenharmony_ci            '\s+(?P<size>[0-9a-fA-F]+)'
24419ea8026Sopenharmony_ci            '\s+(?P<name>[^\s]+)\s*$')
24519ea8026Sopenharmony_ci    line_pattern = re.compile(
24619ea8026Sopenharmony_ci        '^\s+(?:'
24719ea8026Sopenharmony_ci            # matches dir/file table
24819ea8026Sopenharmony_ci            '(?P<no>[0-9]+)'
24919ea8026Sopenharmony_ci                '(?:\s+(?P<dir>[0-9]+))?'
25019ea8026Sopenharmony_ci                '\s+.*'
25119ea8026Sopenharmony_ci                '\s+(?P<path>[^\s]+)'
25219ea8026Sopenharmony_ci            # matches line opcodes
25319ea8026Sopenharmony_ci            '|' '\[[^\]]*\]\s+'
25419ea8026Sopenharmony_ci                '(?:'
25519ea8026Sopenharmony_ci                    '(?P<op_special>Special)'
25619ea8026Sopenharmony_ci                    '|' '(?P<op_copy>Copy)'
25719ea8026Sopenharmony_ci                    '|' '(?P<op_end>End of Sequence)'
25819ea8026Sopenharmony_ci                    '|' 'File .*?to (?:entry )?(?P<op_file>\d+)'
25919ea8026Sopenharmony_ci                    '|' 'Line .*?to (?P<op_line>[0-9]+)'
26019ea8026Sopenharmony_ci                    '|' '(?:Address|PC) .*?to (?P<op_addr>[0x0-9a-fA-F]+)'
26119ea8026Sopenharmony_ci                    '|' '.' ')*'
26219ea8026Sopenharmony_ci            ')$', re.IGNORECASE)
26319ea8026Sopenharmony_ci
26419ea8026Sopenharmony_ci    # figure out symbol addresses and file+line ranges
26519ea8026Sopenharmony_ci    syms = {}
26619ea8026Sopenharmony_ci    sym_at = []
26719ea8026Sopenharmony_ci    cmd = objdump_path + ['-t', obj_path]
26819ea8026Sopenharmony_ci    if args.get('verbose'):
26919ea8026Sopenharmony_ci        print(' '.join(shlex.quote(c) for c in cmd))
27019ea8026Sopenharmony_ci    proc = sp.Popen(cmd,
27119ea8026Sopenharmony_ci        stdout=sp.PIPE,
27219ea8026Sopenharmony_ci        stderr=sp.PIPE if not args.get('verbose') else None,
27319ea8026Sopenharmony_ci        universal_newlines=True,
27419ea8026Sopenharmony_ci        errors='replace',
27519ea8026Sopenharmony_ci        close_fds=False)
27619ea8026Sopenharmony_ci    for line in proc.stdout:
27719ea8026Sopenharmony_ci        m = symbol_pattern.match(line)
27819ea8026Sopenharmony_ci        if m:
27919ea8026Sopenharmony_ci            name = m.group('name')
28019ea8026Sopenharmony_ci            addr = int(m.group('addr'), 16)
28119ea8026Sopenharmony_ci            size = int(m.group('size'), 16)
28219ea8026Sopenharmony_ci            # ignore zero-sized symbols
28319ea8026Sopenharmony_ci            if not size:
28419ea8026Sopenharmony_ci                continue
28519ea8026Sopenharmony_ci            # note multiple symbols can share a name
28619ea8026Sopenharmony_ci            if name not in syms:
28719ea8026Sopenharmony_ci                syms[name] = set()
28819ea8026Sopenharmony_ci            syms[name].add((addr, size))
28919ea8026Sopenharmony_ci            sym_at.append((addr, name, size))
29019ea8026Sopenharmony_ci    proc.wait()
29119ea8026Sopenharmony_ci    if proc.returncode != 0:
29219ea8026Sopenharmony_ci        if not args.get('verbose'):
29319ea8026Sopenharmony_ci            for line in proc.stderr:
29419ea8026Sopenharmony_ci                sys.stdout.write(line)
29519ea8026Sopenharmony_ci        # assume no debug-info on failure
29619ea8026Sopenharmony_ci        pass
29719ea8026Sopenharmony_ci
29819ea8026Sopenharmony_ci    # sort and keep largest/first when duplicates
29919ea8026Sopenharmony_ci    sym_at.sort(key=lambda x: (x[0], -x[2], x[1]))
30019ea8026Sopenharmony_ci    sym_at_ = []
30119ea8026Sopenharmony_ci    for addr, name, size in sym_at:
30219ea8026Sopenharmony_ci        if len(sym_at_) == 0 or sym_at_[-1][0] != addr:
30319ea8026Sopenharmony_ci            sym_at_.append((addr, name, size))
30419ea8026Sopenharmony_ci    sym_at = sym_at_
30519ea8026Sopenharmony_ci
30619ea8026Sopenharmony_ci    # state machine for dwarf line numbers, note that objdump's
30719ea8026Sopenharmony_ci    # decodedline seems to have issues with multiple dir/file
30819ea8026Sopenharmony_ci    # tables, which is why we need this
30919ea8026Sopenharmony_ci    lines = []
31019ea8026Sopenharmony_ci    line_at = []
31119ea8026Sopenharmony_ci    dirs = {}
31219ea8026Sopenharmony_ci    files = {}
31319ea8026Sopenharmony_ci    op_file = 1
31419ea8026Sopenharmony_ci    op_line = 1
31519ea8026Sopenharmony_ci    op_addr = 0
31619ea8026Sopenharmony_ci    cmd = objdump_path + ['--dwarf=rawline', obj_path]
31719ea8026Sopenharmony_ci    if args.get('verbose'):
31819ea8026Sopenharmony_ci        print(' '.join(shlex.quote(c) for c in cmd))
31919ea8026Sopenharmony_ci    proc = sp.Popen(cmd,
32019ea8026Sopenharmony_ci        stdout=sp.PIPE,
32119ea8026Sopenharmony_ci        stderr=sp.PIPE if not args.get('verbose') else None,
32219ea8026Sopenharmony_ci        universal_newlines=True,
32319ea8026Sopenharmony_ci        errors='replace',
32419ea8026Sopenharmony_ci        close_fds=False)
32519ea8026Sopenharmony_ci    for line in proc.stdout:
32619ea8026Sopenharmony_ci        m = line_pattern.match(line)
32719ea8026Sopenharmony_ci        if m:
32819ea8026Sopenharmony_ci            if m.group('no') and not m.group('dir'):
32919ea8026Sopenharmony_ci                # found a directory entry
33019ea8026Sopenharmony_ci                dirs[int(m.group('no'))] = m.group('path')
33119ea8026Sopenharmony_ci            elif m.group('no'):
33219ea8026Sopenharmony_ci                # found a file entry
33319ea8026Sopenharmony_ci                dir = int(m.group('dir'))
33419ea8026Sopenharmony_ci                if dir in dirs:
33519ea8026Sopenharmony_ci                    files[int(m.group('no'))] = os.path.join(
33619ea8026Sopenharmony_ci                        dirs[dir],
33719ea8026Sopenharmony_ci                        m.group('path'))
33819ea8026Sopenharmony_ci                else:
33919ea8026Sopenharmony_ci                    files[int(m.group('no'))] = m.group('path')
34019ea8026Sopenharmony_ci            else:
34119ea8026Sopenharmony_ci                # found a state machine update
34219ea8026Sopenharmony_ci                if m.group('op_file'):
34319ea8026Sopenharmony_ci                    op_file = int(m.group('op_file'), 0)
34419ea8026Sopenharmony_ci                if m.group('op_line'):
34519ea8026Sopenharmony_ci                    op_line = int(m.group('op_line'), 0)
34619ea8026Sopenharmony_ci                if m.group('op_addr'):
34719ea8026Sopenharmony_ci                    op_addr = int(m.group('op_addr'), 0)
34819ea8026Sopenharmony_ci
34919ea8026Sopenharmony_ci                if (m.group('op_special')
35019ea8026Sopenharmony_ci                        or m.group('op_copy')
35119ea8026Sopenharmony_ci                        or m.group('op_end')):
35219ea8026Sopenharmony_ci                    file = os.path.abspath(files.get(op_file, '?'))
35319ea8026Sopenharmony_ci                    lines.append((file, op_line, op_addr))
35419ea8026Sopenharmony_ci                    line_at.append((op_addr, file, op_line))
35519ea8026Sopenharmony_ci
35619ea8026Sopenharmony_ci                if m.group('op_end'):
35719ea8026Sopenharmony_ci                    op_file = 1
35819ea8026Sopenharmony_ci                    op_line = 1
35919ea8026Sopenharmony_ci                    op_addr = 0
36019ea8026Sopenharmony_ci    proc.wait()
36119ea8026Sopenharmony_ci    if proc.returncode != 0:
36219ea8026Sopenharmony_ci        if not args.get('verbose'):
36319ea8026Sopenharmony_ci            for line in proc.stderr:
36419ea8026Sopenharmony_ci                sys.stdout.write(line)
36519ea8026Sopenharmony_ci        # assume no debug-info on failure
36619ea8026Sopenharmony_ci        pass
36719ea8026Sopenharmony_ci
36819ea8026Sopenharmony_ci    # sort and keep first when duplicates
36919ea8026Sopenharmony_ci    lines.sort()
37019ea8026Sopenharmony_ci    lines_ = []
37119ea8026Sopenharmony_ci    for file, line, addr in lines:
37219ea8026Sopenharmony_ci        if len(lines_) == 0 or lines_[-1][0] != file or lines[-1][1] != line:
37319ea8026Sopenharmony_ci            lines_.append((file, line, addr))
37419ea8026Sopenharmony_ci    lines = lines_
37519ea8026Sopenharmony_ci
37619ea8026Sopenharmony_ci    # sort and keep first when duplicates
37719ea8026Sopenharmony_ci    line_at.sort()
37819ea8026Sopenharmony_ci    line_at_ = []
37919ea8026Sopenharmony_ci    for addr, file, line in line_at:
38019ea8026Sopenharmony_ci        if len(line_at_) == 0 or line_at_[-1][0] != addr:
38119ea8026Sopenharmony_ci            line_at_.append((addr, file, line))
38219ea8026Sopenharmony_ci    line_at = line_at_
38319ea8026Sopenharmony_ci
38419ea8026Sopenharmony_ci    return syms, sym_at, lines, line_at
38519ea8026Sopenharmony_ci
38619ea8026Sopenharmony_ci
38719ea8026Sopenharmony_cidef collect_decompressed(path, *,
38819ea8026Sopenharmony_ci        perf_path=PERF_PATH,
38919ea8026Sopenharmony_ci        sources=None,
39019ea8026Sopenharmony_ci        everything=False,
39119ea8026Sopenharmony_ci        propagate=0,
39219ea8026Sopenharmony_ci        depth=1,
39319ea8026Sopenharmony_ci        **args):
39419ea8026Sopenharmony_ci    sample_pattern = re.compile(
39519ea8026Sopenharmony_ci        '(?P<comm>\w+)'
39619ea8026Sopenharmony_ci        '\s+(?P<pid>\w+)'
39719ea8026Sopenharmony_ci        '\s+(?P<time>[\w.]+):'
39819ea8026Sopenharmony_ci        '\s*(?P<period>\w+)'
39919ea8026Sopenharmony_ci        '\s+(?P<event>[^:]+):')
40019ea8026Sopenharmony_ci    frame_pattern = re.compile(
40119ea8026Sopenharmony_ci        '\s+(?P<addr>\w+)'
40219ea8026Sopenharmony_ci        '\s+(?P<sym>[^\s\+]+)(?:\+(?P<off>\w+))?'
40319ea8026Sopenharmony_ci        '\s+\((?P<dso>[^\)]+)\)')
40419ea8026Sopenharmony_ci    events = {
40519ea8026Sopenharmony_ci        'cycles':           'cycles',
40619ea8026Sopenharmony_ci        'branch-misses':    'bmisses',
40719ea8026Sopenharmony_ci        'branches':         'branches',
40819ea8026Sopenharmony_ci        'cache-misses':     'cmisses',
40919ea8026Sopenharmony_ci        'cache-references': 'caches'}
41019ea8026Sopenharmony_ci
41119ea8026Sopenharmony_ci    # note perf_path may contain extra args
41219ea8026Sopenharmony_ci    cmd = perf_path + [
41319ea8026Sopenharmony_ci        'script',
41419ea8026Sopenharmony_ci        '-i%s' % path]
41519ea8026Sopenharmony_ci    if args.get('verbose'):
41619ea8026Sopenharmony_ci        print(' '.join(shlex.quote(c) for c in cmd))
41719ea8026Sopenharmony_ci    proc = sp.Popen(cmd,
41819ea8026Sopenharmony_ci        stdout=sp.PIPE,
41919ea8026Sopenharmony_ci        stderr=sp.PIPE if not args.get('verbose') else None,
42019ea8026Sopenharmony_ci        universal_newlines=True,
42119ea8026Sopenharmony_ci        errors='replace',
42219ea8026Sopenharmony_ci        close_fds=False)
42319ea8026Sopenharmony_ci
42419ea8026Sopenharmony_ci    last_filtered = False
42519ea8026Sopenharmony_ci    last_event = ''
42619ea8026Sopenharmony_ci    last_period = 0
42719ea8026Sopenharmony_ci    last_stack = []
42819ea8026Sopenharmony_ci    deltas = co.defaultdict(lambda: {})
42919ea8026Sopenharmony_ci    syms_ = co.defaultdict(lambda: {})
43019ea8026Sopenharmony_ci    at_cache = {}
43119ea8026Sopenharmony_ci    results = {}
43219ea8026Sopenharmony_ci
43319ea8026Sopenharmony_ci    def commit():
43419ea8026Sopenharmony_ci        # tail-recursively propagate measurements
43519ea8026Sopenharmony_ci        for i in range(len(last_stack)):
43619ea8026Sopenharmony_ci            results_ = results
43719ea8026Sopenharmony_ci            for j in reversed(range(i+1)):
43819ea8026Sopenharmony_ci                if i+1-j > depth:
43919ea8026Sopenharmony_ci                    break
44019ea8026Sopenharmony_ci
44119ea8026Sopenharmony_ci                # propagate
44219ea8026Sopenharmony_ci                name = last_stack[j]
44319ea8026Sopenharmony_ci                if name not in results_:
44419ea8026Sopenharmony_ci                    results_[name] = (co.defaultdict(lambda: 0), {})
44519ea8026Sopenharmony_ci                results_[name][0][last_event] += last_period
44619ea8026Sopenharmony_ci
44719ea8026Sopenharmony_ci                # recurse
44819ea8026Sopenharmony_ci                results_ = results_[name][1]
44919ea8026Sopenharmony_ci
45019ea8026Sopenharmony_ci    for line in proc.stdout:
45119ea8026Sopenharmony_ci        # we need to process a lot of data, so wait to use regex as late
45219ea8026Sopenharmony_ci        # as possible
45319ea8026Sopenharmony_ci        if not line.startswith('\t'):
45419ea8026Sopenharmony_ci            if last_filtered:
45519ea8026Sopenharmony_ci                commit()
45619ea8026Sopenharmony_ci            last_filtered = False
45719ea8026Sopenharmony_ci
45819ea8026Sopenharmony_ci            if line:
45919ea8026Sopenharmony_ci                m = sample_pattern.match(line)
46019ea8026Sopenharmony_ci                if m and m.group('event') in events:
46119ea8026Sopenharmony_ci                    last_filtered = True
46219ea8026Sopenharmony_ci                    last_event = m.group('event')
46319ea8026Sopenharmony_ci                    last_period = int(m.group('period'), 0)
46419ea8026Sopenharmony_ci                    last_stack = []
46519ea8026Sopenharmony_ci
46619ea8026Sopenharmony_ci        elif last_filtered:
46719ea8026Sopenharmony_ci            m = frame_pattern.match(line)
46819ea8026Sopenharmony_ci            if m:
46919ea8026Sopenharmony_ci                # filter out internal/kernel functions
47019ea8026Sopenharmony_ci                if not everything and (
47119ea8026Sopenharmony_ci                        m.group('sym').startswith('__')
47219ea8026Sopenharmony_ci                        or m.group('sym').startswith('0')
47319ea8026Sopenharmony_ci                        or m.group('sym').startswith('-')
47419ea8026Sopenharmony_ci                        or m.group('sym').startswith('[')
47519ea8026Sopenharmony_ci                        or m.group('dso').startswith('/usr/lib')):
47619ea8026Sopenharmony_ci                    continue
47719ea8026Sopenharmony_ci
47819ea8026Sopenharmony_ci                dso = m.group('dso')
47919ea8026Sopenharmony_ci                sym = m.group('sym')
48019ea8026Sopenharmony_ci                off = int(m.group('off'), 0) if m.group('off') else 0
48119ea8026Sopenharmony_ci                addr_ = int(m.group('addr'), 16)
48219ea8026Sopenharmony_ci
48319ea8026Sopenharmony_ci                # get the syms/lines for the dso, this is cached
48419ea8026Sopenharmony_ci                syms, sym_at, lines, line_at = collect_syms_and_lines(
48519ea8026Sopenharmony_ci                    dso,
48619ea8026Sopenharmony_ci                    **args)
48719ea8026Sopenharmony_ci
48819ea8026Sopenharmony_ci                # ASLR is tricky, we have symbols+offsets, but static symbols
48919ea8026Sopenharmony_ci                # means we may have multiple options for each symbol.
49019ea8026Sopenharmony_ci                #
49119ea8026Sopenharmony_ci                # To try to solve this, we use previous seen symbols to build
49219ea8026Sopenharmony_ci                # confidence for the correct ASLR delta. This means we may
49319ea8026Sopenharmony_ci                # guess incorrectly for early symbols, but this will only affect
49419ea8026Sopenharmony_ci                # a few samples.
49519ea8026Sopenharmony_ci                if sym in syms:
49619ea8026Sopenharmony_ci                    sym_addr_ = addr_ - off
49719ea8026Sopenharmony_ci
49819ea8026Sopenharmony_ci                    # track possible deltas?
49919ea8026Sopenharmony_ci                    for sym_addr, size in syms[sym]:
50019ea8026Sopenharmony_ci                        delta = sym_addr - sym_addr_
50119ea8026Sopenharmony_ci                        if delta not in deltas[dso]:
50219ea8026Sopenharmony_ci                            deltas[dso][delta] = sum(
50319ea8026Sopenharmony_ci                                abs(a_+delta - a)
50419ea8026Sopenharmony_ci                                for s, (a_, _) in syms_[dso].items()
50519ea8026Sopenharmony_ci                                for a, _ in syms[s])
50619ea8026Sopenharmony_ci                    for delta in deltas[dso].keys():
50719ea8026Sopenharmony_ci                        deltas[dso][delta] += abs(sym_addr_+delta - sym_addr)
50819ea8026Sopenharmony_ci                    syms_[dso][sym] = sym_addr_, size
50919ea8026Sopenharmony_ci
51019ea8026Sopenharmony_ci                    # guess the best delta
51119ea8026Sopenharmony_ci                    delta, _ = min(deltas[dso].items(),
51219ea8026Sopenharmony_ci                        key=lambda x: (x[1], x[0]))
51319ea8026Sopenharmony_ci                    addr = addr_ + delta
51419ea8026Sopenharmony_ci
51519ea8026Sopenharmony_ci                    # cached?
51619ea8026Sopenharmony_ci                    if (dso,addr) in at_cache:
51719ea8026Sopenharmony_ci                        cached = at_cache[(dso,addr)]
51819ea8026Sopenharmony_ci                        if cached is None:
51919ea8026Sopenharmony_ci                            # cache says to skip
52019ea8026Sopenharmony_ci                            continue
52119ea8026Sopenharmony_ci                        file, line = cached
52219ea8026Sopenharmony_ci                    else:
52319ea8026Sopenharmony_ci                        # find file+line
52419ea8026Sopenharmony_ci                        i = bisect.bisect(line_at, addr, key=lambda x: x[0])
52519ea8026Sopenharmony_ci                        if i > 0:
52619ea8026Sopenharmony_ci                            _, file, line = line_at[i-1]
52719ea8026Sopenharmony_ci                        else:
52819ea8026Sopenharmony_ci                            file, line = re.sub('(\.o)?$', '.c', dso, 1), 0
52919ea8026Sopenharmony_ci
53019ea8026Sopenharmony_ci                        # ignore filtered sources
53119ea8026Sopenharmony_ci                        if sources is not None:
53219ea8026Sopenharmony_ci                            if not any(
53319ea8026Sopenharmony_ci                                    os.path.abspath(file) == os.path.abspath(s)
53419ea8026Sopenharmony_ci                                    for s in sources):
53519ea8026Sopenharmony_ci                                at_cache[(dso,addr)] = None
53619ea8026Sopenharmony_ci                                continue
53719ea8026Sopenharmony_ci                        else:
53819ea8026Sopenharmony_ci                            # default to only cwd
53919ea8026Sopenharmony_ci                            if not everything and not os.path.commonpath([
54019ea8026Sopenharmony_ci                                    os.getcwd(),
54119ea8026Sopenharmony_ci                                    os.path.abspath(file)]) == os.getcwd():
54219ea8026Sopenharmony_ci                                at_cache[(dso,addr)] = None
54319ea8026Sopenharmony_ci                                continue
54419ea8026Sopenharmony_ci
54519ea8026Sopenharmony_ci                        # simplify path
54619ea8026Sopenharmony_ci                        if os.path.commonpath([
54719ea8026Sopenharmony_ci                                os.getcwd(),
54819ea8026Sopenharmony_ci                                os.path.abspath(file)]) == os.getcwd():
54919ea8026Sopenharmony_ci                            file = os.path.relpath(file)
55019ea8026Sopenharmony_ci                        else:
55119ea8026Sopenharmony_ci                            file = os.path.abspath(file)
55219ea8026Sopenharmony_ci
55319ea8026Sopenharmony_ci                        at_cache[(dso,addr)] = file, line
55419ea8026Sopenharmony_ci                else:
55519ea8026Sopenharmony_ci                    file, line = re.sub('(\.o)?$', '.c', dso, 1), 0
55619ea8026Sopenharmony_ci
55719ea8026Sopenharmony_ci                last_stack.append((file, sym, line))
55819ea8026Sopenharmony_ci
55919ea8026Sopenharmony_ci                # stop propogating?
56019ea8026Sopenharmony_ci                if propagate and len(last_stack) >= propagate:
56119ea8026Sopenharmony_ci                    commit()
56219ea8026Sopenharmony_ci                    last_filtered = False
56319ea8026Sopenharmony_ci    if last_filtered:
56419ea8026Sopenharmony_ci        commit()
56519ea8026Sopenharmony_ci
56619ea8026Sopenharmony_ci    proc.wait()
56719ea8026Sopenharmony_ci    if proc.returncode != 0:
56819ea8026Sopenharmony_ci        if not args.get('verbose'):
56919ea8026Sopenharmony_ci            for line in proc.stderr:
57019ea8026Sopenharmony_ci                sys.stdout.write(line)
57119ea8026Sopenharmony_ci        sys.exit(-1)
57219ea8026Sopenharmony_ci
57319ea8026Sopenharmony_ci    # rearrange results into result type
57419ea8026Sopenharmony_ci    def to_results(results):
57519ea8026Sopenharmony_ci        results_ = []
57619ea8026Sopenharmony_ci        for name, (r, children) in results.items():
57719ea8026Sopenharmony_ci            results_.append(PerfResult(*name,
57819ea8026Sopenharmony_ci                **{events[k]: v for k, v in r.items()},
57919ea8026Sopenharmony_ci                children=to_results(children)))
58019ea8026Sopenharmony_ci        return results_
58119ea8026Sopenharmony_ci
58219ea8026Sopenharmony_ci    return to_results(results)
58319ea8026Sopenharmony_ci
58419ea8026Sopenharmony_cidef collect_job(path, i, **args):
58519ea8026Sopenharmony_ci    # decompress into a temporary file, this is to work around
58619ea8026Sopenharmony_ci    # some limitations of perf
58719ea8026Sopenharmony_ci    with zipfile.ZipFile(path) as z:
58819ea8026Sopenharmony_ci        with z.open(i) as f:
58919ea8026Sopenharmony_ci            with tempfile.NamedTemporaryFile('wb') as g:
59019ea8026Sopenharmony_ci                shutil.copyfileobj(f, g)
59119ea8026Sopenharmony_ci                g.flush()
59219ea8026Sopenharmony_ci
59319ea8026Sopenharmony_ci                return collect_decompressed(g.name, **args)
59419ea8026Sopenharmony_ci
59519ea8026Sopenharmony_cidef starapply(args):
59619ea8026Sopenharmony_ci    f, args, kwargs = args
59719ea8026Sopenharmony_ci    return f(*args, **kwargs)
59819ea8026Sopenharmony_ci
59919ea8026Sopenharmony_cidef collect(perf_paths, *,
60019ea8026Sopenharmony_ci        jobs=None,
60119ea8026Sopenharmony_ci        **args):
60219ea8026Sopenharmony_ci    # automatic job detection?
60319ea8026Sopenharmony_ci    if jobs == 0:
60419ea8026Sopenharmony_ci        jobs = len(os.sched_getaffinity(0))
60519ea8026Sopenharmony_ci
60619ea8026Sopenharmony_ci    records = []
60719ea8026Sopenharmony_ci    for path in perf_paths:
60819ea8026Sopenharmony_ci        # each .perf file is actually a zip file containing perf files from
60919ea8026Sopenharmony_ci        # multiple runs
61019ea8026Sopenharmony_ci        with zipfile.ZipFile(path) as z:
61119ea8026Sopenharmony_ci            records.extend((path, i) for i in z.infolist())
61219ea8026Sopenharmony_ci
61319ea8026Sopenharmony_ci    # we're dealing with a lot of data but also surprisingly
61419ea8026Sopenharmony_ci    # parallelizable
61519ea8026Sopenharmony_ci    if jobs is not None:
61619ea8026Sopenharmony_ci        results = []
61719ea8026Sopenharmony_ci        with mp.Pool(jobs) as p:
61819ea8026Sopenharmony_ci            for results_ in p.imap_unordered(
61919ea8026Sopenharmony_ci                    starapply,
62019ea8026Sopenharmony_ci                    ((collect_job, (path, i), args) for path, i in records)):
62119ea8026Sopenharmony_ci                results.extend(results_)
62219ea8026Sopenharmony_ci    else:
62319ea8026Sopenharmony_ci        results = []
62419ea8026Sopenharmony_ci        for path, i in records:
62519ea8026Sopenharmony_ci            results.extend(collect_job(path, i, **args))
62619ea8026Sopenharmony_ci
62719ea8026Sopenharmony_ci    return results
62819ea8026Sopenharmony_ci
62919ea8026Sopenharmony_ci
63019ea8026Sopenharmony_cidef fold(Result, results, *,
63119ea8026Sopenharmony_ci        by=None,
63219ea8026Sopenharmony_ci        defines=None,
63319ea8026Sopenharmony_ci        **_):
63419ea8026Sopenharmony_ci    if by is None:
63519ea8026Sopenharmony_ci        by = Result._by
63619ea8026Sopenharmony_ci
63719ea8026Sopenharmony_ci    for k in it.chain(by or [], (k for k, _ in defines or [])):
63819ea8026Sopenharmony_ci        if k not in Result._by and k not in Result._fields:
63919ea8026Sopenharmony_ci            print("error: could not find field %r?" % k)
64019ea8026Sopenharmony_ci            sys.exit(-1)
64119ea8026Sopenharmony_ci
64219ea8026Sopenharmony_ci    # filter by matching defines
64319ea8026Sopenharmony_ci    if defines is not None:
64419ea8026Sopenharmony_ci        results_ = []
64519ea8026Sopenharmony_ci        for r in results:
64619ea8026Sopenharmony_ci            if all(getattr(r, k) in vs for k, vs in defines):
64719ea8026Sopenharmony_ci                results_.append(r)
64819ea8026Sopenharmony_ci        results = results_
64919ea8026Sopenharmony_ci
65019ea8026Sopenharmony_ci    # organize results into conflicts
65119ea8026Sopenharmony_ci    folding = co.OrderedDict()
65219ea8026Sopenharmony_ci    for r in results:
65319ea8026Sopenharmony_ci        name = tuple(getattr(r, k) for k in by)
65419ea8026Sopenharmony_ci        if name not in folding:
65519ea8026Sopenharmony_ci            folding[name] = []
65619ea8026Sopenharmony_ci        folding[name].append(r)
65719ea8026Sopenharmony_ci
65819ea8026Sopenharmony_ci    # merge conflicts
65919ea8026Sopenharmony_ci    folded = []
66019ea8026Sopenharmony_ci    for name, rs in folding.items():
66119ea8026Sopenharmony_ci        folded.append(sum(rs[1:], start=rs[0]))
66219ea8026Sopenharmony_ci
66319ea8026Sopenharmony_ci    # fold recursively
66419ea8026Sopenharmony_ci    folded_ = []
66519ea8026Sopenharmony_ci    for r in folded:
66619ea8026Sopenharmony_ci        folded_.append(r._replace(children=fold(
66719ea8026Sopenharmony_ci            Result, r.children,
66819ea8026Sopenharmony_ci            by=by,
66919ea8026Sopenharmony_ci            defines=defines)))
67019ea8026Sopenharmony_ci    folded = folded_
67119ea8026Sopenharmony_ci
67219ea8026Sopenharmony_ci    return folded
67319ea8026Sopenharmony_ci
67419ea8026Sopenharmony_cidef table(Result, results, diff_results=None, *,
67519ea8026Sopenharmony_ci        by=None,
67619ea8026Sopenharmony_ci        fields=None,
67719ea8026Sopenharmony_ci        sort=None,
67819ea8026Sopenharmony_ci        summary=False,
67919ea8026Sopenharmony_ci        all=False,
68019ea8026Sopenharmony_ci        percent=False,
68119ea8026Sopenharmony_ci        depth=1,
68219ea8026Sopenharmony_ci        **_):
68319ea8026Sopenharmony_ci    all_, all = all, __builtins__.all
68419ea8026Sopenharmony_ci
68519ea8026Sopenharmony_ci    if by is None:
68619ea8026Sopenharmony_ci        by = Result._by
68719ea8026Sopenharmony_ci    if fields is None:
68819ea8026Sopenharmony_ci        fields = Result._fields
68919ea8026Sopenharmony_ci    types = Result._types
69019ea8026Sopenharmony_ci
69119ea8026Sopenharmony_ci    # fold again
69219ea8026Sopenharmony_ci    results = fold(Result, results, by=by)
69319ea8026Sopenharmony_ci    if diff_results is not None:
69419ea8026Sopenharmony_ci        diff_results = fold(Result, diff_results, by=by)
69519ea8026Sopenharmony_ci
69619ea8026Sopenharmony_ci    # organize by name
69719ea8026Sopenharmony_ci    table = {
69819ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
69919ea8026Sopenharmony_ci        for r in results}
70019ea8026Sopenharmony_ci    diff_table = {
70119ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
70219ea8026Sopenharmony_ci        for r in diff_results or []}
70319ea8026Sopenharmony_ci    names = list(table.keys() | diff_table.keys())
70419ea8026Sopenharmony_ci
70519ea8026Sopenharmony_ci    # sort again, now with diff info, note that python's sort is stable
70619ea8026Sopenharmony_ci    names.sort()
70719ea8026Sopenharmony_ci    if diff_results is not None:
70819ea8026Sopenharmony_ci        names.sort(key=lambda n: tuple(
70919ea8026Sopenharmony_ci            types[k].ratio(
71019ea8026Sopenharmony_ci                getattr(table.get(n), k, None),
71119ea8026Sopenharmony_ci                getattr(diff_table.get(n), k, None))
71219ea8026Sopenharmony_ci            for k in fields),
71319ea8026Sopenharmony_ci            reverse=True)
71419ea8026Sopenharmony_ci    if sort:
71519ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
71619ea8026Sopenharmony_ci            names.sort(
71719ea8026Sopenharmony_ci                key=lambda n: tuple(
71819ea8026Sopenharmony_ci                    (getattr(table[n], k),)
71919ea8026Sopenharmony_ci                    if getattr(table.get(n), k, None) is not None else ()
72019ea8026Sopenharmony_ci                    for k in ([k] if k else [
72119ea8026Sopenharmony_ci                        k for k in Result._sort if k in fields])),
72219ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in Result._fields))
72319ea8026Sopenharmony_ci
72419ea8026Sopenharmony_ci
72519ea8026Sopenharmony_ci    # build up our lines
72619ea8026Sopenharmony_ci    lines = []
72719ea8026Sopenharmony_ci
72819ea8026Sopenharmony_ci    # header
72919ea8026Sopenharmony_ci    header = []
73019ea8026Sopenharmony_ci    header.append('%s%s' % (
73119ea8026Sopenharmony_ci        ','.join(by),
73219ea8026Sopenharmony_ci        ' (%d added, %d removed)' % (
73319ea8026Sopenharmony_ci            sum(1 for n in table if n not in diff_table),
73419ea8026Sopenharmony_ci            sum(1 for n in diff_table if n not in table))
73519ea8026Sopenharmony_ci            if diff_results is not None and not percent else '')
73619ea8026Sopenharmony_ci        if not summary else '')
73719ea8026Sopenharmony_ci    if diff_results is None:
73819ea8026Sopenharmony_ci        for k in fields:
73919ea8026Sopenharmony_ci            header.append(k)
74019ea8026Sopenharmony_ci    elif percent:
74119ea8026Sopenharmony_ci        for k in fields:
74219ea8026Sopenharmony_ci            header.append(k)
74319ea8026Sopenharmony_ci    else:
74419ea8026Sopenharmony_ci        for k in fields:
74519ea8026Sopenharmony_ci            header.append('o'+k)
74619ea8026Sopenharmony_ci        for k in fields:
74719ea8026Sopenharmony_ci            header.append('n'+k)
74819ea8026Sopenharmony_ci        for k in fields:
74919ea8026Sopenharmony_ci            header.append('d'+k)
75019ea8026Sopenharmony_ci    header.append('')
75119ea8026Sopenharmony_ci    lines.append(header)
75219ea8026Sopenharmony_ci
75319ea8026Sopenharmony_ci    def table_entry(name, r, diff_r=None, ratios=[]):
75419ea8026Sopenharmony_ci        entry = []
75519ea8026Sopenharmony_ci        entry.append(name)
75619ea8026Sopenharmony_ci        if diff_results is None:
75719ea8026Sopenharmony_ci            for k in fields:
75819ea8026Sopenharmony_ci                entry.append(getattr(r, k).table()
75919ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
76019ea8026Sopenharmony_ci                    else types[k].none)
76119ea8026Sopenharmony_ci        elif percent:
76219ea8026Sopenharmony_ci            for k in fields:
76319ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
76419ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
76519ea8026Sopenharmony_ci                    else types[k].diff_none)
76619ea8026Sopenharmony_ci        else:
76719ea8026Sopenharmony_ci            for k in fields:
76819ea8026Sopenharmony_ci                entry.append(getattr(diff_r, k).diff_table()
76919ea8026Sopenharmony_ci                    if getattr(diff_r, k, None) is not None
77019ea8026Sopenharmony_ci                    else types[k].diff_none)
77119ea8026Sopenharmony_ci            for k in fields:
77219ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
77319ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
77419ea8026Sopenharmony_ci                    else types[k].diff_none)
77519ea8026Sopenharmony_ci            for k in fields:
77619ea8026Sopenharmony_ci                entry.append(types[k].diff_diff(
77719ea8026Sopenharmony_ci                        getattr(r, k, None),
77819ea8026Sopenharmony_ci                        getattr(diff_r, k, None)))
77919ea8026Sopenharmony_ci        if diff_results is None:
78019ea8026Sopenharmony_ci            entry.append('')
78119ea8026Sopenharmony_ci        elif percent:
78219ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
78319ea8026Sopenharmony_ci                '+∞%' if t == +m.inf
78419ea8026Sopenharmony_ci                else '-∞%' if t == -m.inf
78519ea8026Sopenharmony_ci                else '%+.1f%%' % (100*t)
78619ea8026Sopenharmony_ci                for t in ratios))
78719ea8026Sopenharmony_ci        else:
78819ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
78919ea8026Sopenharmony_ci                    '+∞%' if t == +m.inf
79019ea8026Sopenharmony_ci                    else '-∞%' if t == -m.inf
79119ea8026Sopenharmony_ci                    else '%+.1f%%' % (100*t)
79219ea8026Sopenharmony_ci                    for t in ratios
79319ea8026Sopenharmony_ci                    if t)
79419ea8026Sopenharmony_ci                if any(ratios) else '')
79519ea8026Sopenharmony_ci        return entry
79619ea8026Sopenharmony_ci
79719ea8026Sopenharmony_ci    # entries
79819ea8026Sopenharmony_ci    if not summary:
79919ea8026Sopenharmony_ci        for name in names:
80019ea8026Sopenharmony_ci            r = table.get(name)
80119ea8026Sopenharmony_ci            if diff_results is None:
80219ea8026Sopenharmony_ci                diff_r = None
80319ea8026Sopenharmony_ci                ratios = None
80419ea8026Sopenharmony_ci            else:
80519ea8026Sopenharmony_ci                diff_r = diff_table.get(name)
80619ea8026Sopenharmony_ci                ratios = [
80719ea8026Sopenharmony_ci                    types[k].ratio(
80819ea8026Sopenharmony_ci                        getattr(r, k, None),
80919ea8026Sopenharmony_ci                        getattr(diff_r, k, None))
81019ea8026Sopenharmony_ci                    for k in fields]
81119ea8026Sopenharmony_ci                if not all_ and not any(ratios):
81219ea8026Sopenharmony_ci                    continue
81319ea8026Sopenharmony_ci            lines.append(table_entry(name, r, diff_r, ratios))
81419ea8026Sopenharmony_ci
81519ea8026Sopenharmony_ci    # total
81619ea8026Sopenharmony_ci    r = next(iter(fold(Result, results, by=[])), None)
81719ea8026Sopenharmony_ci    if diff_results is None:
81819ea8026Sopenharmony_ci        diff_r = None
81919ea8026Sopenharmony_ci        ratios = None
82019ea8026Sopenharmony_ci    else:
82119ea8026Sopenharmony_ci        diff_r = next(iter(fold(Result, diff_results, by=[])), None)
82219ea8026Sopenharmony_ci        ratios = [
82319ea8026Sopenharmony_ci            types[k].ratio(
82419ea8026Sopenharmony_ci                getattr(r, k, None),
82519ea8026Sopenharmony_ci                getattr(diff_r, k, None))
82619ea8026Sopenharmony_ci            for k in fields]
82719ea8026Sopenharmony_ci    lines.append(table_entry('TOTAL', r, diff_r, ratios))
82819ea8026Sopenharmony_ci
82919ea8026Sopenharmony_ci    # find the best widths, note that column 0 contains the names and column -1
83019ea8026Sopenharmony_ci    # the ratios, so those are handled a bit differently
83119ea8026Sopenharmony_ci    widths = [
83219ea8026Sopenharmony_ci        ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
83319ea8026Sopenharmony_ci        for w, i in zip(
83419ea8026Sopenharmony_ci            it.chain([23], it.repeat(7)),
83519ea8026Sopenharmony_ci            range(len(lines[0])-1))]
83619ea8026Sopenharmony_ci
83719ea8026Sopenharmony_ci    # adjust the name width based on the expected call depth, though
83819ea8026Sopenharmony_ci    # note this doesn't really work with unbounded recursion
83919ea8026Sopenharmony_ci    if not summary and not m.isinf(depth):
84019ea8026Sopenharmony_ci        widths[0] += 4*(depth-1)
84119ea8026Sopenharmony_ci
84219ea8026Sopenharmony_ci    # print the tree recursively
84319ea8026Sopenharmony_ci    print('%-*s  %s%s' % (
84419ea8026Sopenharmony_ci        widths[0], lines[0][0],
84519ea8026Sopenharmony_ci        ' '.join('%*s' % (w, x)
84619ea8026Sopenharmony_ci            for w, x in zip(widths[1:], lines[0][1:-1])),
84719ea8026Sopenharmony_ci        lines[0][-1]))
84819ea8026Sopenharmony_ci
84919ea8026Sopenharmony_ci    if not summary:
85019ea8026Sopenharmony_ci        def recurse(results_, depth_, prefixes=('', '', '', '')):
85119ea8026Sopenharmony_ci            # rebuild our tables at each layer
85219ea8026Sopenharmony_ci            table_ = {
85319ea8026Sopenharmony_ci                ','.join(str(getattr(r, k) or '') for k in by): r
85419ea8026Sopenharmony_ci                for r in results_}
85519ea8026Sopenharmony_ci            names_ = list(table_.keys())
85619ea8026Sopenharmony_ci
85719ea8026Sopenharmony_ci            # sort again at each layer, keep in mind the numbers are
85819ea8026Sopenharmony_ci            # changing as we descend
85919ea8026Sopenharmony_ci            names_.sort()
86019ea8026Sopenharmony_ci            if sort:
86119ea8026Sopenharmony_ci                for k, reverse in reversed(sort):
86219ea8026Sopenharmony_ci                    names_.sort(
86319ea8026Sopenharmony_ci                        key=lambda n: tuple(
86419ea8026Sopenharmony_ci                            (getattr(table_[n], k),)
86519ea8026Sopenharmony_ci                            if getattr(table_.get(n), k, None) is not None
86619ea8026Sopenharmony_ci                            else ()
86719ea8026Sopenharmony_ci                            for k in ([k] if k else [
86819ea8026Sopenharmony_ci                                k for k in Result._sort if k in fields])),
86919ea8026Sopenharmony_ci                        reverse=reverse ^ (not k or k in Result._fields))
87019ea8026Sopenharmony_ci
87119ea8026Sopenharmony_ci            for i, name in enumerate(names_):
87219ea8026Sopenharmony_ci                r = table_[name]
87319ea8026Sopenharmony_ci                is_last = (i == len(names_)-1)
87419ea8026Sopenharmony_ci
87519ea8026Sopenharmony_ci                print('%s%-*s  %s' % (
87619ea8026Sopenharmony_ci                    prefixes[0+is_last],
87719ea8026Sopenharmony_ci                    widths[0] - (
87819ea8026Sopenharmony_ci                        len(prefixes[0+is_last])
87919ea8026Sopenharmony_ci                        if not m.isinf(depth) else 0),
88019ea8026Sopenharmony_ci                    name,
88119ea8026Sopenharmony_ci                    ' '.join('%*s' % (w, x)
88219ea8026Sopenharmony_ci                        for w, x in zip(
88319ea8026Sopenharmony_ci                            widths[1:],
88419ea8026Sopenharmony_ci                            table_entry(name, r)[1:]))))
88519ea8026Sopenharmony_ci
88619ea8026Sopenharmony_ci                # recurse?
88719ea8026Sopenharmony_ci                if depth_ > 1:
88819ea8026Sopenharmony_ci                    recurse(
88919ea8026Sopenharmony_ci                        r.children,
89019ea8026Sopenharmony_ci                        depth_-1,
89119ea8026Sopenharmony_ci                        (prefixes[2+is_last] + "|-> ",
89219ea8026Sopenharmony_ci                         prefixes[2+is_last] + "'-> ",
89319ea8026Sopenharmony_ci                         prefixes[2+is_last] + "|   ",
89419ea8026Sopenharmony_ci                         prefixes[2+is_last] + "    "))
89519ea8026Sopenharmony_ci
89619ea8026Sopenharmony_ci        # we have enough going on with diffing to make the top layer
89719ea8026Sopenharmony_ci        # a special case
89819ea8026Sopenharmony_ci        for name, line in zip(names, lines[1:-1]):
89919ea8026Sopenharmony_ci            print('%-*s  %s%s' % (
90019ea8026Sopenharmony_ci                widths[0], line[0],
90119ea8026Sopenharmony_ci                ' '.join('%*s' % (w, x)
90219ea8026Sopenharmony_ci                    for w, x in zip(widths[1:], line[1:-1])),
90319ea8026Sopenharmony_ci                line[-1]))
90419ea8026Sopenharmony_ci
90519ea8026Sopenharmony_ci            if name in table and depth > 1:
90619ea8026Sopenharmony_ci                recurse(
90719ea8026Sopenharmony_ci                    table[name].children,
90819ea8026Sopenharmony_ci                    depth-1,
90919ea8026Sopenharmony_ci                    ("|-> ",
91019ea8026Sopenharmony_ci                     "'-> ",
91119ea8026Sopenharmony_ci                     "|   ",
91219ea8026Sopenharmony_ci                     "    "))
91319ea8026Sopenharmony_ci
91419ea8026Sopenharmony_ci    print('%-*s  %s%s' % (
91519ea8026Sopenharmony_ci        widths[0], lines[-1][0],
91619ea8026Sopenharmony_ci        ' '.join('%*s' % (w, x)
91719ea8026Sopenharmony_ci            for w, x in zip(widths[1:], lines[-1][1:-1])),
91819ea8026Sopenharmony_ci        lines[-1][-1]))
91919ea8026Sopenharmony_ci
92019ea8026Sopenharmony_ci
92119ea8026Sopenharmony_cidef annotate(Result, results, *,
92219ea8026Sopenharmony_ci        annotate=None,
92319ea8026Sopenharmony_ci        threshold=None,
92419ea8026Sopenharmony_ci        branches=False,
92519ea8026Sopenharmony_ci        caches=False,
92619ea8026Sopenharmony_ci        **args):
92719ea8026Sopenharmony_ci    # figure out the threshold
92819ea8026Sopenharmony_ci    if threshold is None:
92919ea8026Sopenharmony_ci        t0, t1 = THRESHOLD
93019ea8026Sopenharmony_ci    elif len(threshold) == 1:
93119ea8026Sopenharmony_ci        t0, t1 = threshold[0], threshold[0]
93219ea8026Sopenharmony_ci    else:
93319ea8026Sopenharmony_ci        t0, t1 = threshold
93419ea8026Sopenharmony_ci    t0, t1 = min(t0, t1), max(t0, t1)
93519ea8026Sopenharmony_ci
93619ea8026Sopenharmony_ci    if not branches and not caches:
93719ea8026Sopenharmony_ci        tk = 'cycles'
93819ea8026Sopenharmony_ci    elif branches:
93919ea8026Sopenharmony_ci        tk = 'bmisses'
94019ea8026Sopenharmony_ci    else:
94119ea8026Sopenharmony_ci        tk = 'cmisses'
94219ea8026Sopenharmony_ci
94319ea8026Sopenharmony_ci    # find max cycles
94419ea8026Sopenharmony_ci    max_ = max(it.chain((float(getattr(r, tk)) for r in results), [1]))
94519ea8026Sopenharmony_ci
94619ea8026Sopenharmony_ci    for path in co.OrderedDict.fromkeys(r.file for r in results).keys():
94719ea8026Sopenharmony_ci        # flatten to line info
94819ea8026Sopenharmony_ci        results = fold(Result, results, by=['file', 'line'])
94919ea8026Sopenharmony_ci        table = {r.line: r for r in results if r.file == path}
95019ea8026Sopenharmony_ci
95119ea8026Sopenharmony_ci        # calculate spans to show
95219ea8026Sopenharmony_ci        if not annotate:
95319ea8026Sopenharmony_ci            spans = []
95419ea8026Sopenharmony_ci            last = None
95519ea8026Sopenharmony_ci            func = None
95619ea8026Sopenharmony_ci            for line, r in sorted(table.items()):
95719ea8026Sopenharmony_ci                if float(getattr(r, tk)) / max_ >= t0:
95819ea8026Sopenharmony_ci                    if last is not None and line - last.stop <= args['context']:
95919ea8026Sopenharmony_ci                        last = range(
96019ea8026Sopenharmony_ci                            last.start,
96119ea8026Sopenharmony_ci                            line+1+args['context'])
96219ea8026Sopenharmony_ci                    else:
96319ea8026Sopenharmony_ci                        if last is not None:
96419ea8026Sopenharmony_ci                            spans.append((last, func))
96519ea8026Sopenharmony_ci                        last = range(
96619ea8026Sopenharmony_ci                            line-args['context'],
96719ea8026Sopenharmony_ci                            line+1+args['context'])
96819ea8026Sopenharmony_ci                        func = r.function
96919ea8026Sopenharmony_ci            if last is not None:
97019ea8026Sopenharmony_ci                spans.append((last, func))
97119ea8026Sopenharmony_ci
97219ea8026Sopenharmony_ci        with open(path) as f:
97319ea8026Sopenharmony_ci            skipped = False
97419ea8026Sopenharmony_ci            for i, line in enumerate(f):
97519ea8026Sopenharmony_ci                # skip lines not in spans?
97619ea8026Sopenharmony_ci                if not annotate and not any(i+1 in s for s, _ in spans):
97719ea8026Sopenharmony_ci                    skipped = True
97819ea8026Sopenharmony_ci                    continue
97919ea8026Sopenharmony_ci
98019ea8026Sopenharmony_ci                if skipped:
98119ea8026Sopenharmony_ci                    skipped = False
98219ea8026Sopenharmony_ci                    print('%s@@ %s:%d: %s @@%s' % (
98319ea8026Sopenharmony_ci                        '\x1b[36m' if args['color'] else '',
98419ea8026Sopenharmony_ci                        path,
98519ea8026Sopenharmony_ci                        i+1,
98619ea8026Sopenharmony_ci                        next(iter(f for _, f in spans)),
98719ea8026Sopenharmony_ci                        '\x1b[m' if args['color'] else ''))
98819ea8026Sopenharmony_ci
98919ea8026Sopenharmony_ci                # build line
99019ea8026Sopenharmony_ci                if line.endswith('\n'):
99119ea8026Sopenharmony_ci                    line = line[:-1]
99219ea8026Sopenharmony_ci
99319ea8026Sopenharmony_ci                r = table.get(i+1)
99419ea8026Sopenharmony_ci                if r is not None and (
99519ea8026Sopenharmony_ci                        float(r.cycles) > 0
99619ea8026Sopenharmony_ci                        if not branches and not caches
99719ea8026Sopenharmony_ci                        else float(r.bmisses) > 0 or float(r.branches) > 0
99819ea8026Sopenharmony_ci                        if branches
99919ea8026Sopenharmony_ci                        else float(r.cmisses) > 0 or float(r.caches) > 0):
100019ea8026Sopenharmony_ci                    line = '%-*s // %s' % (
100119ea8026Sopenharmony_ci                        args['width'],
100219ea8026Sopenharmony_ci                        line,
100319ea8026Sopenharmony_ci                        '%s cycles' % r.cycles
100419ea8026Sopenharmony_ci                        if not branches and not caches
100519ea8026Sopenharmony_ci                        else '%s bmisses, %s branches' % (r.bmisses, r.branches)
100619ea8026Sopenharmony_ci                        if branches
100719ea8026Sopenharmony_ci                        else '%s cmisses, %s caches' % (r.cmisses, r.caches))
100819ea8026Sopenharmony_ci
100919ea8026Sopenharmony_ci                    if args['color']:
101019ea8026Sopenharmony_ci                        if float(getattr(r, tk)) / max_ >= t1:
101119ea8026Sopenharmony_ci                            line = '\x1b[1;31m%s\x1b[m' % line
101219ea8026Sopenharmony_ci                        elif float(getattr(r, tk)) / max_ >= t0:
101319ea8026Sopenharmony_ci                            line = '\x1b[35m%s\x1b[m' % line
101419ea8026Sopenharmony_ci
101519ea8026Sopenharmony_ci                print(line)
101619ea8026Sopenharmony_ci
101719ea8026Sopenharmony_ci
101819ea8026Sopenharmony_cidef report(perf_paths, *,
101919ea8026Sopenharmony_ci        by=None,
102019ea8026Sopenharmony_ci        fields=None,
102119ea8026Sopenharmony_ci        defines=None,
102219ea8026Sopenharmony_ci        sort=None,
102319ea8026Sopenharmony_ci        branches=False,
102419ea8026Sopenharmony_ci        caches=False,
102519ea8026Sopenharmony_ci        **args):
102619ea8026Sopenharmony_ci    # figure out what color should be
102719ea8026Sopenharmony_ci    if args.get('color') == 'auto':
102819ea8026Sopenharmony_ci        args['color'] = sys.stdout.isatty()
102919ea8026Sopenharmony_ci    elif args.get('color') == 'always':
103019ea8026Sopenharmony_ci        args['color'] = True
103119ea8026Sopenharmony_ci    else:
103219ea8026Sopenharmony_ci        args['color'] = False
103319ea8026Sopenharmony_ci
103419ea8026Sopenharmony_ci    # depth of 0 == m.inf
103519ea8026Sopenharmony_ci    if args.get('depth') == 0:
103619ea8026Sopenharmony_ci        args['depth'] = m.inf
103719ea8026Sopenharmony_ci
103819ea8026Sopenharmony_ci    # find sizes
103919ea8026Sopenharmony_ci    if not args.get('use', None):
104019ea8026Sopenharmony_ci        results = collect(perf_paths, **args)
104119ea8026Sopenharmony_ci    else:
104219ea8026Sopenharmony_ci        results = []
104319ea8026Sopenharmony_ci        with openio(args['use']) as f:
104419ea8026Sopenharmony_ci            reader = csv.DictReader(f, restval='')
104519ea8026Sopenharmony_ci            for r in reader:
104619ea8026Sopenharmony_ci                if not any('perf_'+k in r and r['perf_'+k].strip()
104719ea8026Sopenharmony_ci                        for k in PerfResult._fields):
104819ea8026Sopenharmony_ci                    continue
104919ea8026Sopenharmony_ci                try:
105019ea8026Sopenharmony_ci                    results.append(PerfResult(
105119ea8026Sopenharmony_ci                        **{k: r[k] for k in PerfResult._by
105219ea8026Sopenharmony_ci                            if k in r and r[k].strip()},
105319ea8026Sopenharmony_ci                        **{k: r['perf_'+k] for k in PerfResult._fields
105419ea8026Sopenharmony_ci                            if 'perf_'+k in r and r['perf_'+k].strip()}))
105519ea8026Sopenharmony_ci                except TypeError:
105619ea8026Sopenharmony_ci                    pass
105719ea8026Sopenharmony_ci
105819ea8026Sopenharmony_ci    # fold
105919ea8026Sopenharmony_ci    results = fold(PerfResult, results, by=by, defines=defines)
106019ea8026Sopenharmony_ci
106119ea8026Sopenharmony_ci    # sort, note that python's sort is stable
106219ea8026Sopenharmony_ci    results.sort()
106319ea8026Sopenharmony_ci    if sort:
106419ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
106519ea8026Sopenharmony_ci            results.sort(
106619ea8026Sopenharmony_ci                key=lambda r: tuple(
106719ea8026Sopenharmony_ci                    (getattr(r, k),) if getattr(r, k) is not None else ()
106819ea8026Sopenharmony_ci                    for k in ([k] if k else PerfResult._sort)),
106919ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in PerfResult._fields))
107019ea8026Sopenharmony_ci
107119ea8026Sopenharmony_ci    # write results to CSV
107219ea8026Sopenharmony_ci    if args.get('output'):
107319ea8026Sopenharmony_ci        with openio(args['output'], 'w') as f:
107419ea8026Sopenharmony_ci            writer = csv.DictWriter(f,
107519ea8026Sopenharmony_ci                (by if by is not None else PerfResult._by)
107619ea8026Sopenharmony_ci                + ['perf_'+k for k in (
107719ea8026Sopenharmony_ci                    fields if fields is not None else PerfResult._fields)])
107819ea8026Sopenharmony_ci            writer.writeheader()
107919ea8026Sopenharmony_ci            for r in results:
108019ea8026Sopenharmony_ci                writer.writerow(
108119ea8026Sopenharmony_ci                    {k: getattr(r, k) for k in (
108219ea8026Sopenharmony_ci                        by if by is not None else PerfResult._by)}
108319ea8026Sopenharmony_ci                    | {'perf_'+k: getattr(r, k) for k in (
108419ea8026Sopenharmony_ci                        fields if fields is not None else PerfResult._fields)})
108519ea8026Sopenharmony_ci
108619ea8026Sopenharmony_ci    # find previous results?
108719ea8026Sopenharmony_ci    if args.get('diff'):
108819ea8026Sopenharmony_ci        diff_results = []
108919ea8026Sopenharmony_ci        try:
109019ea8026Sopenharmony_ci            with openio(args['diff']) as f:
109119ea8026Sopenharmony_ci                reader = csv.DictReader(f, restval='')
109219ea8026Sopenharmony_ci                for r in reader:
109319ea8026Sopenharmony_ci                    if not any('perf_'+k in r and r['perf_'+k].strip()
109419ea8026Sopenharmony_ci                            for k in PerfResult._fields):
109519ea8026Sopenharmony_ci                        continue
109619ea8026Sopenharmony_ci                    try:
109719ea8026Sopenharmony_ci                        diff_results.append(PerfResult(
109819ea8026Sopenharmony_ci                            **{k: r[k] for k in PerfResult._by
109919ea8026Sopenharmony_ci                                if k in r and r[k].strip()},
110019ea8026Sopenharmony_ci                            **{k: r['perf_'+k] for k in PerfResult._fields
110119ea8026Sopenharmony_ci                                if 'perf_'+k in r and r['perf_'+k].strip()}))
110219ea8026Sopenharmony_ci                    except TypeError:
110319ea8026Sopenharmony_ci                        pass
110419ea8026Sopenharmony_ci        except FileNotFoundError:
110519ea8026Sopenharmony_ci            pass
110619ea8026Sopenharmony_ci
110719ea8026Sopenharmony_ci        # fold
110819ea8026Sopenharmony_ci        diff_results = fold(PerfResult, diff_results, by=by, defines=defines)
110919ea8026Sopenharmony_ci
111019ea8026Sopenharmony_ci    # print table
111119ea8026Sopenharmony_ci    if not args.get('quiet'):
111219ea8026Sopenharmony_ci        if args.get('annotate') or args.get('threshold'):
111319ea8026Sopenharmony_ci            # annotate sources
111419ea8026Sopenharmony_ci            annotate(PerfResult, results,
111519ea8026Sopenharmony_ci                branches=branches,
111619ea8026Sopenharmony_ci                caches=caches,
111719ea8026Sopenharmony_ci                **args)
111819ea8026Sopenharmony_ci        else:
111919ea8026Sopenharmony_ci            # print table
112019ea8026Sopenharmony_ci            table(PerfResult, results,
112119ea8026Sopenharmony_ci                diff_results if args.get('diff') else None,
112219ea8026Sopenharmony_ci                by=by if by is not None else ['function'],
112319ea8026Sopenharmony_ci                fields=fields if fields is not None
112419ea8026Sopenharmony_ci                    else ['cycles'] if not branches and not caches
112519ea8026Sopenharmony_ci                    else ['bmisses', 'branches'] if branches
112619ea8026Sopenharmony_ci                    else ['cmisses', 'caches'],
112719ea8026Sopenharmony_ci                sort=sort,
112819ea8026Sopenharmony_ci                **args)
112919ea8026Sopenharmony_ci
113019ea8026Sopenharmony_ci
113119ea8026Sopenharmony_cidef main(**args):
113219ea8026Sopenharmony_ci    if args.get('record'):
113319ea8026Sopenharmony_ci        return record(**args)
113419ea8026Sopenharmony_ci    else:
113519ea8026Sopenharmony_ci        return report(**args)
113619ea8026Sopenharmony_ci
113719ea8026Sopenharmony_ci
113819ea8026Sopenharmony_ciif __name__ == "__main__":
113919ea8026Sopenharmony_ci    import argparse
114019ea8026Sopenharmony_ci    import sys
114119ea8026Sopenharmony_ci
114219ea8026Sopenharmony_ci    # bit of a hack, but parse_intermixed_args and REMAINDER are
114319ea8026Sopenharmony_ci    # incompatible, so we need to figure out what we want before running
114419ea8026Sopenharmony_ci    # argparse
114519ea8026Sopenharmony_ci    if '-R' in sys.argv or '--record' in sys.argv:
114619ea8026Sopenharmony_ci        nargs = argparse.REMAINDER
114719ea8026Sopenharmony_ci    else:
114819ea8026Sopenharmony_ci        nargs = '*'
114919ea8026Sopenharmony_ci
115019ea8026Sopenharmony_ci    argparse.ArgumentParser._handle_conflict_ignore = lambda *_: None
115119ea8026Sopenharmony_ci    argparse._ArgumentGroup._handle_conflict_ignore = lambda *_: None
115219ea8026Sopenharmony_ci    parser = argparse.ArgumentParser(
115319ea8026Sopenharmony_ci        description="Aggregate and report Linux perf results.",
115419ea8026Sopenharmony_ci        allow_abbrev=False,
115519ea8026Sopenharmony_ci        conflict_handler='ignore')
115619ea8026Sopenharmony_ci    parser.add_argument(
115719ea8026Sopenharmony_ci        'perf_paths',
115819ea8026Sopenharmony_ci        nargs=nargs,
115919ea8026Sopenharmony_ci        help="Input *.perf files.")
116019ea8026Sopenharmony_ci    parser.add_argument(
116119ea8026Sopenharmony_ci        '-v', '--verbose',
116219ea8026Sopenharmony_ci        action='store_true',
116319ea8026Sopenharmony_ci        help="Output commands that run behind the scenes.")
116419ea8026Sopenharmony_ci    parser.add_argument(
116519ea8026Sopenharmony_ci        '-q', '--quiet',
116619ea8026Sopenharmony_ci        action='store_true',
116719ea8026Sopenharmony_ci        help="Don't show anything, useful with -o.")
116819ea8026Sopenharmony_ci    parser.add_argument(
116919ea8026Sopenharmony_ci        '-o', '--output',
117019ea8026Sopenharmony_ci        help="Specify CSV file to store results.")
117119ea8026Sopenharmony_ci    parser.add_argument(
117219ea8026Sopenharmony_ci        '-u', '--use',
117319ea8026Sopenharmony_ci        help="Don't parse anything, use this CSV file.")
117419ea8026Sopenharmony_ci    parser.add_argument(
117519ea8026Sopenharmony_ci        '-d', '--diff',
117619ea8026Sopenharmony_ci        help="Specify CSV file to diff against.")
117719ea8026Sopenharmony_ci    parser.add_argument(
117819ea8026Sopenharmony_ci        '-a', '--all',
117919ea8026Sopenharmony_ci        action='store_true',
118019ea8026Sopenharmony_ci        help="Show all, not just the ones that changed.")
118119ea8026Sopenharmony_ci    parser.add_argument(
118219ea8026Sopenharmony_ci        '-p', '--percent',
118319ea8026Sopenharmony_ci        action='store_true',
118419ea8026Sopenharmony_ci        help="Only show percentage change, not a full diff.")
118519ea8026Sopenharmony_ci    parser.add_argument(
118619ea8026Sopenharmony_ci        '-b', '--by',
118719ea8026Sopenharmony_ci        action='append',
118819ea8026Sopenharmony_ci        choices=PerfResult._by,
118919ea8026Sopenharmony_ci        help="Group by this field.")
119019ea8026Sopenharmony_ci    parser.add_argument(
119119ea8026Sopenharmony_ci        '-f', '--field',
119219ea8026Sopenharmony_ci        dest='fields',
119319ea8026Sopenharmony_ci        action='append',
119419ea8026Sopenharmony_ci        choices=PerfResult._fields,
119519ea8026Sopenharmony_ci        help="Show this field.")
119619ea8026Sopenharmony_ci    parser.add_argument(
119719ea8026Sopenharmony_ci        '-D', '--define',
119819ea8026Sopenharmony_ci        dest='defines',
119919ea8026Sopenharmony_ci        action='append',
120019ea8026Sopenharmony_ci        type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
120119ea8026Sopenharmony_ci        help="Only include results where this field is this value.")
120219ea8026Sopenharmony_ci    class AppendSort(argparse.Action):
120319ea8026Sopenharmony_ci        def __call__(self, parser, namespace, value, option):
120419ea8026Sopenharmony_ci            if namespace.sort is None:
120519ea8026Sopenharmony_ci                namespace.sort = []
120619ea8026Sopenharmony_ci            namespace.sort.append((value, True if option == '-S' else False))
120719ea8026Sopenharmony_ci    parser.add_argument(
120819ea8026Sopenharmony_ci        '-s', '--sort',
120919ea8026Sopenharmony_ci        nargs='?',
121019ea8026Sopenharmony_ci        action=AppendSort,
121119ea8026Sopenharmony_ci        help="Sort by this field.")
121219ea8026Sopenharmony_ci    parser.add_argument(
121319ea8026Sopenharmony_ci        '-S', '--reverse-sort',
121419ea8026Sopenharmony_ci        nargs='?',
121519ea8026Sopenharmony_ci        action=AppendSort,
121619ea8026Sopenharmony_ci        help="Sort by this field, but backwards.")
121719ea8026Sopenharmony_ci    parser.add_argument(
121819ea8026Sopenharmony_ci        '-Y', '--summary',
121919ea8026Sopenharmony_ci        action='store_true',
122019ea8026Sopenharmony_ci        help="Only show the total.")
122119ea8026Sopenharmony_ci    parser.add_argument(
122219ea8026Sopenharmony_ci        '-F', '--source',
122319ea8026Sopenharmony_ci        dest='sources',
122419ea8026Sopenharmony_ci        action='append',
122519ea8026Sopenharmony_ci        help="Only consider definitions in this file. Defaults to anything "
122619ea8026Sopenharmony_ci            "in the current directory.")
122719ea8026Sopenharmony_ci    parser.add_argument(
122819ea8026Sopenharmony_ci        '--everything',
122919ea8026Sopenharmony_ci        action='store_true',
123019ea8026Sopenharmony_ci        help="Include builtin and libc specific symbols.")
123119ea8026Sopenharmony_ci    parser.add_argument(
123219ea8026Sopenharmony_ci        '--branches',
123319ea8026Sopenharmony_ci        action='store_true',
123419ea8026Sopenharmony_ci        help="Show branches and branch misses.")
123519ea8026Sopenharmony_ci    parser.add_argument(
123619ea8026Sopenharmony_ci        '--caches',
123719ea8026Sopenharmony_ci        action='store_true',
123819ea8026Sopenharmony_ci        help="Show cache accesses and cache misses.")
123919ea8026Sopenharmony_ci    parser.add_argument(
124019ea8026Sopenharmony_ci        '-P', '--propagate',
124119ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
124219ea8026Sopenharmony_ci        help="Depth to propagate samples up the call-stack. 0 propagates up "
124319ea8026Sopenharmony_ci            "to the entry point, 1 does no propagation. Defaults to 0.")
124419ea8026Sopenharmony_ci    parser.add_argument(
124519ea8026Sopenharmony_ci        '-Z', '--depth',
124619ea8026Sopenharmony_ci        nargs='?',
124719ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
124819ea8026Sopenharmony_ci        const=0,
124919ea8026Sopenharmony_ci        help="Depth of function calls to show. 0 shows all calls but may not "
125019ea8026Sopenharmony_ci            "terminate!")
125119ea8026Sopenharmony_ci    parser.add_argument(
125219ea8026Sopenharmony_ci        '-A', '--annotate',
125319ea8026Sopenharmony_ci        action='store_true',
125419ea8026Sopenharmony_ci        help="Show source files annotated with coverage info.")
125519ea8026Sopenharmony_ci    parser.add_argument(
125619ea8026Sopenharmony_ci        '-T', '--threshold',
125719ea8026Sopenharmony_ci        nargs='?',
125819ea8026Sopenharmony_ci        type=lambda x: tuple(float(x) for x in x.split(',')),
125919ea8026Sopenharmony_ci        const=THRESHOLD,
126019ea8026Sopenharmony_ci        help="Show lines with samples above this threshold as a percent of "
126119ea8026Sopenharmony_ci            "all lines. Defaults to %s." % ','.join(str(t) for t in THRESHOLD))
126219ea8026Sopenharmony_ci    parser.add_argument(
126319ea8026Sopenharmony_ci        '-c', '--context',
126419ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
126519ea8026Sopenharmony_ci        default=3,
126619ea8026Sopenharmony_ci        help="Show n additional lines of context. Defaults to 3.")
126719ea8026Sopenharmony_ci    parser.add_argument(
126819ea8026Sopenharmony_ci        '-W', '--width',
126919ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
127019ea8026Sopenharmony_ci        default=80,
127119ea8026Sopenharmony_ci        help="Assume source is styled with this many columns. Defaults to 80.")
127219ea8026Sopenharmony_ci    parser.add_argument(
127319ea8026Sopenharmony_ci        '--color',
127419ea8026Sopenharmony_ci        choices=['never', 'always', 'auto'],
127519ea8026Sopenharmony_ci        default='auto',
127619ea8026Sopenharmony_ci        help="When to use terminal colors. Defaults to 'auto'.")
127719ea8026Sopenharmony_ci    parser.add_argument(
127819ea8026Sopenharmony_ci        '-j', '--jobs',
127919ea8026Sopenharmony_ci        nargs='?',
128019ea8026Sopenharmony_ci        type=lambda x: int(x, 0),
128119ea8026Sopenharmony_ci        const=0,
128219ea8026Sopenharmony_ci        help="Number of processes to use. 0 spawns one process per core.")
128319ea8026Sopenharmony_ci    parser.add_argument(
128419ea8026Sopenharmony_ci        '--perf-path',
128519ea8026Sopenharmony_ci        type=lambda x: x.split(),
128619ea8026Sopenharmony_ci        help="Path to the perf executable, may include flags. "
128719ea8026Sopenharmony_ci            "Defaults to %r." % PERF_PATH)
128819ea8026Sopenharmony_ci    parser.add_argument(
128919ea8026Sopenharmony_ci        '--objdump-path',
129019ea8026Sopenharmony_ci        type=lambda x: x.split(),
129119ea8026Sopenharmony_ci        default=OBJDUMP_PATH,
129219ea8026Sopenharmony_ci        help="Path to the objdump executable, may include flags. "
129319ea8026Sopenharmony_ci            "Defaults to %r." % OBJDUMP_PATH)
129419ea8026Sopenharmony_ci
129519ea8026Sopenharmony_ci    # record flags
129619ea8026Sopenharmony_ci    record_parser = parser.add_argument_group('record options')
129719ea8026Sopenharmony_ci    record_parser.add_argument(
129819ea8026Sopenharmony_ci        'command',
129919ea8026Sopenharmony_ci        nargs=nargs,
130019ea8026Sopenharmony_ci        help="Command to run.")
130119ea8026Sopenharmony_ci    record_parser.add_argument(
130219ea8026Sopenharmony_ci        '-R', '--record',
130319ea8026Sopenharmony_ci        action='store_true',
130419ea8026Sopenharmony_ci        help="Run a command and aggregate perf measurements.")
130519ea8026Sopenharmony_ci    record_parser.add_argument(
130619ea8026Sopenharmony_ci        '-o', '--output',
130719ea8026Sopenharmony_ci        help="Output file. Uses flock to synchronize. This is stored as a "
130819ea8026Sopenharmony_ci            "zip-file of multiple perf results.")
130919ea8026Sopenharmony_ci    record_parser.add_argument(
131019ea8026Sopenharmony_ci        '--perf-freq',
131119ea8026Sopenharmony_ci        help="perf sampling frequency. This is passed directly to perf. "
131219ea8026Sopenharmony_ci            "Defaults to %r." % PERF_FREQ)
131319ea8026Sopenharmony_ci    record_parser.add_argument(
131419ea8026Sopenharmony_ci        '--perf-period',
131519ea8026Sopenharmony_ci        help="perf sampling period. This is passed directly to perf.")
131619ea8026Sopenharmony_ci    record_parser.add_argument(
131719ea8026Sopenharmony_ci        '--perf-events',
131819ea8026Sopenharmony_ci        help="perf events to record. This is passed directly to perf. "
131919ea8026Sopenharmony_ci            "Defaults to %r." % PERF_EVENTS)
132019ea8026Sopenharmony_ci    record_parser.add_argument(
132119ea8026Sopenharmony_ci        '--perf-path',
132219ea8026Sopenharmony_ci        type=lambda x: x.split(),
132319ea8026Sopenharmony_ci        help="Path to the perf executable, may include flags. "
132419ea8026Sopenharmony_ci            "Defaults to %r." % PERF_PATH)
132519ea8026Sopenharmony_ci
132619ea8026Sopenharmony_ci    # avoid intermixed/REMAINDER conflict, see above
132719ea8026Sopenharmony_ci    if nargs == argparse.REMAINDER:
132819ea8026Sopenharmony_ci        args = parser.parse_args()
132919ea8026Sopenharmony_ci    else:
133019ea8026Sopenharmony_ci        args = parser.parse_intermixed_args()
133119ea8026Sopenharmony_ci
133219ea8026Sopenharmony_ci    # perf_paths/command overlap, so need to do some munging here
133319ea8026Sopenharmony_ci    args.command = args.perf_paths
133419ea8026Sopenharmony_ci    if args.record:
133519ea8026Sopenharmony_ci        if not args.command:
133619ea8026Sopenharmony_ci            print('error: no command specified?')
133719ea8026Sopenharmony_ci            sys.exit(-1)
133819ea8026Sopenharmony_ci        if not args.output:
133919ea8026Sopenharmony_ci            print('error: no output file specified?')
134019ea8026Sopenharmony_ci            sys.exit(-1)
134119ea8026Sopenharmony_ci
134219ea8026Sopenharmony_ci    sys.exit(main(**{k: v
134319ea8026Sopenharmony_ci        for k, v in vars(args).items()
134419ea8026Sopenharmony_ci        if v is not None}))
1345