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} </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