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