2 from graphviz import Digraph
11 def get_section_option(section_name, option_name, default=None):
13 if fio_file.has_option(section_name, option_name):
14 return fio_file[section_name][option_name]
18 def get_config_option(section_name, option_name, default=None):
20 if config_file.has_option(section_name, option_name):
21 return config_file[section_name][option_name]
25 def get_header_color(keyword='fio_jobs', default_color='black'):
26 return get_config_option(keyword, 'header_color', default_color)
29 def get_shape_color(keyword='fio_jobs', default_color='black'):
30 return get_config_option(keyword, 'shape_color', default_color)
33 def get_text_color(keyword='fio_jobs', default_color='black'):
34 return get_config_option(keyword, 'text_color', default_color)
37 def get_cluster_color(keyword='fio_jobs', default_color='gray92'):
38 return get_config_option(keyword, 'cluster_color', default_color)
41 def get_header(keyword='fio_jobs'):
42 return get_config_option(keyword, 'header')
45 def get_shape(keyword='fio_jobs'):
46 return get_config_option(keyword, 'shape', 'box')
49 def get_style(keyword='fio_jobs'):
50 return get_config_option(keyword, 'style', 'rounded')
53 def get_cluster_style(keyword='fio_jobs'):
54 return get_config_option(keyword, 'cluster_style', 'filled')
57 def get_specific_options(engine):
60 return get_config_option('ioengine_{}'.format(engine), 'specific_options', '').split(' ')
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:
73 display = '{} = {}'.format(display, section[option])
75 # Adding jobs's options into the box, darkgreen is the default color
77 color = color_override
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)
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]
90 # Add a multiplier to the section_name if numjobs is set
91 numjobs = int(get_section_option(section_name, 'numjobs', '1'))
94 get_style('numjobs').format(
95 get_text_color('numjobs'), numjobs)
98 label = get_config_option('fio_jobs', 'title_style').format(display)
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']
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)
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'))
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):
122 label, display = render_option(section, label, display, option)
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)
129 # End of the box content
134 def render_section(current_graph, fio_file, section_name, label):
135 """Render the section."""
137 section = fio_file[section_name]
139 # Let's render the box associated to a job
140 current_graph.node(section_name, label,
142 color=get_shape_color(),
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'])
151 current_graph.edge(section_name, section_name, attr)
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
161 new_graph = Digraph(name=cluster_name, graph_attr=attr)
162 new_graph.attr(style=get_cluster_style(),
163 color=get_cluster_color())
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)
188 def fio_to_graphviz(filename, format):
189 """Compute the graphviz graph from the fio file."""
191 # Let's read the fio file
193 fio_file = configparser.RawConfigParser(
195 default_section="global",
196 inline_comment_prefixes="'#', ';'")
197 fio_file.read(filename)
199 # Prepare the main graph object
200 # Let's define the header of the document
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)
208 main_graph.subgraph(create_legend())
210 # By default all jobs are run in parallel and depends on "global"
211 depends_on = fio_file.default_section
213 # The previous section is by default the global section
214 previous_section = fio_file.default_section
216 current_graph = main_graph
218 # The first job will be a new execution group
219 new_execution_group = True
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]
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
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)
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))
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)
251 # The current section become the parent of the next one
252 previous_section = section_name
254 # We are by default in the same execution group
255 new_execution_group = False
257 # The last subgraph isn't rendered yet
258 main_graph.subgraph(current_graph)
260 # Let's return the main graphviz object
264 def setup_commandline():
265 "Prepare the command line."
266 parser = argparse.ArgumentParser()
267 parser.add_argument('--file', action='store',
270 help='the fio file to graph')
271 parser.add_argument('--output', action='store',
273 help='the output filename')
274 parser.add_argument('--format', action='store',
277 help='the output format')
278 parser.add_argument('--view', action='store_true',
280 help='view the graph')
281 parser.add_argument('--keep', action='store_true',
283 help='keep the graphviz script file')
284 parser.add_argument('--config', action='store',
286 default='fiograph.conf',
287 help='the configuration filename')
288 args = parser.parse_args()
294 args = setup_commandline()
295 if args.output is None:
296 output_file = args.file
297 output_file = output_file.replace('.fio', '')
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)
304 os.remove(output_file)