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