44
44
import logging
45
45
import re
46
46
import subprocess
47
- from dataclasses import dataclass
47
+ from dataclasses import dataclass , replace
48
48
from enum import Enum , auto
49
- from pathlib import Path
50
- from typing import Optional
49
+ from typing import Callable , Optional , Tuple
51
50
52
51
import click
53
52
import coloredlogs
54
53
import cxxfilt
55
54
import plotly .express as px
56
- import plotly .graph_objects as go
57
55
58
56
# Supported log levels, mapping string values required for argument
59
57
# parsing into logging constants
65
63
}
66
64
67
65
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
+ }
71
71
72
72
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 ,
76
79
}
77
80
78
81
@@ -82,12 +85,12 @@ class FetchStyle(Enum):
82
85
83
86
84
87
__FETCH_STYLES__ = {
85
- "nm" : ChartStyle . TREE_MAP ,
86
- "objdump" : ChartStyle . SUNBURST ,
88
+ "nm" : FetchStyle . NM ,
89
+ "objdump" : FetchStyle . OBJDUMP ,
87
90
}
88
91
89
92
90
- @dataclass
93
+ @dataclass ( frozen = True )
91
94
class Symbol :
92
95
name : str
93
96
symbol_type : str
@@ -363,7 +366,8 @@ def build_treemap(
363
366
name : str ,
364
367
symbols : list [Symbol ],
365
368
separator : str ,
366
- style : ChartStyle ,
369
+ figure_generator : Callable ,
370
+ color : Optional [list [str ]],
367
371
max_depth : int ,
368
372
zoom : Optional [str ],
369
373
strip : Optional [str ],
@@ -377,7 +381,7 @@ def build_treemap(
377
381
root = f"FILE: { name } "
378
382
if zoom :
379
383
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 = [ "" ] )
381
385
382
386
known_parents : set [str ] = set ()
383
387
total_sizes : dict = {}
@@ -417,6 +421,8 @@ def build_treemap(
417
421
data ["parent" ].append (partial if partial else root )
418
422
data ["size" ].append (0 )
419
423
data ["hover" ].append (next_value )
424
+ data ["name_with_size" ].append ("" )
425
+ data ["short_name" ].append (name )
420
426
total_sizes [next_value ] = total_sizes .get (next_value , 0 ) + symbol .size
421
427
partial = next_value
422
428
@@ -425,32 +431,64 @@ def build_treemap(
425
431
data ["parent" ].append (partial if partial else root )
426
432
data ["size" ].append (symbol .size )
427
433
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 ])
428
436
429
437
for idx , label in enumerate (data ["name" ]):
430
438
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
+ )
452
486
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
+ )
454
492
fig .show ()
455
493
456
494
@@ -660,6 +698,80 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
660
698
return symbols
661
699
662
700
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
+
663
775
@click .command ()
664
776
@click .option (
665
777
"--log-level" ,
@@ -675,6 +787,13 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
675
787
type = click .Choice (list (__CHART_STYLES__ .keys ()), case_sensitive = False ),
676
788
help = "Style of the chart" ,
677
789
)
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
+ )
678
797
@click .option (
679
798
"--fetch-via" ,
680
799
default = "nm" ,
@@ -699,28 +818,37 @@ def symbols_from_nm(elf_file: str) -> list[Symbol]:
699
818
default = None ,
700
819
help = "Strip out a tree subset (e.g. ::C)" ,
701
820
)
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 ))
703
828
def main (
704
829
log_level ,
705
- elf_file : Path ,
830
+ elf_file : str ,
706
831
display_type : str ,
832
+ color : str ,
707
833
fetch_via : str ,
708
834
max_depth : int ,
709
835
zoom : Optional [str ],
710
836
strip : Optional [str ],
837
+ diff : Optional [str ],
711
838
):
712
839
log_fmt = "%(asctime)s %(levelname)-7s %(message)s"
713
840
coloredlogs .install (level = __LOG_LEVELS__ [log_level ], fmt = log_fmt )
714
841
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 } "
721
849
722
850
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
724
852
)
725
853
726
854
0 commit comments