scripts: Added -t/--hot to recursive scripts, stack.py, etc

This is mainly useful for stack.py, where -t/--hot lets you quickly see
everything that contributes to the stack limit for each function.

This was (and still is) possible with -s + -z, but it was pretty
annoying to use:

- The stack trace rendered _diagonally_ as a consequence of -z, which is
  probably the worst use of screen real estate.

- This trick only really worked with -s, which was the opposite order of
  what you usually want on the command line: -S.

Adding a special for-purpose -t/--hot flag makes looking at the hot path
much easier, at the cost of more hacky python code (and I _mean_ hacky,
making the hot path selection useful while following exising sort rules
was annoyingly complicated).

Also added -t/--hot to perf.py and perfbd.py for consistency, though it
makes a bit less sense there.

Also also reworked related code in all three scripts: stack.py, perf.py,
perfbd.py. The logic should be a bit more equivalent, and
perf.py/perfbd.py detect cycles now.
This commit is contained in:
Christopher Haster
2024-11-03 16:32:38 -06:00
parent 904c2eddd7
commit e32af5cd8a
3 changed files with 364 additions and 125 deletions

View File

@@ -321,7 +321,8 @@ def table(Result, results, diff_results=None, *,
summary=False,
all=False,
percent=False,
depth=1,
depth=None,
hot=False,
**_):
all_, all = all, __builtins__.all
@@ -477,27 +478,26 @@ def table(Result, results, diff_results=None, *,
if not summary:
# find the actual depth
depth_ = depth
if m.isinf(depth_):
def rec_depth(names_, seen=set()):
depth_ = -1
for name in names_:
# found a cycle?
if name in seen:
continue
if hot:
depth_ = 2
elif m.isinf(depth_):
def rec_depth(children_, seen=set()):
names_ = {
','.join(str(getattr(Result(*c), k) or '')
for k in by)
for c in children_}
# recurse?
if name in table:
children = {
','.join(str(getattr(Result(*c), k) or '')
for k in by)
for c in table[name].children}
depth_ = max(depth_,
rec_depth(
[n for n in names if n in children],
seen | {name}))
return depth_ + 1
return max(
(rec_depth(table[name].children, seen | {name})
for name in names_
if name not in seen),
default=-1) + 1
depth_ = rec_depth(names)
depth_ = max(
(rec_depth(table[name].children, {name})
for name in names
if name in table),
default=-1) + 1
# adjust the name width based on the call depth
widths[0] += 4*max(depth_-1, 0)
@@ -513,46 +513,120 @@ def table(Result, results, diff_results=None, *,
if not summary:
line_table = {n: l for n, l in zip(names, lines[1:-1])}
def recurse(names_, depth_, seen=set(), prefixes=('', '', '', '')):
for i, name in enumerate(names_):
if name not in line_table:
continue
line = line_table[name]
is_last = (i == len(names_)-1)
if hot:
def recurse(children_, depth_, seen=set(),
prefixes=('', '', '', '')):
names_ = {','.join(str(getattr(Result(*c), k) or '')
for k in by)
for c in children_}
if not names_:
return
print('%s%-*s %s' % (
prefixes[0+is_last],
widths[0] - len(prefixes[0+is_last]), line[0][0],
' '.join('%*s%-*s' % (
widths[i], x[0],
notes[i],
' (%s)' % ', '.join(it.chain(
x[1], ['cycle detected']))
if i == len(widths)-1 and name in seen
else ' (%s)' % ', '.join(x[1]) if x[1]
else '')
for i, x in enumerate(line[1:], 1))))
# find the "hottest" path at each step, we use
# the sort field if requested, but ignore reversedness
name = max(names_,
key=lambda n: tuple(
tuple(
(getattr(table[n], k),)
if getattr(table.get(n), k, None) is not None
else ()
for k in ([k] if k else [
k for k in Result._sort if k in fields])
if k in fields)
for k, reverse in it.chain(
sort or [],
[(None, False)])))
if name in line_table:
line = line_table[name]
is_last = not table[name].children
print('%s%-*s %s' % (
prefixes[0+is_last],
widths[0] - len(prefixes[0+is_last]), line[0][0],
' '.join('%*s%-*s' % (
widths[i], x[0],
notes[i],
' (%s)' % ', '.join(it.chain(
x[1], ['cycle detected']))
if i == len(widths)-1 and name in seen
else ' (%s)' % ', '.join(x[1]) if x[1]
else '')
for i, x in enumerate(line[1:], 1))))
# found a cycle?
if name in seen:
continue
return
# recurse?
if name in table and depth_ > 1:
children = {
','.join(str(getattr(Result(*c), k) or '') for k in by)
for c in table[name].children}
if depth_ > 1:
recurse(
# note we're maintaining sort order
[n for n in names if n in children],
table[name].children,
depth_-1,
seen | {name},
(prefixes[2+is_last] + "|-> ",
prefixes[2+is_last] + "'-> ",
prefixes[2+is_last] + "| ",
prefixes[2+is_last] + " "))
prefixes)
recurse(names, depth)
else:
def recurse(children_, depth_, seen=set(),
prefixes=('', '', '', '')):
# note we're maintaining sort order
names_ = {','.join(str(getattr(Result(*c), k) or '')
for k in by)
for c in children_}
names_ = [n for n in names if n in names_]
for i, name in enumerate(names_):
if name not in line_table:
continue
line = line_table[name]
is_last = (i == len(names_)-1)
print('%s%-*s %s' % (
prefixes[0+is_last],
widths[0] - len(prefixes[0+is_last]), line[0][0],
' '.join('%*s%-*s' % (
widths[i], x[0],
notes[i],
' (%s)' % ', '.join(it.chain(
x[1], ['cycle detected']))
if i == len(widths)-1 and name in seen
else ' (%s)' % ', '.join(x[1]) if x[1]
else '')
for i, x in enumerate(line[1:], 1))))
# found a cycle?
if name in seen:
continue
# recurse?
if depth_ > 1:
recurse(
table[name].children,
depth_-1,
seen | {name},
(prefixes[2+is_last] + "|-> ",
prefixes[2+is_last] + "'-> ",
prefixes[2+is_last] + "| ",
prefixes[2+is_last] + " "))
# make the top layer a special case
for name, line in zip(names, lines[1:-1]):
print('%-*s %s' % (
widths[0], line[0][0],
' '.join('%*s%-*s' % (
widths[i], x[0],
notes[i], ' (%s)' % ', '.join(x[1]) if x[1] else '')
for i, x in enumerate(line[1:], 1))))
if name in table and depth > 1:
recurse(
table[name].children,
depth-1,
{name},
("|-> ",
"'-> ",
"| ",
" "))
print('%-*s %s' % (
widths[0], lines[-1][0][0],
@@ -568,8 +642,9 @@ def main(ci_paths,
defines=[],
sort=None,
**args):
# figure out depth
if args.get('depth') is None:
args['depth'] = 1
args['depth'] = m.inf if args.get('hot') else 1
elif args.get('depth') == 0:
args['depth'] = m.inf
@@ -760,6 +835,10 @@ if __name__ == "__main__":
const=0,
help="Depth of function calls to show. 0 shows all calls but may not "
"terminate!")
parser.add_argument(
'-t', '--hot',
action='store_true',
help="Show only the hot path for each function call.")
parser.add_argument(
'-e', '--error-on-recursion',
action='store_true',