tools/fiograph: improve default config file search
[fio.git] / tools / fiograph / fiograph.py
CommitLineData
d61215e0 1#!/usr/bin/env python3
69bb4b00 2import errno
d61215e0
EV
3from graphviz import Digraph
4import argparse
5import configparser
6import os
7
8config_file = None
9fio_file = None
10
11
12def get_section_option(section_name, option_name, default=None):
13 global fio_file
14 if fio_file.has_option(section_name, option_name):
15 return fio_file[section_name][option_name]
16 return default
17
18
19def get_config_option(section_name, option_name, default=None):
20 global config_file
21 if config_file.has_option(section_name, option_name):
22 return config_file[section_name][option_name]
23 return default
24
25
26def get_header_color(keyword='fio_jobs', default_color='black'):
27 return get_config_option(keyword, 'header_color', default_color)
28
29
30def get_shape_color(keyword='fio_jobs', default_color='black'):
31 return get_config_option(keyword, 'shape_color', default_color)
32
33
34def get_text_color(keyword='fio_jobs', default_color='black'):
35 return get_config_option(keyword, 'text_color', default_color)
36
37
38def get_cluster_color(keyword='fio_jobs', default_color='gray92'):
39 return get_config_option(keyword, 'cluster_color', default_color)
40
41
42def get_header(keyword='fio_jobs'):
43 return get_config_option(keyword, 'header')
44
45
46def get_shape(keyword='fio_jobs'):
47 return get_config_option(keyword, 'shape', 'box')
48
49
50def get_style(keyword='fio_jobs'):
51 return get_config_option(keyword, 'style', 'rounded')
52
53
54def get_cluster_style(keyword='fio_jobs'):
55 return get_config_option(keyword, 'cluster_style', 'filled')
56
57
58def get_specific_options(engine):
59 if not engine:
60 return ''
61 return get_config_option('ioengine_{}'.format(engine), 'specific_options', '').split(' ')
62
63
64def render_option(section, label, display, option, color_override=None):
65 # These options are already shown with graphical helpers, no need to report them directly
66 skip_list = ['size', 'stonewall', 'runtime', 'time_based',
67 'numjobs', 'wait_for', 'wait_for_previous']
68 # If the option doesn't exist or if a special handling is already done
69 # don't render it, just return the current state
70 if option in skip_list or option not in section:
71 return label, display
72 display = option
73 if section[option]:
74 display = '{} = {}'.format(display, section[option])
75
76 # Adding jobs's options into the box, darkgreen is the default color
77 if color_override:
78 color = color_override
79 else:
80 color = get_text_color(option, get_text_color('fio_jobs', 'darkgreen'))
81 label += get_config_option('fio_jobs',
82 'item_style').format(color, display)
83 return label, display
84
85
86def render_options(fio_file, section_name):
87 """Render all options of a section."""
88 display = section_name
89 section = fio_file[section_name]
90
91 # Add a multiplier to the section_name if numjobs is set
92 numjobs = int(get_section_option(section_name, 'numjobs', '1'))
93 if numjobs > 1:
94 display = display + \
95 get_style('numjobs').format(
96 get_text_color('numjobs'), numjobs)
97
98 # Header of the box
99 label = get_config_option('fio_jobs', 'title_style').format(display)
100
101 # Let's parse all the options of the current fio thread
102 # Some needs to be printed on top or bottom of the job to ease the read
103 to_early_print = ['exec_prerun', 'ioengine']
104 to_late_print = ['exec_postrun']
105
106 # Let's print the options on top of the box
107 for early_print in to_early_print:
108 label, display = render_option(
109 section, label, display, early_print)
110
111 current_io_engine = get_section_option(
112 section_name, 'ioengine', None)
113 if current_io_engine:
114 # Let's print all specifics options for this engine
115 for specific_option in sorted(get_specific_options(current_io_engine)):
116 label, display = render_option(
117 section, label, display, specific_option, get_config_option('ioengine', 'specific_options_color'))
118
119 # Let's print generic options sorted by name
120 for option in sorted(section):
121 if option in to_early_print or option in to_late_print or option in get_specific_options(current_io_engine):
122 continue
123 label, display = render_option(section, label, display, option)
124
125 # let's print options on the bottom of the box
126 for late_print in to_late_print:
127 label, display = render_option(
128 section, label, display, late_print)
129
130 # End of the box content
131 label += '</table>>'
132 return label
133
134
135def render_section(current_graph, fio_file, section_name, label):
136 """Render the section."""
137 attr = None
138 section = fio_file[section_name]
139
140 # Let's render the box associated to a job
141 current_graph.node(section_name, label,
142 shape=get_shape(),
143 color=get_shape_color(),
144 style=get_style())
145
146 # Let's report the duration of the jobs with a self-loop arrow
147 if 'runtime' in section and 'time_based' in section:
148 attr = 'runtime={}'.format(section['runtime'])
149 elif 'size' in section:
150 attr = 'size={}'.format(section['size'])
151 if attr:
152 current_graph.edge(section_name, section_name, attr)
153
154
155def create_sub_graph(name):
156 """Return a new graph."""
157 # We need to put 'cluster' in the name to ensure graphviz consider it as a cluster
158 cluster_name = 'cluster_' + name
159 # Unset the main graph labels to avoid a recopy in each subgraph
160 attr = {}
161 attr['label'] = ''
162 new_graph = Digraph(name=cluster_name, graph_attr=attr)
163 new_graph.attr(style=get_cluster_style(),
164 color=get_cluster_color())
165 return new_graph
166
167
168def create_legend():
169 """Return a legend."""
170 html_table = "<<table border='0' cellborder='1' cellspacing='0' cellpadding='4'>"
171 html_table += '<tr><td COLSPAN="2"><b>Legend</b></td></tr>'
172 legend_item = '<tr> <td>{}</td> <td><font color="{}">{}</font></td></tr>"'
173 legend_bgcolor_item = '<tr><td>{}</td><td BGCOLOR="{}"></td></tr>'
174 html_table += legend_item.format('numjobs',
175 get_text_color('numjobs'), 'x numjobs')
176 html_table += legend_item.format('generic option',
177 get_text_color(), 'generic option')
178 html_table += legend_item.format('ioengine option',
179 get_text_color('ioengine'), 'ioengine option')
180 html_table += legend_bgcolor_item.format('job', get_shape_color())
181 html_table += legend_bgcolor_item.format(
182 'execution group', get_cluster_color())
183 html_table += '</table>>'
184 legend = Digraph('html_table')
185 legend.node('legend', shape='none', label=html_table)
186 return legend
187
188
189def fio_to_graphviz(filename, format):
190 """Compute the graphviz graph from the fio file."""
191
192 # Let's read the fio file
193 global fio_file
194 fio_file = configparser.RawConfigParser(
195 allow_no_value=True,
196 default_section="global",
197 inline_comment_prefixes="'#', ';'")
198 fio_file.read(filename)
199
200 # Prepare the main graph object
201 # Let's define the header of the document
202 attrs = {}
203 attrs['labelloc'] = 't'
204 attrs['label'] = get_header().format(
205 get_header_color(), os.path.basename(filename))
206 main_graph = Digraph(engine='dot', graph_attr=attrs, format=format)
207
208 # Let's add a legend
209 main_graph.subgraph(create_legend())
210
211 # By default all jobs are run in parallel and depends on "global"
212 depends_on = fio_file.default_section
213
214 # The previous section is by default the global section
215 previous_section = fio_file.default_section
216
217 current_graph = main_graph
218
219 # The first job will be a new execution group
220 new_execution_group = True
221
fc002f14 222 # Let's iterate on all sections to create links between them
d61215e0
EV
223 for section_name in fio_file.sections():
224 # The current section
225 section = fio_file[section_name]
226
227 # If the current section is waiting the previous job
228 if ('stonewall' or 'wait_for_previous') in section:
229 # let's remember what was the previous job we depend on
230 depends_on = previous_section
231 new_execution_group = True
232 elif 'wait_for' in section:
233 # This sections depends on a named section pointed by wait_for
234 depends_on = section['wait_for']
235 new_execution_group = True
236
237 if new_execution_group:
238 # Let's link the current graph with the main one
239 main_graph.subgraph(current_graph)
240 # Let's create a new graph to represent all the incoming jobs running at the same time
241 current_graph = create_sub_graph(section_name)
242
243 # Let's render the current section in its execution group
244 render_section(current_graph, fio_file, section_name,
245 render_options(fio_file, section_name))
246
247 # Let's trace the link between this job and the one it depends on
248 # If we depend on 'global', we can avoid doing adding an arrow as we don't want to see 'global'
249 if depends_on != fio_file.default_section:
250 current_graph.edge(depends_on, section_name)
251
252 # The current section become the parent of the next one
253 previous_section = section_name
254
255 # We are by default in the same execution group
256 new_execution_group = False
257
258 # The last subgraph isn't rendered yet
259 main_graph.subgraph(current_graph)
260
261 # Let's return the main graphviz object
262 return main_graph
263
264
265def setup_commandline():
266 "Prepare the command line."
267 parser = argparse.ArgumentParser()
268 parser.add_argument('--file', action='store',
269 type=str,
270 required=True,
271 help='the fio file to graph')
272 parser.add_argument('--output', action='store',
273 type=str,
274 help='the output filename')
275 parser.add_argument('--format', action='store',
276 type=str,
277 default='png',
0d672d86 278 help='the output format (see https://graphviz.org/docs/outputs/)')
d61215e0
EV
279 parser.add_argument('--view', action='store_true',
280 default=False,
281 help='view the graph')
282 parser.add_argument('--keep', action='store_true',
283 default=False,
284 help='keep the graphviz script file')
285 parser.add_argument('--config', action='store',
286 type=str,
d61215e0
EV
287 help='the configuration filename')
288 args = parser.parse_args()
289 return args
290
291
292def main():
293 global config_file
294 args = setup_commandline()
69bb4b00 295
d61215e0 296 if args.output is None:
04cd8b6f 297 output_file = args.file
24ff9393
VF
298 if output_file.endswith('.fio'):
299 output_file = output_file[:-4]
04cd8b6f
DLM
300 else:
301 output_file = args.output
69bb4b00
VF
302
303 if args.config is None:
304 if os.path.exists('fiograph.conf'):
305 config_filename = 'fiograph.conf'
306 else:
307 config_filename = os.path.join(os.path.dirname(__file__), 'fiograph.conf')
308 if not os.path.exists(config_filename):
309 raise FileNotFoundError("Cannot locate configuration file")
310 else:
311 config_filename = args.config
d61215e0 312 config_file = configparser.RawConfigParser(allow_no_value=True)
69bb4b00
VF
313 config_file.read(config_filename)
314
d61215e0
EV
315 fio_to_graphviz(args.file, args.format).render(output_file, view=args.view)
316 if not args.keep:
317 os.remove(output_file)
318
319
320main()