Skip to content

Commit a1985e4

Browse files
authored
Add analyze command (#620)
* Add analyze command Analyze is for diagnosing statiscs and properties of the graph you have already built Some specific concepts include build stats Graph coloring and node sizing based on performance * Add graph coloring * Fix scaling and add font scaling * Update traversal for stats * Get things working with exclusions * Style * Add aditional filter argument * Format and update
1 parent 05bf613 commit a1985e4

File tree

2 files changed

+239
-0
lines changed

2 files changed

+239
-0
lines changed

manager/cmd/manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import sys
99

10+
import spack.extensions.manager.manager_cmds.analyze as analyze
1011
import spack.extensions.manager.manager_cmds.binary_finder as binary_finder
1112
import spack.extensions.manager.manager_cmds.cache_query as cache_query
1213
import spack.extensions.manager.manager_cmds.cli_config as cli_config
@@ -39,6 +40,7 @@ def setup_parser(subparser):
3940
cli_config.cli_commands["remove"](sp, _subcommands)
4041
cli_config.cli_commands["list"](sp, _subcommands)
4142

43+
analyze.add_command(sp, _subcommands)
4244
binary_finder.add_command(sp, _subcommands)
4345
cache_query.add_command(sp, _subcommands)
4446
create_env.add_command(sp, _subcommands)

manager/manager_cmds/analyze.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Copyright (c) 2022, National Technology & Engineering Solutions of Sandia,
2+
# LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
3+
# Government retains certain rights in this software.
4+
#
5+
# This software is released under the BSD 3-clause license. See LICENSE file
6+
# for more details.
7+
import json
8+
import os
9+
import statistics
10+
import sys
11+
12+
import spack.cmd
13+
import spack.deptypes as dt
14+
import spack.traverse as traverse
15+
from spack.graph import DotGraphBuilder
16+
17+
command_name = "analyze"
18+
description = "tooling for analyzing statistics of the DAG"
19+
aliases = []
20+
21+
22+
class RequirePackageAttributeVisitor(traverse.BaseVisitor):
23+
"""A visitor that only accepts sparse path"""
24+
25+
def __init__(self, attribute):
26+
super().__init__()
27+
self.attribute = attribute
28+
self.accepted = []
29+
30+
def accept(self, node):
31+
key = node.edge.spec
32+
test = hasattr(key.package, self.attribute)
33+
if test:
34+
self.accepted.append(node)
35+
return test
36+
37+
38+
class OmitSpecsVisitor(traverse.BaseVisitor):
39+
"""A visitor that clips the graph upon satisfied specs"""
40+
41+
def __init__(self, clip_specs):
42+
super().__init__()
43+
self.clip_specs = clip_specs
44+
self.accepted = []
45+
46+
def accept(self, node):
47+
key = node.edge.spec
48+
test = not any(key.satisfies(p) for p in self.clip_specs)
49+
if test:
50+
self.accepted.append(node)
51+
return test
52+
53+
54+
def setup_parser_args(subparser):
55+
visitor_types = subparser.add_mutually_exclusive_group()
56+
visitor_types.add_argument(
57+
"--trim-specs",
58+
nargs="+",
59+
default=[],
60+
help="clip the graph at nodes that satisfy these specs",
61+
)
62+
visitor_types.add_argument(
63+
"--require-attribute",
64+
"-r",
65+
help="only include packages that have this package attribute",
66+
)
67+
subparser.add_argument(
68+
"--stats", action="store_true", help="display stats for graph build/install"
69+
)
70+
subparser.add_argument(
71+
"--graph",
72+
action="store_true",
73+
help="generate a dot file of the graph requested",
74+
)
75+
subparser.add_argument(
76+
"--scale-nodes",
77+
"-S",
78+
action="store_true",
79+
help="scale graph nodes relative to the mean install time",
80+
)
81+
subparser.add_argument(
82+
"--color",
83+
"-c",
84+
action="store_true",
85+
help="color graph nodes based on the time to build",
86+
)
87+
88+
89+
def traverse_nodes_with_visitor(specs, visitor):
90+
traverse.traverse_breadth_first_with_visitor(
91+
specs, traverse.CoverNodesVisitor(visitor)
92+
)
93+
return visitor.accepted
94+
95+
96+
def get_timings(spec):
97+
if spec.installed:
98+
timing_files = spec.package.times_log_path
99+
if os.path.isfile(timing_files):
100+
with open(timing_files, "r") as f:
101+
spec_data = json.load(f)
102+
# extract phases
103+
output = {"total": 0.0}
104+
for phase in spec_data["phases"]:
105+
output[phase["name"]] = phase["seconds"]
106+
output["total"] += phase["seconds"]
107+
return output
108+
return None
109+
110+
111+
def compute_dag_stats(specs, visitor, depflag=dt.ALL):
112+
dag_data = {}
113+
nodes = traverse_nodes_with_visitor(specs, visitor)
114+
for node in nodes:
115+
spec_data = get_timings(node.edge.spec)
116+
if spec_data:
117+
for phase, time in spec_data.items():
118+
full_data = dag_data.get(phase, [])
119+
full_data.append(time)
120+
dag_data[phase] = full_data
121+
122+
stats = {}
123+
for key, data in dag_data.items():
124+
stats[key] = {
125+
"mean": statistics.mean(data),
126+
"stddev": statistics.stdev(data) if len(data) > 1 else 0,
127+
"quartiles": statistics.quantiles(data) if len(data) > 1 else [0, 0, 0],
128+
"min": min(data),
129+
"max": max(data),
130+
"sum": sum(data),
131+
}
132+
return stats
133+
134+
135+
class StatsGraphBuilder(DotGraphBuilder):
136+
def __init__(self, stats, to_color=False, to_scale=False):
137+
super().__init__()
138+
self.dag_stats = stats
139+
self.to_color = to_color
140+
self.to_scale = to_scale
141+
142+
def _get_scaling_factor(self, mean, time):
143+
return time / mean
144+
145+
def _get_properties(self, spec):
146+
timings = get_timings(spec)
147+
if timings:
148+
total = timings["total"]
149+
scaling = self._get_scaling_factor(self.dag_stats["mean"], total)
150+
if total < self.dag_stats["stddev"]:
151+
return "lightblue", scaling
152+
elif total <= self.dag_stats["mean"] + self.dag_stats["stddev"]:
153+
return "green", scaling
154+
elif total <= self.dag_stats["mean"] + 2.0 * self.dag_stats["stddev"]:
155+
return "yellow", scaling
156+
else:
157+
return "red", scaling
158+
return "dodgerblue", 1.0
159+
160+
def node_entry(self, node):
161+
color_compute, scale_factor = self._get_properties(node)
162+
x = 3.0
163+
y = 1.0
164+
fontsize = 48
165+
if self.to_scale:
166+
x *= scale_factor
167+
y *= scale_factor
168+
fontsize *= scale_factor
169+
scale_str = f"width={x} height={y} fixedsize=true fontsize={fontsize}"
170+
color_str = f'fillcolor="{color_compute if self.to_color else"lightblue"}"'
171+
172+
return (
173+
node.dag_hash(),
174+
f'[label="{node.format("{name}")}", {color_str}, {scale_str}]',
175+
)
176+
177+
def edge_entry(self, edge):
178+
return (edge.parent.dag_hash(), edge.spec.dag_hash(), None)
179+
180+
181+
def graph_dot(specs, builder, visitor, depflag=dt.ALL, out=None):
182+
"""DOT graph of the concrete specs passed as input.
183+
184+
Args:
185+
specs: specs to be represented
186+
builder: builder to use to render the graph
187+
depflag: dependency types to consider
188+
out: optional output stream. If None sys.stdout is used
189+
"""
190+
if not specs:
191+
raise ValueError("Must provide specs to graph_dot")
192+
193+
if out is None:
194+
out = sys.stdout
195+
196+
root_edges = traverse.with_artificial_edges(specs)
197+
198+
for edge in traverse.traverse_breadth_first_edges_generator(
199+
root_edges, traverse.CoverEdgesVisitor(visitor), root=True, depth=False
200+
):
201+
builder.visit(edge)
202+
203+
out.write(builder.render())
204+
205+
206+
def analyze(parser, args):
207+
env = spack.cmd.require_active_env(cmd_name=command_name)
208+
specs = env.concrete_roots()
209+
210+
visitor = None
211+
if args.trim_specs:
212+
omissions = spack.cmd.parse_specs(args.trim_specs)
213+
visitor = OmitSpecsVisitor(omissions)
214+
if args.require_attribute:
215+
visitor = RequirePackageAttributeVisitor(args.require_attribute)
216+
217+
stats = compute_dag_stats(specs, visitor)
218+
219+
if args.stats:
220+
pretty_stats = json.dumps(stats, indent=4)
221+
sys.stdout.write(pretty_stats)
222+
223+
if args.graph:
224+
# reset visitor from stats computation
225+
visitor.accepted = []
226+
builder = StatsGraphBuilder(stats["total"], args.color, args.scale_nodes)
227+
graph_dot(specs, builder, visitor)
228+
229+
230+
def add_command(parser, command_dict):
231+
subparser = parser.add_parser(
232+
command_name, description=description, help=description, aliases=aliases
233+
)
234+
setup_parser_args(subparser)
235+
command_dict[command_name] = analyze
236+
for alias in aliases:
237+
command_dict[alias] = analyze

0 commit comments

Comments
 (0)