Home | History | Annotate | Download | only in opt-viewer
      1 #!/usr/bin/env python2.7
      2 
      3 from __future__ import print_function
      4 
      5 import argparse
      6 import cgi
      7 import errno
      8 import functools
      9 from multiprocessing import cpu_count
     10 import os.path
     11 import re
     12 import shutil
     13 import sys
     14 
     15 from pygments import highlight
     16 from pygments.lexers.c_cpp import CppLexer
     17 from pygments.formatters import HtmlFormatter
     18 
     19 import optpmap
     20 import optrecord
     21 
     22 
     23 desc = '''Generate HTML output to visualize optimization records from the YAML files
     24 generated with -fsave-optimization-record and -fdiagnostics-show-hotness.
     25 
     26 The tools requires PyYAML and Pygments Python packages.'''
     27 
     28 
     29 # This allows passing the global context to the child processes.
     30 class Context:
     31     def __init__(self, caller_loc = dict()):
     32        # Map function names to their source location for function where inlining happened
     33        self.caller_loc = caller_loc
     34 
     35 context = Context()
     36 
     37 class SourceFileRenderer:
     38     def __init__(self, source_dir, output_dir, filename):
     39         existing_filename = None
     40         if os.path.exists(filename):
     41             existing_filename = filename
     42         else:
     43             fn = os.path.join(source_dir, filename)
     44             if os.path.exists(fn):
     45                 existing_filename = fn
     46 
     47         self.stream = open(os.path.join(output_dir, optrecord.html_file_name(filename)), 'w')
     48         if existing_filename:
     49             self.source_stream = open(existing_filename)
     50         else:
     51             self.source_stream = None
     52             print('''
     53 <html>
     54 <h1>Unable to locate file {}</h1>
     55 </html>
     56             '''.format(filename), file=self.stream)
     57 
     58         self.html_formatter = HtmlFormatter(encoding='utf-8')
     59         self.cpp_lexer = CppLexer(stripnl=False)
     60 
     61     def render_source_lines(self, stream, line_remarks):
     62         file_text = stream.read()
     63         html_highlighted = highlight(
     64             file_text,
     65             self.cpp_lexer,
     66             self.html_formatter)
     67 
     68         # On Python 3, pygments.highlight() returns a bytes object, not a str.
     69         if sys.version_info >= (3, 0):
     70           html_highlighted = html_highlighted.decode('utf-8')
     71 
     72         # Take off the header and footer, these must be
     73         #   reapplied line-wise, within the page structure
     74         html_highlighted = html_highlighted.replace('<div class="highlight"><pre>', '')
     75         html_highlighted = html_highlighted.replace('</pre></div>', '')
     76 
     77         for (linenum, html_line) in enumerate(html_highlighted.split('\n'), start=1):
     78             print('''
     79 <tr>
     80 <td><a name=\"L{linenum}\">{linenum}</a></td>
     81 <td></td>
     82 <td></td>
     83 <td><div class="highlight"><pre>{html_line}</pre></div></td>
     84 </tr>'''.format(**locals()), file=self.stream)
     85 
     86             for remark in line_remarks.get(linenum, []):
     87                 self.render_inline_remarks(remark, html_line)
     88 
     89     def render_inline_remarks(self, r, line):
     90         inlining_context = r.DemangledFunctionName
     91         dl = context.caller_loc.get(r.Function)
     92         if dl:
     93             dl_dict = dict(list(dl))
     94             link = optrecord.make_link(dl_dict['File'], dl_dict['Line'] - 2)
     95             inlining_context = "<a href={link}>{r.DemangledFunctionName}</a>".format(**locals())
     96 
     97         # Column is the number of characters *including* tabs, keep those and
     98         # replace everything else with spaces.
     99         indent = line[:max(r.Column, 1) - 1]
    100         indent = re.sub('\S', ' ', indent)
    101 
    102         print('''
    103 <tr>
    104 <td></td>
    105 <td>{r.RelativeHotness}</td>
    106 <td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td>
    107 <td><pre style="display:inline">{indent}</pre><span class=\"column-entry-yellow\"> {r.message}&nbsp;</span></td>
    108 <td class=\"column-entry-yellow\">{inlining_context}</td>
    109 </tr>'''.format(**locals()), file=self.stream)
    110 
    111     def render(self, line_remarks):
    112         if not self.source_stream:
    113             return
    114 
    115         print('''
    116 <html>
    117 <head>
    118 <link rel='stylesheet' type='text/css' href='style.css'>
    119 </head>
    120 <body>
    121 <div class="centered">
    122 <table>
    123 <tr>
    124 <td>Line</td>
    125 <td>Hotness</td>
    126 <td>Optimization</td>
    127 <td>Source</td>
    128 <td>Inline Context</td>
    129 </tr>''', file=self.stream)
    130         self.render_source_lines(self.source_stream, line_remarks)
    131 
    132         print('''
    133 </table>
    134 </body>
    135 </html>''', file=self.stream)
    136 
    137 
    138 class IndexRenderer:
    139     def __init__(self, output_dir):
    140         self.stream = open(os.path.join(output_dir, 'index.html'), 'w')
    141 
    142     def render_entry(self, r, odd):
    143         escaped_name = cgi.escape(r.DemangledFunctionName)
    144         print('''
    145 <tr>
    146 <td class=\"column-entry-{odd}\"><a href={r.Link}>{r.DebugLocString}</a></td>
    147 <td class=\"column-entry-{odd}\">{r.RelativeHotness}</td>
    148 <td class=\"column-entry-{odd}\">{escaped_name}</td>
    149 <td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td>
    150 </tr>'''.format(**locals()), file=self.stream)
    151 
    152     def render(self, all_remarks):
    153         print('''
    154 <html>
    155 <head>
    156 <link rel='stylesheet' type='text/css' href='style.css'>
    157 </head>
    158 <body>
    159 <div class="centered">
    160 <table>
    161 <tr>
    162 <td>Source Location</td>
    163 <td>Hotness</td>
    164 <td>Function</td>
    165 <td>Pass</td>
    166 </tr>''', file=self.stream)
    167         for i, remark in enumerate(all_remarks):
    168             self.render_entry(remark, i % 2)
    169         print('''
    170 </table>
    171 </body>
    172 </html>''', file=self.stream)
    173 
    174 
    175 def _render_file(source_dir, output_dir, ctx, entry):
    176     global context
    177     context = ctx
    178     filename, remarks = entry
    179     SourceFileRenderer(source_dir, output_dir, filename).render(remarks)
    180 
    181 
    182 def map_remarks(all_remarks):
    183     # Set up a map between function names and their source location for
    184     # function where inlining happened
    185     for remark in optrecord.itervalues(all_remarks):
    186         if isinstance(remark, optrecord.Passed) and remark.Pass == "inline" and remark.Name == "Inlined":
    187             for arg in remark.Args:
    188                 arg_dict = dict(list(arg))
    189                 caller = arg_dict.get('Caller')
    190                 if caller:
    191                     try:
    192                         context.caller_loc[caller] = arg_dict['DebugLoc']
    193                     except KeyError:
    194                         pass
    195 
    196 
    197 def generate_report(all_remarks,
    198                     file_remarks,
    199                     source_dir,
    200                     output_dir,
    201                     should_display_hotness,
    202                     num_jobs,
    203                     should_print_progress):
    204     try:
    205         os.makedirs(output_dir)
    206     except OSError as e:
    207         if e.errno == errno.EEXIST and os.path.isdir(output_dir):
    208             pass
    209         else:
    210             raise
    211 
    212     _render_file_bound = functools.partial(_render_file, source_dir, output_dir, context)
    213     if should_print_progress:
    214         print('Rendering HTML files...')
    215     optpmap.pmap(_render_file_bound,
    216                  file_remarks.items(),
    217                  num_jobs,
    218                  should_print_progress)
    219 
    220     if should_display_hotness:
    221         sorted_remarks = sorted(optrecord.itervalues(all_remarks), key=lambda r: (r.Hotness, r.File, r.Line, r.Column, r.PassWithDiffPrefix, r.yaml_tag, r.Function), reverse=True)
    222     else:
    223         sorted_remarks = sorted(optrecord.itervalues(all_remarks), key=lambda r: (r.File, r.Line, r.Column, r.PassWithDiffPrefix, r.yaml_tag, r.Function))
    224     IndexRenderer(args.output_dir).render(sorted_remarks)
    225 
    226     shutil.copy(os.path.join(os.path.dirname(os.path.realpath(__file__)),
    227             "style.css"), output_dir)
    228 
    229 
    230 if __name__ == '__main__':
    231     parser = argparse.ArgumentParser(description=desc)
    232     parser.add_argument(
    233         'yaml_dirs_or_files',
    234         nargs='+',
    235         help='List of optimization record files or directories searched '
    236              'for optimization record files.')
    237     parser.add_argument(
    238         '--output-dir',
    239         '-o',
    240         default='html',
    241         help='Path to a directory where generated HTML files will be output. '
    242              'If the directory does not already exist, it will be created. '
    243              '"%(default)s" by default.')
    244     parser.add_argument(
    245         '--jobs',
    246         '-j',
    247         default=cpu_count(),
    248         type=int,
    249         help='Max job count (defaults to %(default)s, the current CPU count)')
    250     parser.add_argument(
    251         '-source-dir',
    252         '-s',
    253         default='',
    254         help='set source directory')
    255     parser.add_argument(
    256         '--no-progress-indicator',
    257         '-n',
    258         action='store_true',
    259         default=False,
    260         help='Do not display any indicator of how many YAML files were read '
    261              'or rendered into HTML.')
    262     args = parser.parse_args()
    263 
    264     print_progress = not args.no_progress_indicator
    265 
    266     files = optrecord.find_opt_files(*args.yaml_dirs_or_files)
    267     if not files:
    268         parser.error("No *.opt.yaml files found")
    269         sys.exit(1)
    270 
    271     all_remarks, file_remarks, should_display_hotness = \
    272         optrecord.gather_results(files, args.jobs, print_progress)
    273 
    274     map_remarks(all_remarks)
    275 
    276     generate_report(all_remarks,
    277                     file_remarks,
    278                     args.source_dir,
    279                     args.output_dir,
    280                     should_display_hotness,
    281                     args.jobs,
    282                     print_progress)
    283