Skip to content

Commit 4dae342

Browse files
andy31415andreilitvinrestyled-commits
authored andcommitted
Add the ability to add a diff for the file size differ (project-chip#37764)
* Start implementing a diff for symbols * Fix up size computations * make things run * Added icicles graph * Also add percent root to individual items * Show name sizes * Better name formatting * Restyled by autopep8 * Make linter happy * add assert for fetch values, clean up asserts a bit * Match statements seem easier to read and provable branches with static analysis * Cleaner code by using express everywhere * Shorter code * Allow some color control too * Restyled by autopep8 --------- Co-authored-by: Andrei Litvin <andreilitvin@google.com> Co-authored-by: Restyled.io <commits@restyled.io>
1 parent a1f97dc commit 4dae342

File tree

1 file changed

+174
-46
lines changed

1 file changed

+174
-46
lines changed

scripts/tools/file_size_from_nm.py

+174-46
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,14 @@
4444
import logging
4545
import re
4646
import subprocess
47-
from dataclasses import dataclass
47+
from dataclasses import dataclass, replace
4848
from enum import Enum, auto
49-
from pathlib import Path
50-
from typing import Optional
49+
from typing import Callable, Optional, Tuple
5150

5251
import click
5352
import coloredlogs
5453
import cxxfilt
5554
import plotly.express as px
56-
import plotly.graph_objects as go
5755

5856
# Supported log levels, mapping string values required for argument
5957
# parsing into logging constants
@@ -65,14 +63,19 @@
6563
}
6664

6765

68-
class ChartStyle(Enum):
69-
TREE_MAP = auto()
70-
SUNBURST = auto()
66+
__CHART_STYLES__ = {
67+
"treemap": px.treemap,
68+
"sunburst": px.sunburst,
69+
"icicle": px.icicle,
70+
}
7171

7272

73-
__CHART_STYLES__ = {
74-
"treemap": ChartStyle.TREE_MAP,
75-
"sunburst": ChartStyle.SUNBURST,
73+
# Scales from https://plotly.com/python/builtin-colorscales/
74+
__COLOR_SCALES__ = {
75+
"none": None,
76+
"tempo": px.colors.sequential.tempo,
77+
"blues": px.colors.sequential.Blues,
78+
"plasma": px.colors.sequential.Plasma_r,
7679
}
7780

7881

@@ -82,12 +85,12 @@ class FetchStyle(Enum):
8285

8386

8487
__FETCH_STYLES__ = {
85-
"nm": ChartStyle.TREE_MAP,
86-
"objdump": ChartStyle.SUNBURST,
88+
"nm": FetchStyle.NM,
89+
"objdump": FetchStyle.OBJDUMP,
8790
}
8891

8992

90-
@dataclass
93+
@dataclass(frozen=True)
9194
class Symbol:
9295
name: str
9396
symbol_type: str
@@ -363,7 +366,8 @@ def build_treemap(
363366
name: str,
364367
symbols: list[Symbol],
365368
separator: str,
366-
style: ChartStyle,
369+
figure_generator: Callable,
370+
color: Optional[list[str]],
367371
max_depth: int,
368372
zoom: Optional[str],
369373
strip: Optional[str],
@@ -377,7 +381,7 @@ def build_treemap(
377381
root = f"FILE: {name}"
378382
if zoom:
379383
root = root + f" (FILTER: {zoom})"
380-
data: dict[str, list] = dict(name=[root], parent=[""], size=[0], hover=[""])
384+
data: dict[str, list] = dict(name=[root], parent=[""], size=[0], hover=[""], name_with_size=[""], short_name=[""])
381385

382386
known_parents: set[str] = set()
383387
total_sizes: dict = {}
@@ -417,6 +421,8 @@ def build_treemap(
417421
data["parent"].append(partial if partial else root)
418422
data["size"].append(0)
419423
data["hover"].append(next_value)
424+
data["name_with_size"].append("")
425+
data["short_name"].append(name)
420426
total_sizes[next_value] = total_sizes.get(next_value, 0) + symbol.size
421427
partial = next_value
422428

@@ -425,32 +431,64 @@ def build_treemap(
425431
data["parent"].append(partial if partial else root)
426432
data["size"].append(symbol.size)
427433
data["hover"].append(f"{symbol.name} of type {symbol.symbol_type}")
434+
data["name_with_size"].append("")
435+
data["short_name"].append(tree_name[-1])
428436

429437
for idx, label in enumerate(data["name"]):
430438
if data["size"][idx] == 0:
431-
data["hover"][idx] = f"{label}: {total_sizes.get(label, 0)}"
432-
433-
if style == ChartStyle.TREE_MAP:
434-
fig = go.Figure(
435-
go.Treemap(
436-
labels=data["name"],
437-
parents=data["parent"],
438-
values=data["size"],
439-
textinfo="label+value+percent parent",
440-
hovertext=data["hover"],
441-
maxdepth=max_depth,
442-
)
443-
)
444-
else:
445-
fig = px.sunburst(
446-
data,
447-
names="name",
448-
parents="parent",
449-
values="size",
450-
maxdepth=max_depth,
451-
)
439+
total_size = total_sizes.get(label, 0)
440+
data["hover"][idx] = f"{label}: {total_size}"
441+
if idx == 0:
442+
data["name_with_size"][idx] = f"{label}: {total_size}"
443+
else:
444+
# The "full name" is generally quite long, so shorten it...
445+
data["name_with_size"][idx] = f"{data["short_name"][idx]}: {total_size}"
446+
else:
447+
# When using object files, the paths hare are the full "foo::bar::....::method"
448+
# so clean them up a bit
449+
short_name = data["short_name"][idx]
450+
451+
# remove namespaces, but keep template parts
452+
# This tries to convert:
453+
# foo::bar::baz(int, double) -> baz(int, double)
454+
# foo::bar::baz<x::y>(int, double) -> baz<x::y>(int, double)
455+
# foo::bar::baz(some::ns:bit, double) -> baz(some::ns::bit, double)
456+
# foo::bar::baz<x::y>(some::ns:bit, double) -> baz<x::y>(some::ns::bit, double)
457+
#
458+
# Remove all before '::', however '::' found before the first of < or (
459+
#
460+
limit1 = short_name.find('<')
461+
limit2 = short_name.find('(')
462+
if limit1 >= 0 and limit1 < limit2:
463+
limit = limit1
464+
else:
465+
limit = limit2
466+
separate_idx = short_name.rfind('::', 0, limit)
467+
if separate_idx:
468+
short_name = short_name[separate_idx+2:]
469+
470+
data["name_with_size"][idx] = f"{short_name}: {data["size"][idx]}"
471+
472+
extra_args = {}
473+
if color is not None:
474+
extra_args['color_continuous_scale'] = color
475+
extra_args['color'] = "size"
476+
477+
fig = figure_generator(
478+
data,
479+
names="name_with_size",
480+
ids="name",
481+
parents="parent",
482+
values="size",
483+
maxdepth=max_depth,
484+
**extra_args,
485+
)
452486

453-
fig.update_traces(root_color="lightgray")
487+
fig.update_traces(
488+
root_color="lightgray",
489+
textinfo="label+value+percent parent+percent root",
490+
hovertext="hover",
491+
)
454492
fig.show()
455493

456494

@@ -660,6 +698,80 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
660698
return symbols
661699

662700

701+
def fetch_symbols(elf_file: str, fetch: FetchStyle) -> Tuple[list[Symbol], str]:
702+
"""Returns the sumbol list and the separator used to split symbols
703+
"""
704+
match fetch:
705+
case FetchStyle.NM:
706+
return symbols_from_nm(elf_file), "::"
707+
case FetchStyle.OBJDUMP:
708+
return symbols_from_objdump(elf_file), '/'
709+
710+
711+
def list_id(tree_path: list[str]) -> str:
712+
"""Converts a tree path in to a single string (so that it is hashable)"""
713+
return "->".join(tree_path)
714+
715+
716+
def compute_symbol_diff(orig: list[Symbol], base: list[Symbol]) -> list[Symbol]:
717+
"""
718+
Generates a NEW set of symbols for the difference between original and base.
719+
720+
Two symbols with the same name are assumed different IF AND ONLY IF they have a different size
721+
between original and base.
722+
723+
Symbols are the same if their "name" if the have the same tree path.
724+
"""
725+
orig_items = dict([(list_id(v.tree_path), v) for v in orig])
726+
base_items = dict([(list_id(v.tree_path), v) for v in base])
727+
728+
unique_paths = set(orig_items.keys()).union(set(base_items.keys()))
729+
730+
result = []
731+
732+
for path in unique_paths:
733+
orig_symbol = orig_items.get(path, None)
734+
base_symbol = base_items.get(path, None)
735+
736+
if not orig_symbol:
737+
if not base_symbol:
738+
raise AssertionError("Internal logic error: paths should be valid somewhere")
739+
740+
result.append(replace(base_symbol,
741+
name=f"REMOVED: {base_symbol.name}",
742+
tree_path=["DECREASE"] + base_symbol.tree_path,
743+
))
744+
continue
745+
746+
if not base_symbol:
747+
result.append(replace(orig_symbol,
748+
name=f"ADDED: {orig_symbol.name}",
749+
tree_path=["INCREASE"] + orig_symbol.tree_path,
750+
))
751+
continue
752+
753+
if orig_symbol.size == base_symbol.size:
754+
# symbols are identical
755+
continue
756+
757+
size_delta = orig_symbol.size - base_symbol.size
758+
759+
if size_delta > 0:
760+
result.append(replace(orig_symbol,
761+
name=f"CHANGED: {orig_symbol.name}",
762+
tree_path=["INCREASE"] + orig_symbol.tree_path,
763+
size=size_delta,
764+
))
765+
else:
766+
result.append(replace(orig_symbol,
767+
name=f"CHANGED: {orig_symbol.name}",
768+
tree_path=["DECREASE"] + orig_symbol.tree_path,
769+
size=-size_delta,
770+
))
771+
772+
return result
773+
774+
663775
@click.command()
664776
@click.option(
665777
"--log-level",
@@ -675,6 +787,13 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
675787
type=click.Choice(list(__CHART_STYLES__.keys()), case_sensitive=False),
676788
help="Style of the chart",
677789
)
790+
@click.option(
791+
"--color",
792+
default="None",
793+
show_default=True,
794+
type=click.Choice(list(__COLOR_SCALES__.keys()), case_sensitive=False),
795+
help="Color display (if any)",
796+
)
678797
@click.option(
679798
"--fetch-via",
680799
default="nm",
@@ -699,28 +818,37 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
699818
default=None,
700819
help="Strip out a tree subset (e.g. ::C)",
701820
)
702-
@click.argument("elf-file", type=Path)
821+
@click.option(
822+
"--diff",
823+
default=None,
824+
type=click.Path(file_okay=True, dir_okay=False, exists=True),
825+
help="Diff against the given file (changes symbols to increase/decrease)",
826+
)
827+
@click.argument("elf-file", type=click.Path(file_okay=True, dir_okay=False, exists=True))
703828
def main(
704829
log_level,
705-
elf_file: Path,
830+
elf_file: str,
706831
display_type: str,
832+
color: str,
707833
fetch_via: str,
708834
max_depth: int,
709835
zoom: Optional[str],
710836
strip: Optional[str],
837+
diff: Optional[str],
711838
):
712839
log_fmt = "%(asctime)s %(levelname)-7s %(message)s"
713840
coloredlogs.install(level=__LOG_LEVELS__[log_level], fmt=log_fmt)
714841

715-
if __FETCH_STYLES__[fetch_via] == FetchStyle.NM:
716-
symbols = symbols_from_nm(elf_file.absolute().as_posix())
717-
separator = "::"
718-
else:
719-
symbols = symbols_from_objdump(elf_file.absolute().as_posix())
720-
separator = "/"
842+
symbols, separator = fetch_symbols(elf_file, __FETCH_STYLES__[fetch_via])
843+
title = elf_file
844+
845+
if diff:
846+
diff_symbols, _ = fetch_symbols(diff, __FETCH_STYLES__[fetch_via])
847+
symbols = compute_symbol_diff(symbols, diff_symbols)
848+
title = f"{elf_file} COMPARED TO {diff}"
721849

722850
build_treemap(
723-
elf_file.name, symbols, separator, __CHART_STYLES__[display_type], max_depth, zoom, strip
851+
title, symbols, separator, __CHART_STYLES__[display_type], __COLOR_SCALES__[color], max_depth, zoom, strip
724852
)
725853

726854

0 commit comments

Comments
 (0)