| 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() |