5 from graphviz import Digraph
14 def get_section_option(section_name, option_name, default=None):
16 if fio_file.has_option(section_name, option_name):
17 return fio_file[section_name][option_name]
21 def get_config_option(section_name, option_name, default=None):
23 if config_file.has_option(section_name, option_name):
24 return config_file[section_name][option_name]
28 def get_header_color(keyword='fio_jobs', default_color='black'):
29 return get_config_option(keyword, 'header_color', default_color)
32 def get_shape_color(keyword='fio_jobs', default_color='black'):
33 return get_config_option(keyword, 'shape_color', default_color)
36 def get_text_color(keyword='fio_jobs', default_color='black'):
37 return get_config_option(keyword, 'text_color', default_color)
40 def get_cluster_color(keyword='fio_jobs', default_color='gray92'):
41 return get_config_option(keyword, 'cluster_color', default_color)
44 def get_header(keyword='fio_jobs'):
45 return get_config_option(keyword, 'header')
48 def get_shape(keyword='fio_jobs'):
49 return get_config_option(keyword, 'shape', 'box')
52 def get_style(keyword='fio_jobs'):
53 return get_config_option(keyword, 'style', 'rounded')
56 def get_cluster_style(keyword='fio_jobs'):
57 return get_config_option(keyword, 'cluster_style', 'filled')
60 def get_specific_options(engine):
63 return get_config_option('ioengine_{}'.format(engine), 'specific_options', '').split(' ')
66 def 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:
76 display = '{} = {}'.format(display, section[option])
78 # Adding jobs's options into the box, darkgreen is the default color
80 color = color_override
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)
88 def render_options(fio_file, section_name):
89 """Render all options of a section."""
90 display = section_name
91 section = fio_file[section_name]
93 # Add a multiplier to the section_name if numjobs is set
94 numjobs = int(get_section_option(section_name, 'numjobs', '1'))
97 get_style('numjobs').format(
98 get_text_color('numjobs'), numjobs)
101 label = get_config_option('fio_jobs', 'title_style').format(display)
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']
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)
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'))
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):
125 label, display = render_option(section, label, display, option)
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)
132 # End of the box content
137 def render_section(current_graph, fio_file, section_name, label):
138 """Render the section."""
140 section = fio_file[section_name]
142 # Let's render the box associated to a job
143 current_graph.node(section_name, label,
145 color=get_shape_color(),
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'])
154 current_graph.edge(section_name, section_name, attr)
157 def 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
164 new_graph = Digraph(name=cluster_name, graph_attr=attr)
165 new_graph.attr(style=get_cluster_style(),
166 color=get_cluster_color())
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)
191 def fio_to_graphviz(filename, format):
192 """Compute the graphviz graph from the fio file."""
194 # Let's read the fio file
196 fio_file = configparser.RawConfigParser(
198 default_section="global",
199 inline_comment_prefixes="'#', ';'")
200 fio_file.read(filename)
202 # Prepare the main graph object
203 # Let's define the header of the document
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)
211 main_graph.subgraph(create_legend())
213 # By default all jobs are run in parallel and depends on "global"
214 depends_on = fio_file.default_section
216 # The previous section is by default the global section
217 previous_section = fio_file.default_section
219 current_graph = main_graph
221 # The first job will be a new execution group
222 new_execution_group = True
224 # Let's iterate on all sections to create links between them
225 for section_name in fio_file.sections():
226 # The current section
227 section = fio_file[section_name]
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
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)
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))
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)
254 # The current section become the parent of the next one
255 previous_section = section_name
257 # We are by default in the same execution group
258 new_execution_group = False
260 # The last subgraph isn't rendered yet
261 main_graph.subgraph(current_graph)
263 # Let's return the main graphviz object
267 def setup_commandline():
268 "Prepare the command line."
269 parser = argparse.ArgumentParser()
270 parser.add_argument('--file', action='store',
273 help='the fio file to graph')
274 parser.add_argument('--output', action='store',
276 help='the output filename')
277 parser.add_argument('--format', action='store',
280 help='the output format (see https://graphviz.org/docs/outputs/)')
281 parser.add_argument('--view', action='store_true',
283 help='view the graph')
284 parser.add_argument('--keep', action='store_true',
286 help='keep the graphviz script file')
287 parser.add_argument('--config', action='store',
289 help='the configuration filename')
290 args = parser.parse_args()
296 args = setup_commandline()
298 if args.config is None:
299 if os.path.exists('fiograph.conf'):
300 config_filename = 'fiograph.conf'
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")
306 config_filename = args.config
307 config_file = configparser.RawConfigParser(allow_no_value=True)
308 config_file.read(config_filename)
310 temp_filename = uuid.uuid4().hex
311 image_filename = fio_to_graphviz(args.file, args.format).render(temp_filename, view=args.view)
313 output_filename_stub = args.file
315 output_filename = args.output
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)
322 # allow time for the file to be opened before renaming it
323 os.rename(image_filename, output_filename)
326 os.remove(temp_filename)
328 os.rename(temp_filename, output_filename_stub + '.gv')