scripts: csv.py: Added explicit z field, reusing -Z/--children

In an effort to move away from magic usage of -i/--enumerate, this adds
an explicit z field for differentiating -r/--hot results (and for normal
recursive results).

Instead of trying to think of a new flag to control this, this just
piggybacks on -Z/--children, which now accepts a tuple:

- ./scripts/csv.py -z3 -Z
- ./scripts/csv.py -z3 -Zchildren
- ./scripts/csv.py -z3 -Zz,children

The only tricky bit was needing to insert z in front of the by fields,
otherwise it was mostly a simplification from the enumerate mess.

Another positive side-effect: -r/--hot (and -z/--depth) now implies
-Zz,children, removing the annoying/confusing behavior of hotify folding
results by default.
This commit is contained in:
Christopher Haster
2026-01-23 01:00:22 -06:00
parent 078a1fb4c6
commit ac338e66f0
6 changed files with 71 additions and 65 deletions
+41 -20
View File
@@ -1560,6 +1560,7 @@ def compile(fields_, results,
mods=[], mods=[],
exprs=[], exprs=[],
sort=None, sort=None,
z=None,
children=None, children=None,
hot=None, hot=None,
notes=None, notes=None,
@@ -1646,6 +1647,8 @@ def compile(fields_, results,
{k: r__.get(k, '') for k in by} {k: r__.get(k, '') for k in by}
| {k: ([r__[k]], 1) if k in r__ else ([], 0) | {k: ([r__[k]], 1) if k in r__ else ([], 0)
for k in fields} for k in fields}
| ({z: r[z] if z in r else 0}
if z is not None else {})
| ({children: r[children] if children in r else []} | ({children: r[children] if children in r else []}
if children is not None else {}) if children is not None else {})
| ({notes: r[notes] if notes in r else set()} | ({notes: r[notes] if notes in r else set()}
@@ -1667,6 +1670,8 @@ def compile(fields_, results,
object.__getattribute__(self, k), object.__getattribute__(self, k),
object.__getattribute__(other, k)) object.__getattribute__(other, k))
for k in fields} for k in fields}
| ({z: object.__getattribute__(self, z)}
if z is not None else {})
| ({children: object.__getattribute__(self, children) | ({children: object.__getattribute__(self, children)
+ object.__getattribute__(other, children)} + object.__getattribute__(other, children)}
if children is not None else {}) if children is not None else {})
@@ -1703,6 +1708,7 @@ def compile(fields_, results,
_types={k: t for k, (_, t) in folds___.items()}, _types={k: t for k, (_, t) in folds___.items()},
_mods=mods, _mods=mods,
_exprs=exprs, _exprs=exprs,
**{'_z': z} if z is not None else {},
**{'_children': children} if children is not None else {}, **{'_children': children} if children is not None else {},
**{'_notes': notes} if notes is not None else {})) **{'_notes': notes} if notes is not None else {}))
@@ -1710,6 +1716,7 @@ def homogenize(Result, results, *,
enumerates=None, enumerates=None,
defines=[], defines=[],
depth=1, depth=1,
depth_=0,
**_): **_):
# running result state # running result state
state = {} state = {}
@@ -1734,12 +1741,15 @@ def homogenize(Result, results, *,
| ({e: len(results_) for e in enumerates} | ({e: len(results_) for e in enumerates}
if enumerates is not None if enumerates is not None
else {}) else {})
# keep track of depth?
| ({Result._z: depth_} if hasattr(Result, '_z') else {})
# recurse? # recurse?
| ({Result._children: homogenize( | ({Result._children: homogenize(
Result, r[Result._children], Result, r[Result._children],
# only filter defines at the top level! # only filter defines at the top level!
enumerates=enumerates, enumerates=enumerates,
depth=depth-1)} depth=depth-1,
depth_=depth_+1)}
if hasattr(Result, '_children') if hasattr(Result, '_children')
and Result._children in r and Result._children in r
and r[Result._children] is not None and r[Result._children] is not None
@@ -1834,14 +1844,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -1863,12 +1873,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1:
@@ -2360,12 +2366,17 @@ def main(csv_paths, *,
file=sys.stderr) file=sys.stderr)
sys.exit(-1) sys.exit(-1)
z = None
if children is not None: if children is not None:
if len(children) > 1: if len(children) > 1:
print("error: multiple --children fields currently not supported", print("error: multiple --children fields currently not supported",
file=sys.stderr) file=sys.stderr)
sys.exit(-1) sys.exit(-1)
children = children[0] children = children[0]
if len(children) > 1:
z, children = children
else:
children, = children
if notes is not None: if notes is not None:
if len(notes) > 1: if len(notes) > 1:
@@ -2375,8 +2386,11 @@ def main(csv_paths, *,
notes = notes[0] notes = notes[0]
# recursive results imply --children # recursive results imply --children
if (depth is not None or hot is not None) and children is None: if depth is not None or hot is not None:
children = 'children' if z is None:
z = 'z'
if children is None:
children = 'children'
# figure out depth # figure out depth
if depth is None: if depth is None:
@@ -2442,6 +2456,10 @@ def main(csv_paths, *,
or args.get('output') or args.get('output')
or args.get('output_json')] or args.get('output_json')]
# insert zed
if z is not None:
by__.insert(0, z)
# if by not specified, guess it's anything not in fields/defines/exprs/etc # if by not specified, guess it's anything not in fields/defines/exprs/etc
if by is None or all(hidden for (k, v), hidden in by): if by is None or all(hidden for (k, v), hidden in by):
by__.extend(k for k in fields_ by__.extend(k for k in fields_
@@ -2449,8 +2467,9 @@ def main(csv_paths, *,
and not any(k == k_ for (k_, _), _ in (fields or [])) and not any(k == k_ for (k_, _), _ in (fields or []))
and not any(k == k_ for k_, _ in defines) and not any(k == k_ for k_, _ in defines)
and not any(k == k_ for (k_, _), _ in (sort or [])) and not any(k == k_ for (k_, _), _ in (sort or []))
and k != children
and not any(k == k_ for (k_, _), _ in (hot or [])) and not any(k == k_ for (k_, _), _ in (hot or []))
and k != z
and k != children
and k != notes and k != notes
and not any(k == k_ and not any(k == k_
for _, expr in exprs for _, expr in exprs
@@ -2463,8 +2482,9 @@ def main(csv_paths, *,
and not any(k == k_ for (k_, _), _ in (fields or [])) and not any(k == k_ for (k_, _), _ in (fields or []))
and not any(k == k_ for k_, _ in defines) and not any(k == k_ for k_, _ in defines)
and not any(k == k_ for (k_, _), _ in (sort or [])) and not any(k == k_ for (k_, _), _ in (sort or []))
and k != children
and not any(k == k_ for (k_, _), _ in (hot or [])) and not any(k == k_ for (k_, _), _ in (hot or []))
and k != z
and k != children
and k != notes and k != notes
and not any(k == k_ and not any(k == k_
for _, expr in exprs for _, expr in exprs
@@ -2488,6 +2508,7 @@ def main(csv_paths, *,
mods=mods, mods=mods,
exprs=exprs, exprs=exprs,
sort=sort, sort=sort,
z=z,
children=children, children=children,
hot=hot, hot=hot,
notes=notes) notes=notes)
@@ -2507,7 +2528,6 @@ def main(csv_paths, *,
# hotify? # hotify?
if hot: if hot:
results = hotify(Result, results, results = hotify(Result, results,
enumerates=enumerates,
depth=depth, depth=depth,
hot=hot) hot=hot)
@@ -2543,7 +2563,6 @@ def main(csv_paths, *,
# hotify? # hotify?
if hot: if hot:
diff_results = hotify(Result, diff_results, diff_results = hotify(Result, diff_results,
enumerates=enumerates,
depth=depth, depth=depth,
hot=hot) hot=hot)
@@ -2740,10 +2759,12 @@ if __name__ == "__main__":
parser.add_argument( parser.add_argument(
'-Z', '--children', '-Z', '--children',
nargs='?', nargs='?',
const='children', const=('z', 'children'),
action='append', action='append',
help="Field to use for recursive results. This expects a list " type=lambda x: tuple(v.strip() for v in x.split(',')),
"and really only works with JSON input.") help="Fields to use for recursive results, either the children "
"field or depth,children fields. This really only works with "
"JSON input. Defaults to 'z' and 'children'.")
class AppendHot(argparse.Action): class AppendHot(argparse.Action):
def __call__(self, parser, namespace, value, option): def __call__(self, parser, namespace, value, option):
if namespace.hot is None: if namespace.hot is None:
@@ -2778,7 +2799,7 @@ if __name__ == "__main__":
nargs='?', nargs='?',
const='notes', const='notes',
action='append', action='append',
help="Field to use for notes.") help="Field to use for notes. Defaults to 'notes'.")
parser.add_argument( parser.add_argument(
'--no-header', '--no-header',
action='store_true', action='store_true',
+6 -9
View File
@@ -150,6 +150,7 @@ class CtxResult(co.namedtuple('CtxResult', [
_fields = ['off', 'size'] _fields = ['off', 'size']
_sort = ['size'] _sort = ['size']
_types = {'off': CsvInt, 'size': CsvInt} _types = {'off': CsvInt, 'size': CsvInt}
_z = 'z'
_children = 'children' _children = 'children'
_notes = 'notes' _notes = 'notes'
@@ -814,14 +815,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -843,12 +844,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1:
+6 -9
View File
@@ -167,6 +167,7 @@ class PerfResult(co.namedtuple('PerfResult', [
'cycles': CsvInt, 'cycles': CsvInt,
'bmisses': CsvInt, 'branches': CsvInt, 'bmisses': CsvInt, 'branches': CsvInt,
'cmisses': CsvInt, 'caches': CsvInt} 'cmisses': CsvInt, 'caches': CsvInt}
_z = 'z'
_children = 'children' _children = 'children'
__slots__ = () __slots__ = ()
@@ -913,14 +914,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -942,12 +943,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1:
+6 -9
View File
@@ -155,6 +155,7 @@ class PerfBdResult(co.namedtuple('PerfBdResult', [
_fields = ['readed', 'proged', 'erased'] _fields = ['readed', 'proged', 'erased']
_sort = ['erased', 'proged', 'readed'] _sort = ['erased', 'proged', 'readed']
_types = {'readed': CsvInt, 'proged': CsvInt, 'erased': CsvInt} _types = {'readed': CsvInt, 'proged': CsvInt, 'erased': CsvInt}
_z = 'z'
_children = 'children' _children = 'children'
__slots__ = () __slots__ = ()
@@ -887,14 +888,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -916,12 +917,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1:
+6 -9
View File
@@ -150,6 +150,7 @@ class StackResult(co.namedtuple('StackResult', [
_fields = ['frame', 'limit'] _fields = ['frame', 'limit']
_sort = ['limit', 'frame'] _sort = ['limit', 'frame']
_types = {'frame': CsvInt, 'limit': CsvInt} _types = {'frame': CsvInt, 'limit': CsvInt}
_z = 'z'
_children = 'children' _children = 'children'
_notes = 'notes' _notes = 'notes'
@@ -555,14 +556,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -584,12 +585,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1:
+6 -9
View File
@@ -150,6 +150,7 @@ class StructResult(co.namedtuple('StructResult', [
_fields = ['off', 'size', 'align'] _fields = ['off', 'size', 'align']
_sort = ['size', 'align'] _sort = ['size', 'align']
_types = {'off': CsvInt, 'size': CsvInt, 'align': CsvInt} _types = {'off': CsvInt, 'size': CsvInt, 'align': CsvInt}
_z = 'z'
_children = 'children' _children = 'children'
__slots__ = () __slots__ = ()
@@ -703,14 +704,14 @@ def fold(Result, results, *,
return folded return folded
def hotify(Result, results, *, def hotify(Result, results, *,
enumerates=None,
depth=1, depth=1,
hot=None, hot=None,
**_): **_):
# note! hotifying risks confusion if you don't enumerate/have a # note! hotifying risks confusion if you don't have a z field, since
# z field, since it will allow folding across recursive boundaries # it will allow folding across recursive boundaries
# hotify only makes sense for recursive results # hotify only makes sense for recursive results
assert hasattr(Result, '_z')
assert hasattr(Result, '_children') assert hasattr(Result, '_children')
results_ = [] results_ = []
@@ -732,12 +733,8 @@ def hotify(Result, results, *,
for k_ in ([k] if k else Result._sort))) for k_ in ([k] if k else Result._sort)))
for k, reverse in it.chain(hot, [(None, False)]))) for k, reverse in it.chain(hot, [(None, False)])))
hot_.append(r._replace(**( # flatten, dropping children
# enumerate? hot_.append(r._replace(**{Result._children: []}))
({e: len(hot_) for e in enumerates}
if enumerates is not None
else {})
| {Result._children: []})))
# recurse? # recurse?
if depth_ > 1: if depth_ > 1: