119ea8026Sopenharmony_ci#!/usr/bin/env python3
219ea8026Sopenharmony_ci#
319ea8026Sopenharmony_ci# Script to find data size at the function level. Basically just a big wrapper
419ea8026Sopenharmony_ci# around nm with some extra conveniences for comparing builds. Heavily inspired
519ea8026Sopenharmony_ci# by Linux's Bloat-O-Meter.
619ea8026Sopenharmony_ci#
719ea8026Sopenharmony_ci# Example:
819ea8026Sopenharmony_ci# ./scripts/data.py lfs.o lfs_util.o -Ssize
919ea8026Sopenharmony_ci#
1019ea8026Sopenharmony_ci# Copyright (c) 2022, The littlefs authors.
1119ea8026Sopenharmony_ci# Copyright (c) 2020, Arm Limited. All rights reserved.
1219ea8026Sopenharmony_ci# SPDX-License-Identifier: BSD-3-Clause
1319ea8026Sopenharmony_ci#
1419ea8026Sopenharmony_ci
1519ea8026Sopenharmony_ciimport collections as co
1619ea8026Sopenharmony_ciimport csv
1719ea8026Sopenharmony_ciimport difflib
1819ea8026Sopenharmony_ciimport itertools as it
1919ea8026Sopenharmony_ciimport math as m
2019ea8026Sopenharmony_ciimport os
2119ea8026Sopenharmony_ciimport re
2219ea8026Sopenharmony_ciimport shlex
2319ea8026Sopenharmony_ciimport subprocess as sp
2419ea8026Sopenharmony_ci
2519ea8026Sopenharmony_ci
2619ea8026Sopenharmony_ciNM_PATH = ['nm']
2719ea8026Sopenharmony_ciNM_TYPES = 'dDbB'
2819ea8026Sopenharmony_ciOBJDUMP_PATH = ['objdump']
2919ea8026Sopenharmony_ci
3019ea8026Sopenharmony_ci
3119ea8026Sopenharmony_ci# integer fields
3219ea8026Sopenharmony_ciclass Int(co.namedtuple('Int', 'x')):
3319ea8026Sopenharmony_ci    __slots__ = ()
3419ea8026Sopenharmony_ci    def __new__(cls, x=0):
3519ea8026Sopenharmony_ci        if isinstance(x, Int):
3619ea8026Sopenharmony_ci            return x
3719ea8026Sopenharmony_ci        if isinstance(x, str):
3819ea8026Sopenharmony_ci            try:
3919ea8026Sopenharmony_ci                x = int(x, 0)
4019ea8026Sopenharmony_ci            except ValueError:
4119ea8026Sopenharmony_ci                # also accept +-∞ and +-inf
4219ea8026Sopenharmony_ci                if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
4319ea8026Sopenharmony_ci                    x = m.inf
4419ea8026Sopenharmony_ci                elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
4519ea8026Sopenharmony_ci                    x = -m.inf
4619ea8026Sopenharmony_ci                else:
4719ea8026Sopenharmony_ci                    raise
4819ea8026Sopenharmony_ci        assert isinstance(x, int) or m.isinf(x), x
4919ea8026Sopenharmony_ci        return super().__new__(cls, x)
5019ea8026Sopenharmony_ci
5119ea8026Sopenharmony_ci    def __str__(self):
5219ea8026Sopenharmony_ci        if self.x == m.inf:
5319ea8026Sopenharmony_ci            return '∞'
5419ea8026Sopenharmony_ci        elif self.x == -m.inf:
5519ea8026Sopenharmony_ci            return '-∞'
5619ea8026Sopenharmony_ci        else:
5719ea8026Sopenharmony_ci            return str(self.x)
5819ea8026Sopenharmony_ci
5919ea8026Sopenharmony_ci    def __int__(self):
6019ea8026Sopenharmony_ci        assert not m.isinf(self.x)
6119ea8026Sopenharmony_ci        return self.x
6219ea8026Sopenharmony_ci
6319ea8026Sopenharmony_ci    def __float__(self):
6419ea8026Sopenharmony_ci        return float(self.x)
6519ea8026Sopenharmony_ci
6619ea8026Sopenharmony_ci    none = '%7s' % '-'
6719ea8026Sopenharmony_ci    def table(self):
6819ea8026Sopenharmony_ci        return '%7s' % (self,)
6919ea8026Sopenharmony_ci
7019ea8026Sopenharmony_ci    diff_none = '%7s' % '-'
7119ea8026Sopenharmony_ci    diff_table = table
7219ea8026Sopenharmony_ci
7319ea8026Sopenharmony_ci    def diff_diff(self, other):
7419ea8026Sopenharmony_ci        new = self.x if self else 0
7519ea8026Sopenharmony_ci        old = other.x if other else 0
7619ea8026Sopenharmony_ci        diff = new - old
7719ea8026Sopenharmony_ci        if diff == +m.inf:
7819ea8026Sopenharmony_ci            return '%7s' % '+∞'
7919ea8026Sopenharmony_ci        elif diff == -m.inf:
8019ea8026Sopenharmony_ci            return '%7s' % '-∞'
8119ea8026Sopenharmony_ci        else:
8219ea8026Sopenharmony_ci            return '%+7d' % diff
8319ea8026Sopenharmony_ci
8419ea8026Sopenharmony_ci    def ratio(self, other):
8519ea8026Sopenharmony_ci        new = self.x if self else 0
8619ea8026Sopenharmony_ci        old = other.x if other else 0
8719ea8026Sopenharmony_ci        if m.isinf(new) and m.isinf(old):
8819ea8026Sopenharmony_ci            return 0.0
8919ea8026Sopenharmony_ci        elif m.isinf(new):
9019ea8026Sopenharmony_ci            return +m.inf
9119ea8026Sopenharmony_ci        elif m.isinf(old):
9219ea8026Sopenharmony_ci            return -m.inf
9319ea8026Sopenharmony_ci        elif not old and not new:
9419ea8026Sopenharmony_ci            return 0.0
9519ea8026Sopenharmony_ci        elif not old:
9619ea8026Sopenharmony_ci            return 1.0
9719ea8026Sopenharmony_ci        else:
9819ea8026Sopenharmony_ci            return (new-old) / old
9919ea8026Sopenharmony_ci
10019ea8026Sopenharmony_ci    def __add__(self, other):
10119ea8026Sopenharmony_ci        return self.__class__(self.x + other.x)
10219ea8026Sopenharmony_ci
10319ea8026Sopenharmony_ci    def __sub__(self, other):
10419ea8026Sopenharmony_ci        return self.__class__(self.x - other.x)
10519ea8026Sopenharmony_ci
10619ea8026Sopenharmony_ci    def __mul__(self, other):
10719ea8026Sopenharmony_ci        return self.__class__(self.x * other.x)
10819ea8026Sopenharmony_ci
10919ea8026Sopenharmony_ci# data size results
11019ea8026Sopenharmony_ciclass DataResult(co.namedtuple('DataResult', [
11119ea8026Sopenharmony_ci        'file', 'function',
11219ea8026Sopenharmony_ci        'size'])):
11319ea8026Sopenharmony_ci    _by = ['file', 'function']
11419ea8026Sopenharmony_ci    _fields = ['size']
11519ea8026Sopenharmony_ci    _sort = ['size']
11619ea8026Sopenharmony_ci    _types = {'size': Int}
11719ea8026Sopenharmony_ci
11819ea8026Sopenharmony_ci    __slots__ = ()
11919ea8026Sopenharmony_ci    def __new__(cls, file='', function='', size=0):
12019ea8026Sopenharmony_ci        return super().__new__(cls, file, function,
12119ea8026Sopenharmony_ci            Int(size))
12219ea8026Sopenharmony_ci
12319ea8026Sopenharmony_ci    def __add__(self, other):
12419ea8026Sopenharmony_ci        return DataResult(self.file, self.function,
12519ea8026Sopenharmony_ci            self.size + other.size)
12619ea8026Sopenharmony_ci
12719ea8026Sopenharmony_ci
12819ea8026Sopenharmony_cidef openio(path, mode='r', buffering=-1):
12919ea8026Sopenharmony_ci    # allow '-' for stdin/stdout
13019ea8026Sopenharmony_ci    if path == '-':
13119ea8026Sopenharmony_ci        if mode == 'r':
13219ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
13319ea8026Sopenharmony_ci        else:
13419ea8026Sopenharmony_ci            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
13519ea8026Sopenharmony_ci    else:
13619ea8026Sopenharmony_ci        return open(path, mode, buffering)
13719ea8026Sopenharmony_ci
13819ea8026Sopenharmony_cidef collect(obj_paths, *,
13919ea8026Sopenharmony_ci        nm_path=NM_PATH,
14019ea8026Sopenharmony_ci        nm_types=NM_TYPES,
14119ea8026Sopenharmony_ci        objdump_path=OBJDUMP_PATH,
14219ea8026Sopenharmony_ci        sources=None,
14319ea8026Sopenharmony_ci        everything=False,
14419ea8026Sopenharmony_ci        **args):
14519ea8026Sopenharmony_ci    size_pattern = re.compile(
14619ea8026Sopenharmony_ci        '^(?P<size>[0-9a-fA-F]+)' +
14719ea8026Sopenharmony_ci        ' (?P<type>[%s])' % re.escape(nm_types) +
14819ea8026Sopenharmony_ci        ' (?P<func>.+?)$')
14919ea8026Sopenharmony_ci    line_pattern = re.compile(
15019ea8026Sopenharmony_ci        '^\s+(?P<no>[0-9]+)'
15119ea8026Sopenharmony_ci            '(?:\s+(?P<dir>[0-9]+))?'
15219ea8026Sopenharmony_ci            '\s+.*'
15319ea8026Sopenharmony_ci            '\s+(?P<path>[^\s]+)$')
15419ea8026Sopenharmony_ci    info_pattern = re.compile(
15519ea8026Sopenharmony_ci        '^(?:.*(?P<tag>DW_TAG_[a-z_]+).*'
15619ea8026Sopenharmony_ci            '|.*DW_AT_name.*:\s*(?P<name>[^:\s]+)\s*'
15719ea8026Sopenharmony_ci            '|.*DW_AT_decl_file.*:\s*(?P<file>[0-9]+)\s*)$')
15819ea8026Sopenharmony_ci
15919ea8026Sopenharmony_ci    results = []
16019ea8026Sopenharmony_ci    for path in obj_paths:
16119ea8026Sopenharmony_ci        # guess the source, if we have debug-info we'll replace this later
16219ea8026Sopenharmony_ci        file = re.sub('(\.o)?$', '.c', path, 1)
16319ea8026Sopenharmony_ci
16419ea8026Sopenharmony_ci        # find symbol sizes
16519ea8026Sopenharmony_ci        results_ = []
16619ea8026Sopenharmony_ci        # note nm-path may contain extra args
16719ea8026Sopenharmony_ci        cmd = nm_path + ['--size-sort', path]
16819ea8026Sopenharmony_ci        if args.get('verbose'):
16919ea8026Sopenharmony_ci            print(' '.join(shlex.quote(c) for c in cmd))
17019ea8026Sopenharmony_ci        proc = sp.Popen(cmd,
17119ea8026Sopenharmony_ci            stdout=sp.PIPE,
17219ea8026Sopenharmony_ci            stderr=sp.PIPE if not args.get('verbose') else None,
17319ea8026Sopenharmony_ci            universal_newlines=True,
17419ea8026Sopenharmony_ci            errors='replace',
17519ea8026Sopenharmony_ci            close_fds=False)
17619ea8026Sopenharmony_ci        for line in proc.stdout:
17719ea8026Sopenharmony_ci            m = size_pattern.match(line)
17819ea8026Sopenharmony_ci            if m:
17919ea8026Sopenharmony_ci                func = m.group('func')
18019ea8026Sopenharmony_ci                # discard internal functions
18119ea8026Sopenharmony_ci                if not everything and func.startswith('__'):
18219ea8026Sopenharmony_ci                    continue
18319ea8026Sopenharmony_ci                results_.append(DataResult(
18419ea8026Sopenharmony_ci                    file, func,
18519ea8026Sopenharmony_ci                    int(m.group('size'), 16)))
18619ea8026Sopenharmony_ci        proc.wait()
18719ea8026Sopenharmony_ci        if proc.returncode != 0:
18819ea8026Sopenharmony_ci            if not args.get('verbose'):
18919ea8026Sopenharmony_ci                for line in proc.stderr:
19019ea8026Sopenharmony_ci                    sys.stdout.write(line)
19119ea8026Sopenharmony_ci            sys.exit(-1)
19219ea8026Sopenharmony_ci
19319ea8026Sopenharmony_ci
19419ea8026Sopenharmony_ci        # try to figure out the source file if we have debug-info
19519ea8026Sopenharmony_ci        dirs = {}
19619ea8026Sopenharmony_ci        files = {}
19719ea8026Sopenharmony_ci        # note objdump-path may contain extra args
19819ea8026Sopenharmony_ci        cmd = objdump_path + ['--dwarf=rawline', path]
19919ea8026Sopenharmony_ci        if args.get('verbose'):
20019ea8026Sopenharmony_ci            print(' '.join(shlex.quote(c) for c in cmd))
20119ea8026Sopenharmony_ci        proc = sp.Popen(cmd,
20219ea8026Sopenharmony_ci            stdout=sp.PIPE,
20319ea8026Sopenharmony_ci            stderr=sp.PIPE if not args.get('verbose') else None,
20419ea8026Sopenharmony_ci            universal_newlines=True,
20519ea8026Sopenharmony_ci            errors='replace',
20619ea8026Sopenharmony_ci            close_fds=False)
20719ea8026Sopenharmony_ci        for line in proc.stdout:
20819ea8026Sopenharmony_ci            # note that files contain references to dirs, which we
20919ea8026Sopenharmony_ci            # dereference as soon as we see them as each file table follows a
21019ea8026Sopenharmony_ci            # dir table
21119ea8026Sopenharmony_ci            m = line_pattern.match(line)
21219ea8026Sopenharmony_ci            if m:
21319ea8026Sopenharmony_ci                if not m.group('dir'):
21419ea8026Sopenharmony_ci                    # found a directory entry
21519ea8026Sopenharmony_ci                    dirs[int(m.group('no'))] = m.group('path')
21619ea8026Sopenharmony_ci                else:
21719ea8026Sopenharmony_ci                    # found a file entry
21819ea8026Sopenharmony_ci                    dir = int(m.group('dir'))
21919ea8026Sopenharmony_ci                    if dir in dirs:
22019ea8026Sopenharmony_ci                        files[int(m.group('no'))] = os.path.join(
22119ea8026Sopenharmony_ci                            dirs[dir],
22219ea8026Sopenharmony_ci                            m.group('path'))
22319ea8026Sopenharmony_ci                    else:
22419ea8026Sopenharmony_ci                        files[int(m.group('no'))] = m.group('path')
22519ea8026Sopenharmony_ci        proc.wait()
22619ea8026Sopenharmony_ci        if proc.returncode != 0:
22719ea8026Sopenharmony_ci            if not args.get('verbose'):
22819ea8026Sopenharmony_ci                for line in proc.stderr:
22919ea8026Sopenharmony_ci                    sys.stdout.write(line)
23019ea8026Sopenharmony_ci            # do nothing on error, we don't need objdump to work, source files
23119ea8026Sopenharmony_ci            # may just be inaccurate
23219ea8026Sopenharmony_ci            pass
23319ea8026Sopenharmony_ci
23419ea8026Sopenharmony_ci        defs = {}
23519ea8026Sopenharmony_ci        is_func = False
23619ea8026Sopenharmony_ci        f_name = None
23719ea8026Sopenharmony_ci        f_file = None
23819ea8026Sopenharmony_ci        # note objdump-path may contain extra args
23919ea8026Sopenharmony_ci        cmd = objdump_path + ['--dwarf=info', path]
24019ea8026Sopenharmony_ci        if args.get('verbose'):
24119ea8026Sopenharmony_ci            print(' '.join(shlex.quote(c) for c in cmd))
24219ea8026Sopenharmony_ci        proc = sp.Popen(cmd,
24319ea8026Sopenharmony_ci            stdout=sp.PIPE,
24419ea8026Sopenharmony_ci            stderr=sp.PIPE if not args.get('verbose') else None,
24519ea8026Sopenharmony_ci            universal_newlines=True,
24619ea8026Sopenharmony_ci            errors='replace',
24719ea8026Sopenharmony_ci            close_fds=False)
24819ea8026Sopenharmony_ci        for line in proc.stdout:
24919ea8026Sopenharmony_ci            # state machine here to find definitions
25019ea8026Sopenharmony_ci            m = info_pattern.match(line)
25119ea8026Sopenharmony_ci            if m:
25219ea8026Sopenharmony_ci                if m.group('tag'):
25319ea8026Sopenharmony_ci                    if is_func:
25419ea8026Sopenharmony_ci                        defs[f_name] = files.get(f_file, '?')
25519ea8026Sopenharmony_ci                    is_func = (m.group('tag') == 'DW_TAG_subprogram')
25619ea8026Sopenharmony_ci                elif m.group('name'):
25719ea8026Sopenharmony_ci                    f_name = m.group('name')
25819ea8026Sopenharmony_ci                elif m.group('file'):
25919ea8026Sopenharmony_ci                    f_file = int(m.group('file'))
26019ea8026Sopenharmony_ci        if is_func:
26119ea8026Sopenharmony_ci            defs[f_name] = files.get(f_file, '?')
26219ea8026Sopenharmony_ci        proc.wait()
26319ea8026Sopenharmony_ci        if proc.returncode != 0:
26419ea8026Sopenharmony_ci            if not args.get('verbose'):
26519ea8026Sopenharmony_ci                for line in proc.stderr:
26619ea8026Sopenharmony_ci                    sys.stdout.write(line)
26719ea8026Sopenharmony_ci            # do nothing on error, we don't need objdump to work, source files
26819ea8026Sopenharmony_ci            # may just be inaccurate
26919ea8026Sopenharmony_ci            pass
27019ea8026Sopenharmony_ci
27119ea8026Sopenharmony_ci        for r in results_:
27219ea8026Sopenharmony_ci            # find best matching debug symbol, this may be slightly different
27319ea8026Sopenharmony_ci            # due to optimizations
27419ea8026Sopenharmony_ci            if defs:
27519ea8026Sopenharmony_ci                # exact match? avoid difflib if we can for speed
27619ea8026Sopenharmony_ci                if r.function in defs:
27719ea8026Sopenharmony_ci                    file = defs[r.function]
27819ea8026Sopenharmony_ci                else:
27919ea8026Sopenharmony_ci                    _, file = max(
28019ea8026Sopenharmony_ci                        defs.items(),
28119ea8026Sopenharmony_ci                        key=lambda d: difflib.SequenceMatcher(None,
28219ea8026Sopenharmony_ci                            d[0],
28319ea8026Sopenharmony_ci                            r.function, False).ratio())
28419ea8026Sopenharmony_ci            else:
28519ea8026Sopenharmony_ci                file = r.file
28619ea8026Sopenharmony_ci
28719ea8026Sopenharmony_ci            # ignore filtered sources
28819ea8026Sopenharmony_ci            if sources is not None:
28919ea8026Sopenharmony_ci                if not any(
29019ea8026Sopenharmony_ci                        os.path.abspath(file) == os.path.abspath(s)
29119ea8026Sopenharmony_ci                        for s in sources):
29219ea8026Sopenharmony_ci                    continue
29319ea8026Sopenharmony_ci            else:
29419ea8026Sopenharmony_ci                # default to only cwd
29519ea8026Sopenharmony_ci                if not everything and not os.path.commonpath([
29619ea8026Sopenharmony_ci                        os.getcwd(),
29719ea8026Sopenharmony_ci                        os.path.abspath(file)]) == os.getcwd():
29819ea8026Sopenharmony_ci                    continue
29919ea8026Sopenharmony_ci
30019ea8026Sopenharmony_ci            # simplify path
30119ea8026Sopenharmony_ci            if os.path.commonpath([
30219ea8026Sopenharmony_ci                    os.getcwd(),
30319ea8026Sopenharmony_ci                    os.path.abspath(file)]) == os.getcwd():
30419ea8026Sopenharmony_ci                file = os.path.relpath(file)
30519ea8026Sopenharmony_ci            else:
30619ea8026Sopenharmony_ci                file = os.path.abspath(file)
30719ea8026Sopenharmony_ci
30819ea8026Sopenharmony_ci            results.append(r._replace(file=file))
30919ea8026Sopenharmony_ci
31019ea8026Sopenharmony_ci    return results
31119ea8026Sopenharmony_ci
31219ea8026Sopenharmony_ci
31319ea8026Sopenharmony_cidef fold(Result, results, *,
31419ea8026Sopenharmony_ci        by=None,
31519ea8026Sopenharmony_ci        defines=None,
31619ea8026Sopenharmony_ci        **_):
31719ea8026Sopenharmony_ci    if by is None:
31819ea8026Sopenharmony_ci        by = Result._by
31919ea8026Sopenharmony_ci
32019ea8026Sopenharmony_ci    for k in it.chain(by or [], (k for k, _ in defines or [])):
32119ea8026Sopenharmony_ci        if k not in Result._by and k not in Result._fields:
32219ea8026Sopenharmony_ci            print("error: could not find field %r?" % k)
32319ea8026Sopenharmony_ci            sys.exit(-1)
32419ea8026Sopenharmony_ci
32519ea8026Sopenharmony_ci    # filter by matching defines
32619ea8026Sopenharmony_ci    if defines is not None:
32719ea8026Sopenharmony_ci        results_ = []
32819ea8026Sopenharmony_ci        for r in results:
32919ea8026Sopenharmony_ci            if all(getattr(r, k) in vs for k, vs in defines):
33019ea8026Sopenharmony_ci                results_.append(r)
33119ea8026Sopenharmony_ci        results = results_
33219ea8026Sopenharmony_ci
33319ea8026Sopenharmony_ci    # organize results into conflicts
33419ea8026Sopenharmony_ci    folding = co.OrderedDict()
33519ea8026Sopenharmony_ci    for r in results:
33619ea8026Sopenharmony_ci        name = tuple(getattr(r, k) for k in by)
33719ea8026Sopenharmony_ci        if name not in folding:
33819ea8026Sopenharmony_ci            folding[name] = []
33919ea8026Sopenharmony_ci        folding[name].append(r)
34019ea8026Sopenharmony_ci
34119ea8026Sopenharmony_ci    # merge conflicts
34219ea8026Sopenharmony_ci    folded = []
34319ea8026Sopenharmony_ci    for name, rs in folding.items():
34419ea8026Sopenharmony_ci        folded.append(sum(rs[1:], start=rs[0]))
34519ea8026Sopenharmony_ci
34619ea8026Sopenharmony_ci    return folded
34719ea8026Sopenharmony_ci
34819ea8026Sopenharmony_cidef table(Result, results, diff_results=None, *,
34919ea8026Sopenharmony_ci        by=None,
35019ea8026Sopenharmony_ci        fields=None,
35119ea8026Sopenharmony_ci        sort=None,
35219ea8026Sopenharmony_ci        summary=False,
35319ea8026Sopenharmony_ci        all=False,
35419ea8026Sopenharmony_ci        percent=False,
35519ea8026Sopenharmony_ci        **_):
35619ea8026Sopenharmony_ci    all_, all = all, __builtins__.all
35719ea8026Sopenharmony_ci
35819ea8026Sopenharmony_ci    if by is None:
35919ea8026Sopenharmony_ci        by = Result._by
36019ea8026Sopenharmony_ci    if fields is None:
36119ea8026Sopenharmony_ci        fields = Result._fields
36219ea8026Sopenharmony_ci    types = Result._types
36319ea8026Sopenharmony_ci
36419ea8026Sopenharmony_ci    # fold again
36519ea8026Sopenharmony_ci    results = fold(Result, results, by=by)
36619ea8026Sopenharmony_ci    if diff_results is not None:
36719ea8026Sopenharmony_ci        diff_results = fold(Result, diff_results, by=by)
36819ea8026Sopenharmony_ci
36919ea8026Sopenharmony_ci    # organize by name
37019ea8026Sopenharmony_ci    table = {
37119ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
37219ea8026Sopenharmony_ci        for r in results}
37319ea8026Sopenharmony_ci    diff_table = {
37419ea8026Sopenharmony_ci        ','.join(str(getattr(r, k) or '') for k in by): r
37519ea8026Sopenharmony_ci        for r in diff_results or []}
37619ea8026Sopenharmony_ci    names = list(table.keys() | diff_table.keys())
37719ea8026Sopenharmony_ci
37819ea8026Sopenharmony_ci    # sort again, now with diff info, note that python's sort is stable
37919ea8026Sopenharmony_ci    names.sort()
38019ea8026Sopenharmony_ci    if diff_results is not None:
38119ea8026Sopenharmony_ci        names.sort(key=lambda n: tuple(
38219ea8026Sopenharmony_ci            types[k].ratio(
38319ea8026Sopenharmony_ci                getattr(table.get(n), k, None),
38419ea8026Sopenharmony_ci                getattr(diff_table.get(n), k, None))
38519ea8026Sopenharmony_ci            for k in fields),
38619ea8026Sopenharmony_ci            reverse=True)
38719ea8026Sopenharmony_ci    if sort:
38819ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
38919ea8026Sopenharmony_ci            names.sort(
39019ea8026Sopenharmony_ci                key=lambda n: tuple(
39119ea8026Sopenharmony_ci                    (getattr(table[n], k),)
39219ea8026Sopenharmony_ci                    if getattr(table.get(n), k, None) is not None else ()
39319ea8026Sopenharmony_ci                    for k in ([k] if k else [
39419ea8026Sopenharmony_ci                        k for k in Result._sort if k in fields])),
39519ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in Result._fields))
39619ea8026Sopenharmony_ci
39719ea8026Sopenharmony_ci
39819ea8026Sopenharmony_ci    # build up our lines
39919ea8026Sopenharmony_ci    lines = []
40019ea8026Sopenharmony_ci
40119ea8026Sopenharmony_ci    # header
40219ea8026Sopenharmony_ci    header = []
40319ea8026Sopenharmony_ci    header.append('%s%s' % (
40419ea8026Sopenharmony_ci        ','.join(by),
40519ea8026Sopenharmony_ci        ' (%d added, %d removed)' % (
40619ea8026Sopenharmony_ci            sum(1 for n in table if n not in diff_table),
40719ea8026Sopenharmony_ci            sum(1 for n in diff_table if n not in table))
40819ea8026Sopenharmony_ci            if diff_results is not None and not percent else '')
40919ea8026Sopenharmony_ci        if not summary else '')
41019ea8026Sopenharmony_ci    if diff_results is None:
41119ea8026Sopenharmony_ci        for k in fields:
41219ea8026Sopenharmony_ci            header.append(k)
41319ea8026Sopenharmony_ci    elif percent:
41419ea8026Sopenharmony_ci        for k in fields:
41519ea8026Sopenharmony_ci            header.append(k)
41619ea8026Sopenharmony_ci    else:
41719ea8026Sopenharmony_ci        for k in fields:
41819ea8026Sopenharmony_ci            header.append('o'+k)
41919ea8026Sopenharmony_ci        for k in fields:
42019ea8026Sopenharmony_ci            header.append('n'+k)
42119ea8026Sopenharmony_ci        for k in fields:
42219ea8026Sopenharmony_ci            header.append('d'+k)
42319ea8026Sopenharmony_ci    header.append('')
42419ea8026Sopenharmony_ci    lines.append(header)
42519ea8026Sopenharmony_ci
42619ea8026Sopenharmony_ci    def table_entry(name, r, diff_r=None, ratios=[]):
42719ea8026Sopenharmony_ci        entry = []
42819ea8026Sopenharmony_ci        entry.append(name)
42919ea8026Sopenharmony_ci        if diff_results is None:
43019ea8026Sopenharmony_ci            for k in fields:
43119ea8026Sopenharmony_ci                entry.append(getattr(r, k).table()
43219ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
43319ea8026Sopenharmony_ci                    else types[k].none)
43419ea8026Sopenharmony_ci        elif percent:
43519ea8026Sopenharmony_ci            for k in fields:
43619ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
43719ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
43819ea8026Sopenharmony_ci                    else types[k].diff_none)
43919ea8026Sopenharmony_ci        else:
44019ea8026Sopenharmony_ci            for k in fields:
44119ea8026Sopenharmony_ci                entry.append(getattr(diff_r, k).diff_table()
44219ea8026Sopenharmony_ci                    if getattr(diff_r, k, None) is not None
44319ea8026Sopenharmony_ci                    else types[k].diff_none)
44419ea8026Sopenharmony_ci            for k in fields:
44519ea8026Sopenharmony_ci                entry.append(getattr(r, k).diff_table()
44619ea8026Sopenharmony_ci                    if getattr(r, k, None) is not None
44719ea8026Sopenharmony_ci                    else types[k].diff_none)
44819ea8026Sopenharmony_ci            for k in fields:
44919ea8026Sopenharmony_ci                entry.append(types[k].diff_diff(
45019ea8026Sopenharmony_ci                        getattr(r, k, None),
45119ea8026Sopenharmony_ci                        getattr(diff_r, k, None)))
45219ea8026Sopenharmony_ci        if diff_results is None:
45319ea8026Sopenharmony_ci            entry.append('')
45419ea8026Sopenharmony_ci        elif percent:
45519ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
45619ea8026Sopenharmony_ci                '+∞%' if t == +m.inf
45719ea8026Sopenharmony_ci                else '-∞%' if t == -m.inf
45819ea8026Sopenharmony_ci                else '%+.1f%%' % (100*t)
45919ea8026Sopenharmony_ci                for t in ratios))
46019ea8026Sopenharmony_ci        else:
46119ea8026Sopenharmony_ci            entry.append(' (%s)' % ', '.join(
46219ea8026Sopenharmony_ci                    '+∞%' if t == +m.inf
46319ea8026Sopenharmony_ci                    else '-∞%' if t == -m.inf
46419ea8026Sopenharmony_ci                    else '%+.1f%%' % (100*t)
46519ea8026Sopenharmony_ci                    for t in ratios
46619ea8026Sopenharmony_ci                    if t)
46719ea8026Sopenharmony_ci                if any(ratios) else '')
46819ea8026Sopenharmony_ci        return entry
46919ea8026Sopenharmony_ci
47019ea8026Sopenharmony_ci    # entries
47119ea8026Sopenharmony_ci    if not summary:
47219ea8026Sopenharmony_ci        for name in names:
47319ea8026Sopenharmony_ci            r = table.get(name)
47419ea8026Sopenharmony_ci            if diff_results is None:
47519ea8026Sopenharmony_ci                diff_r = None
47619ea8026Sopenharmony_ci                ratios = None
47719ea8026Sopenharmony_ci            else:
47819ea8026Sopenharmony_ci                diff_r = diff_table.get(name)
47919ea8026Sopenharmony_ci                ratios = [
48019ea8026Sopenharmony_ci                    types[k].ratio(
48119ea8026Sopenharmony_ci                        getattr(r, k, None),
48219ea8026Sopenharmony_ci                        getattr(diff_r, k, None))
48319ea8026Sopenharmony_ci                    for k in fields]
48419ea8026Sopenharmony_ci                if not all_ and not any(ratios):
48519ea8026Sopenharmony_ci                    continue
48619ea8026Sopenharmony_ci            lines.append(table_entry(name, r, diff_r, ratios))
48719ea8026Sopenharmony_ci
48819ea8026Sopenharmony_ci    # total
48919ea8026Sopenharmony_ci    r = next(iter(fold(Result, results, by=[])), None)
49019ea8026Sopenharmony_ci    if diff_results is None:
49119ea8026Sopenharmony_ci        diff_r = None
49219ea8026Sopenharmony_ci        ratios = None
49319ea8026Sopenharmony_ci    else:
49419ea8026Sopenharmony_ci        diff_r = next(iter(fold(Result, diff_results, by=[])), None)
49519ea8026Sopenharmony_ci        ratios = [
49619ea8026Sopenharmony_ci            types[k].ratio(
49719ea8026Sopenharmony_ci                getattr(r, k, None),
49819ea8026Sopenharmony_ci                getattr(diff_r, k, None))
49919ea8026Sopenharmony_ci            for k in fields]
50019ea8026Sopenharmony_ci    lines.append(table_entry('TOTAL', r, diff_r, ratios))
50119ea8026Sopenharmony_ci
50219ea8026Sopenharmony_ci    # find the best widths, note that column 0 contains the names and column -1
50319ea8026Sopenharmony_ci    # the ratios, so those are handled a bit differently
50419ea8026Sopenharmony_ci    widths = [
50519ea8026Sopenharmony_ci        ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1
50619ea8026Sopenharmony_ci        for w, i in zip(
50719ea8026Sopenharmony_ci            it.chain([23], it.repeat(7)),
50819ea8026Sopenharmony_ci            range(len(lines[0])-1))]
50919ea8026Sopenharmony_ci
51019ea8026Sopenharmony_ci    # print our table
51119ea8026Sopenharmony_ci    for line in lines:
51219ea8026Sopenharmony_ci        print('%-*s  %s%s' % (
51319ea8026Sopenharmony_ci            widths[0], line[0],
51419ea8026Sopenharmony_ci            ' '.join('%*s' % (w, x)
51519ea8026Sopenharmony_ci                for w, x in zip(widths[1:], line[1:-1])),
51619ea8026Sopenharmony_ci            line[-1]))
51719ea8026Sopenharmony_ci
51819ea8026Sopenharmony_ci
51919ea8026Sopenharmony_cidef main(obj_paths, *,
52019ea8026Sopenharmony_ci        by=None,
52119ea8026Sopenharmony_ci        fields=None,
52219ea8026Sopenharmony_ci        defines=None,
52319ea8026Sopenharmony_ci        sort=None,
52419ea8026Sopenharmony_ci        **args):
52519ea8026Sopenharmony_ci    # find sizes
52619ea8026Sopenharmony_ci    if not args.get('use', None):
52719ea8026Sopenharmony_ci        results = collect(obj_paths, **args)
52819ea8026Sopenharmony_ci    else:
52919ea8026Sopenharmony_ci        results = []
53019ea8026Sopenharmony_ci        with openio(args['use']) as f:
53119ea8026Sopenharmony_ci            reader = csv.DictReader(f, restval='')
53219ea8026Sopenharmony_ci            for r in reader:
53319ea8026Sopenharmony_ci                try:
53419ea8026Sopenharmony_ci                    results.append(DataResult(
53519ea8026Sopenharmony_ci                        **{k: r[k] for k in DataResult._by
53619ea8026Sopenharmony_ci                            if k in r and r[k].strip()},
53719ea8026Sopenharmony_ci                        **{k: r['data_'+k] for k in DataResult._fields
53819ea8026Sopenharmony_ci                            if 'data_'+k in r and r['data_'+k].strip()}))
53919ea8026Sopenharmony_ci                except TypeError:
54019ea8026Sopenharmony_ci                    pass
54119ea8026Sopenharmony_ci
54219ea8026Sopenharmony_ci    # fold
54319ea8026Sopenharmony_ci    results = fold(DataResult, results, by=by, defines=defines)
54419ea8026Sopenharmony_ci
54519ea8026Sopenharmony_ci    # sort, note that python's sort is stable
54619ea8026Sopenharmony_ci    results.sort()
54719ea8026Sopenharmony_ci    if sort:
54819ea8026Sopenharmony_ci        for k, reverse in reversed(sort):
54919ea8026Sopenharmony_ci            results.sort(
55019ea8026Sopenharmony_ci                key=lambda r: tuple(
55119ea8026Sopenharmony_ci                    (getattr(r, k),) if getattr(r, k) is not None else ()
55219ea8026Sopenharmony_ci                    for k in ([k] if k else DataResult._sort)),
55319ea8026Sopenharmony_ci                reverse=reverse ^ (not k or k in DataResult._fields))
55419ea8026Sopenharmony_ci
55519ea8026Sopenharmony_ci    # write results to CSV
55619ea8026Sopenharmony_ci    if args.get('output'):
55719ea8026Sopenharmony_ci        with openio(args['output'], 'w') as f:
55819ea8026Sopenharmony_ci            writer = csv.DictWriter(f,
55919ea8026Sopenharmony_ci                (by if by is not None else DataResult._by)
56019ea8026Sopenharmony_ci                + ['data_'+k for k in (
56119ea8026Sopenharmony_ci                    fields if fields is not None else DataResult._fields)])
56219ea8026Sopenharmony_ci            writer.writeheader()
56319ea8026Sopenharmony_ci            for r in results:
56419ea8026Sopenharmony_ci                writer.writerow(
56519ea8026Sopenharmony_ci                    {k: getattr(r, k) for k in (
56619ea8026Sopenharmony_ci                        by if by is not None else DataResult._by)}
56719ea8026Sopenharmony_ci                    | {'data_'+k: getattr(r, k) for k in (
56819ea8026Sopenharmony_ci                        fields if fields is not None else DataResult._fields)})
56919ea8026Sopenharmony_ci
57019ea8026Sopenharmony_ci    # find previous results?
57119ea8026Sopenharmony_ci    if args.get('diff'):
57219ea8026Sopenharmony_ci        diff_results = []
57319ea8026Sopenharmony_ci        try:
57419ea8026Sopenharmony_ci            with openio(args['diff']) as f:
57519ea8026Sopenharmony_ci                reader = csv.DictReader(f, restval='')
57619ea8026Sopenharmony_ci                for r in reader:
57719ea8026Sopenharmony_ci                    if not any('data_'+k in r and r['data_'+k].strip()
57819ea8026Sopenharmony_ci                            for k in DataResult._fields):
57919ea8026Sopenharmony_ci                        continue
58019ea8026Sopenharmony_ci                    try:
58119ea8026Sopenharmony_ci                        diff_results.append(DataResult(
58219ea8026Sopenharmony_ci                            **{k: r[k] for k in DataResult._by
58319ea8026Sopenharmony_ci                                if k in r and r[k].strip()},
58419ea8026Sopenharmony_ci                            **{k: r['data_'+k] for k in DataResult._fields
58519ea8026Sopenharmony_ci                                if 'data_'+k in r and r['data_'+k].strip()}))
58619ea8026Sopenharmony_ci                    except TypeError:
58719ea8026Sopenharmony_ci                        pass
58819ea8026Sopenharmony_ci        except FileNotFoundError:
58919ea8026Sopenharmony_ci            pass
59019ea8026Sopenharmony_ci
59119ea8026Sopenharmony_ci        # fold
59219ea8026Sopenharmony_ci        diff_results = fold(DataResult, diff_results, by=by, defines=defines)
59319ea8026Sopenharmony_ci
59419ea8026Sopenharmony_ci    # print table
59519ea8026Sopenharmony_ci    if not args.get('quiet'):
59619ea8026Sopenharmony_ci        table(DataResult, results,
59719ea8026Sopenharmony_ci            diff_results if args.get('diff') else None,
59819ea8026Sopenharmony_ci            by=by if by is not None else ['function'],
59919ea8026Sopenharmony_ci            fields=fields,
60019ea8026Sopenharmony_ci            sort=sort,
60119ea8026Sopenharmony_ci            **args)
60219ea8026Sopenharmony_ci
60319ea8026Sopenharmony_ci
60419ea8026Sopenharmony_ciif __name__ == "__main__":
60519ea8026Sopenharmony_ci    import argparse
60619ea8026Sopenharmony_ci    import sys
60719ea8026Sopenharmony_ci    parser = argparse.ArgumentParser(
60819ea8026Sopenharmony_ci        description="Find data size at the function level.",
60919ea8026Sopenharmony_ci        allow_abbrev=False)
61019ea8026Sopenharmony_ci    parser.add_argument(
61119ea8026Sopenharmony_ci        'obj_paths',
61219ea8026Sopenharmony_ci        nargs='*',
61319ea8026Sopenharmony_ci        help="Input *.o files.")
61419ea8026Sopenharmony_ci    parser.add_argument(
61519ea8026Sopenharmony_ci        '-v', '--verbose',
61619ea8026Sopenharmony_ci        action='store_true',
61719ea8026Sopenharmony_ci        help="Output commands that run behind the scenes.")
61819ea8026Sopenharmony_ci    parser.add_argument(
61919ea8026Sopenharmony_ci        '-q', '--quiet',
62019ea8026Sopenharmony_ci        action='store_true',
62119ea8026Sopenharmony_ci        help="Don't show anything, useful with -o.")
62219ea8026Sopenharmony_ci    parser.add_argument(
62319ea8026Sopenharmony_ci        '-o', '--output',
62419ea8026Sopenharmony_ci        help="Specify CSV file to store results.")
62519ea8026Sopenharmony_ci    parser.add_argument(
62619ea8026Sopenharmony_ci        '-u', '--use',
62719ea8026Sopenharmony_ci        help="Don't parse anything, use this CSV file.")
62819ea8026Sopenharmony_ci    parser.add_argument(
62919ea8026Sopenharmony_ci        '-d', '--diff',
63019ea8026Sopenharmony_ci        help="Specify CSV file to diff against.")
63119ea8026Sopenharmony_ci    parser.add_argument(
63219ea8026Sopenharmony_ci        '-a', '--all',
63319ea8026Sopenharmony_ci        action='store_true',
63419ea8026Sopenharmony_ci        help="Show all, not just the ones that changed.")
63519ea8026Sopenharmony_ci    parser.add_argument(
63619ea8026Sopenharmony_ci        '-p', '--percent',
63719ea8026Sopenharmony_ci        action='store_true',
63819ea8026Sopenharmony_ci        help="Only show percentage change, not a full diff.")
63919ea8026Sopenharmony_ci    parser.add_argument(
64019ea8026Sopenharmony_ci        '-b', '--by',
64119ea8026Sopenharmony_ci        action='append',
64219ea8026Sopenharmony_ci        choices=DataResult._by,
64319ea8026Sopenharmony_ci        help="Group by this field.")
64419ea8026Sopenharmony_ci    parser.add_argument(
64519ea8026Sopenharmony_ci        '-f', '--field',
64619ea8026Sopenharmony_ci        dest='fields',
64719ea8026Sopenharmony_ci        action='append',
64819ea8026Sopenharmony_ci        choices=DataResult._fields,
64919ea8026Sopenharmony_ci        help="Show this field.")
65019ea8026Sopenharmony_ci    parser.add_argument(
65119ea8026Sopenharmony_ci        '-D', '--define',
65219ea8026Sopenharmony_ci        dest='defines',
65319ea8026Sopenharmony_ci        action='append',
65419ea8026Sopenharmony_ci        type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
65519ea8026Sopenharmony_ci        help="Only include results where this field is this value.")
65619ea8026Sopenharmony_ci    class AppendSort(argparse.Action):
65719ea8026Sopenharmony_ci        def __call__(self, parser, namespace, value, option):
65819ea8026Sopenharmony_ci            if namespace.sort is None:
65919ea8026Sopenharmony_ci                namespace.sort = []
66019ea8026Sopenharmony_ci            namespace.sort.append((value, True if option == '-S' else False))
66119ea8026Sopenharmony_ci    parser.add_argument(
66219ea8026Sopenharmony_ci        '-s', '--sort',
66319ea8026Sopenharmony_ci        nargs='?',
66419ea8026Sopenharmony_ci        action=AppendSort,
66519ea8026Sopenharmony_ci        help="Sort by this field.")
66619ea8026Sopenharmony_ci    parser.add_argument(
66719ea8026Sopenharmony_ci        '-S', '--reverse-sort',
66819ea8026Sopenharmony_ci        nargs='?',
66919ea8026Sopenharmony_ci        action=AppendSort,
67019ea8026Sopenharmony_ci        help="Sort by this field, but backwards.")
67119ea8026Sopenharmony_ci    parser.add_argument(
67219ea8026Sopenharmony_ci        '-Y', '--summary',
67319ea8026Sopenharmony_ci        action='store_true',
67419ea8026Sopenharmony_ci        help="Only show the total.")
67519ea8026Sopenharmony_ci    parser.add_argument(
67619ea8026Sopenharmony_ci        '-F', '--source',
67719ea8026Sopenharmony_ci        dest='sources',
67819ea8026Sopenharmony_ci        action='append',
67919ea8026Sopenharmony_ci        help="Only consider definitions in this file. Defaults to anything "
68019ea8026Sopenharmony_ci            "in the current directory.")
68119ea8026Sopenharmony_ci    parser.add_argument(
68219ea8026Sopenharmony_ci        '--everything',
68319ea8026Sopenharmony_ci        action='store_true',
68419ea8026Sopenharmony_ci        help="Include builtin and libc specific symbols.")
68519ea8026Sopenharmony_ci    parser.add_argument(
68619ea8026Sopenharmony_ci        '--nm-types',
68719ea8026Sopenharmony_ci        default=NM_TYPES,
68819ea8026Sopenharmony_ci        help="Type of symbols to report, this uses the same single-character "
68919ea8026Sopenharmony_ci            "type-names emitted by nm. Defaults to %r." % NM_TYPES)
69019ea8026Sopenharmony_ci    parser.add_argument(
69119ea8026Sopenharmony_ci        '--nm-path',
69219ea8026Sopenharmony_ci        type=lambda x: x.split(),
69319ea8026Sopenharmony_ci        default=NM_PATH,
69419ea8026Sopenharmony_ci        help="Path to the nm executable, may include flags. "
69519ea8026Sopenharmony_ci            "Defaults to %r." % NM_PATH)
69619ea8026Sopenharmony_ci    parser.add_argument(
69719ea8026Sopenharmony_ci        '--objdump-path',
69819ea8026Sopenharmony_ci        type=lambda x: x.split(),
69919ea8026Sopenharmony_ci        default=OBJDUMP_PATH,
70019ea8026Sopenharmony_ci        help="Path to the objdump executable, may include flags. "
70119ea8026Sopenharmony_ci            "Defaults to %r." % OBJDUMP_PATH)
70219ea8026Sopenharmony_ci    sys.exit(main(**{k: v
70319ea8026Sopenharmony_ci        for k, v in vars(parser.parse_intermixed_args()).items()
70419ea8026Sopenharmony_ci        if v is not None}))
705