1#!/usr/bin/env python 2 3from __future__ import print_function 4 5import argparse 6import errno 7import functools 8import html 9import io 10from multiprocessing import cpu_count 11import os.path 12import re 13import shutil 14import sys 15 16from pygments import highlight 17from pygments.lexers.c_cpp import CppLexer 18from pygments.formatters import HtmlFormatter 19 20import optpmap 21import optrecord 22 23 24desc = """Generate HTML output to visualize optimization records from the YAML files 25generated with -fsave-optimization-record and -fdiagnostics-show-hotness. 26 27The tools requires PyYAML and Pygments Python packages.""" 28 29 30# This allows passing the global context to the child processes. 31class Context: 32 def __init__(self, caller_loc=dict()): 33 # Map function names to their source location for function where inlining happened 34 self.caller_loc = caller_loc 35 36 37context = Context() 38 39 40def suppress(remark): 41 if remark.Name == "sil.Specialized": 42 return remark.getArgDict()["Function"][0].startswith('"Swift.') 43 elif remark.Name == "sil.Inlined": 44 return remark.getArgDict()["Callee"][0].startswith( 45 ('"Swift.', '"specialized Swift.') 46 ) 47 return False 48 49 50class SourceFileRenderer: 51 def __init__(self, source_dir, output_dir, filename, no_highlight): 52 self.filename = filename 53 existing_filename = None 54 if os.path.exists(filename): 55 existing_filename = filename 56 else: 57 fn = os.path.join(source_dir, filename) 58 if os.path.exists(fn): 59 existing_filename = fn 60 61 self.no_highlight = no_highlight 62 self.stream = io.open( 63 os.path.join(output_dir, optrecord.html_file_name(filename)), 64 "w", 65 encoding="utf-8", 66 ) 67 if existing_filename: 68 self.source_stream = io.open(existing_filename, encoding="utf-8") 69 else: 70 self.source_stream = None 71 print( 72 """ 73<html> 74<h1>Unable to locate file {}</h1> 75</html> 76 """.format( 77 filename 78 ), 79 file=self.stream, 80 ) 81 82 self.html_formatter = HtmlFormatter(encoding="utf-8") 83 self.cpp_lexer = CppLexer(stripnl=False) 84 85 def render_source_lines(self, stream, line_remarks): 86 file_text = stream.read() 87 88 if self.no_highlight: 89 html_highlighted = file_text 90 else: 91 html_highlighted = highlight(file_text, self.cpp_lexer, self.html_formatter) 92 93 # Note that the API is different between Python 2 and 3. On 94 # Python 3, pygments.highlight() returns a bytes object, so we 95 # have to decode. On Python 2, the output is str but since we 96 # support unicode characters and the output streams is unicode we 97 # decode too. 98 html_highlighted = html_highlighted.decode("utf-8") 99 100 # Take off the header and footer, these must be 101 # reapplied line-wise, within the page structure 102 html_highlighted = html_highlighted.replace( 103 '<div class="highlight"><pre>', "" 104 ) 105 html_highlighted = html_highlighted.replace("</pre></div>", "") 106 107 for (linenum, html_line) in enumerate(html_highlighted.split("\n"), start=1): 108 print( 109 """ 110<tr> 111<td><a name=\"L{linenum}\">{linenum}</a></td> 112<td></td> 113<td></td> 114<td><div class="highlight"><pre>{html_line}</pre></div></td> 115</tr>""".format( 116 **locals() 117 ), 118 file=self.stream, 119 ) 120 121 for remark in line_remarks.get(linenum, []): 122 if not suppress(remark): 123 self.render_inline_remarks(remark, html_line) 124 125 def render_inline_remarks(self, r, line): 126 inlining_context = r.DemangledFunctionName 127 dl = context.caller_loc.get(r.Function) 128 if dl: 129 dl_dict = dict(list(dl)) 130 link = optrecord.make_link(dl_dict["File"], dl_dict["Line"] - 2) 131 inlining_context = "<a href={link}>{r.DemangledFunctionName}</a>".format( 132 **locals() 133 ) 134 135 # Column is the number of characters *including* tabs, keep those and 136 # replace everything else with spaces. 137 indent = line[: max(r.Column, 1) - 1] 138 indent = re.sub("\S", " ", indent) 139 140 # Create expanded message and link if we have a multiline message. 141 lines = r.message.split("\n") 142 if len(lines) > 1: 143 expand_link = '<a style="text-decoration: none;" href="" onclick="toggleExpandedMessage(this); return false;">+</a>' 144 message = lines[0] 145 expand_message = """ 146<div class="full-info" style="display:none;"> 147 <div class="col-left"><pre style="display:inline">{}</pre></div> 148 <div class="expanded col-left"><pre>{}</pre></div> 149</div>""".format( 150 indent, "\n".join(lines[1:]) 151 ) 152 else: 153 expand_link = "" 154 expand_message = "" 155 message = r.message 156 print( 157 """ 158<tr> 159<td></td> 160<td>{r.RelativeHotness}</td> 161<td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td> 162<td><pre style="display:inline">{indent}</pre><span class=\"column-entry-yellow\">{expand_link} {message} </span>{expand_message}</td> 163<td class=\"column-entry-yellow\">{inlining_context}</td> 164</tr>""".format( 165 **locals() 166 ), 167 file=self.stream, 168 ) 169 170 def render(self, line_remarks): 171 if not self.source_stream: 172 return 173 174 print( 175 """ 176<html> 177<title>{}</title> 178<meta charset="utf-8" /> 179<head> 180<link rel='stylesheet' type='text/css' href='style.css'> 181<script type="text/javascript"> 182/* Simple helper to show/hide the expanded message of a remark. */ 183function toggleExpandedMessage(e) {{ 184 var FullTextElems = e.parentElement.parentElement.getElementsByClassName("full-info"); 185 if (!FullTextElems || FullTextElems.length < 1) {{ 186 return false; 187 }} 188 var FullText = FullTextElems[0]; 189 if (FullText.style.display == 'none') {{ 190 e.innerHTML = '-'; 191 FullText.style.display = 'block'; 192 }} else {{ 193 e.innerHTML = '+'; 194 FullText.style.display = 'none'; 195 }} 196}} 197</script> 198</head> 199<body> 200<div class="centered"> 201<table class="source"> 202<thead> 203<tr> 204<th style="width: 2%">Line</td> 205<th style="width: 3%">Hotness</td> 206<th style="width: 10%">Optimization</td> 207<th style="width: 70%">Source</td> 208<th style="width: 15%">Inline Context</td> 209</tr> 210</thead> 211<tbody>""".format( 212 os.path.basename(self.filename) 213 ), 214 file=self.stream, 215 ) 216 self.render_source_lines(self.source_stream, line_remarks) 217 218 print( 219 """ 220</tbody> 221</table> 222</body> 223</html>""", 224 file=self.stream, 225 ) 226 227 228class IndexRenderer: 229 def __init__( 230 self, output_dir, should_display_hotness, max_hottest_remarks_on_index 231 ): 232 self.stream = io.open( 233 os.path.join(output_dir, "index.html"), "w", encoding="utf-8" 234 ) 235 self.should_display_hotness = should_display_hotness 236 self.max_hottest_remarks_on_index = max_hottest_remarks_on_index 237 238 def render_entry(self, r, odd): 239 escaped_name = html.escape(r.DemangledFunctionName) 240 print( 241 """ 242<tr> 243<td class=\"column-entry-{odd}\"><a href={r.Link}>{r.DebugLocString}</a></td> 244<td class=\"column-entry-{odd}\">{r.RelativeHotness}</td> 245<td class=\"column-entry-{odd}\">{escaped_name}</td> 246<td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td> 247</tr>""".format( 248 **locals() 249 ), 250 file=self.stream, 251 ) 252 253 def render(self, all_remarks): 254 print( 255 """ 256<html> 257<meta charset="utf-8" /> 258<head> 259<link rel='stylesheet' type='text/css' href='style.css'> 260</head> 261<body> 262<div class="centered"> 263<table> 264<tr> 265<td>Source Location</td> 266<td>Hotness</td> 267<td>Function</td> 268<td>Pass</td> 269</tr>""", 270 file=self.stream, 271 ) 272 273 max_entries = None 274 if self.should_display_hotness: 275 max_entries = self.max_hottest_remarks_on_index 276 277 for i, remark in enumerate(all_remarks[:max_entries]): 278 if not suppress(remark): 279 self.render_entry(remark, i % 2) 280 print( 281 """ 282</table> 283</body> 284</html>""", 285 file=self.stream, 286 ) 287 288 289def _render_file(source_dir, output_dir, ctx, no_highlight, entry, filter_): 290 global context 291 context = ctx 292 filename, remarks = entry 293 SourceFileRenderer(source_dir, output_dir, filename, no_highlight).render(remarks) 294 295 296def map_remarks(all_remarks): 297 # Set up a map between function names and their source location for 298 # function where inlining happened 299 for remark in optrecord.itervalues(all_remarks): 300 if ( 301 isinstance(remark, optrecord.Passed) 302 and remark.Pass == "inline" 303 and remark.Name == "Inlined" 304 ): 305 for arg in remark.Args: 306 arg_dict = dict(list(arg)) 307 caller = arg_dict.get("Caller") 308 if caller: 309 try: 310 context.caller_loc[caller] = arg_dict["DebugLoc"] 311 except KeyError: 312 pass 313 314 315def generate_report( 316 all_remarks, 317 file_remarks, 318 source_dir, 319 output_dir, 320 no_highlight, 321 should_display_hotness, 322 max_hottest_remarks_on_index, 323 num_jobs, 324 should_print_progress, 325): 326 try: 327 os.makedirs(output_dir) 328 except OSError as e: 329 if e.errno == errno.EEXIST and os.path.isdir(output_dir): 330 pass 331 else: 332 raise 333 334 if should_print_progress: 335 print("Rendering index page...") 336 if should_display_hotness: 337 sorted_remarks = sorted( 338 optrecord.itervalues(all_remarks), 339 key=lambda r: ( 340 r.Hotness, 341 r.File, 342 r.Line, 343 r.Column, 344 r.PassWithDiffPrefix, 345 r.yaml_tag, 346 r.Function, 347 ), 348 reverse=True, 349 ) 350 else: 351 sorted_remarks = sorted( 352 optrecord.itervalues(all_remarks), 353 key=lambda r: ( 354 r.File, 355 r.Line, 356 r.Column, 357 r.PassWithDiffPrefix, 358 r.yaml_tag, 359 r.Function, 360 ), 361 ) 362 IndexRenderer( 363 output_dir, should_display_hotness, max_hottest_remarks_on_index 364 ).render(sorted_remarks) 365 366 shutil.copy( 367 os.path.join(os.path.dirname(os.path.realpath(__file__)), "style.css"), 368 output_dir, 369 ) 370 371 _render_file_bound = functools.partial( 372 _render_file, source_dir, output_dir, context, no_highlight 373 ) 374 if should_print_progress: 375 print("Rendering HTML files...") 376 optpmap.pmap( 377 _render_file_bound, file_remarks.items(), num_jobs, should_print_progress 378 ) 379 380 381def main(): 382 parser = argparse.ArgumentParser(description=desc) 383 parser.add_argument( 384 "yaml_dirs_or_files", 385 nargs="+", 386 help="List of optimization record files or directories searched " 387 "for optimization record files.", 388 ) 389 parser.add_argument( 390 "--output-dir", 391 "-o", 392 default="html", 393 help="Path to a directory where generated HTML files will be output. " 394 "If the directory does not already exist, it will be created. " 395 '"%(default)s" by default.', 396 ) 397 parser.add_argument( 398 "--jobs", 399 "-j", 400 default=None, 401 type=int, 402 help="Max job count (defaults to %(default)s, the current CPU count)", 403 ) 404 parser.add_argument("--source-dir", "-s", default="", help="set source directory") 405 parser.add_argument( 406 "--no-progress-indicator", 407 "-n", 408 action="store_true", 409 default=False, 410 help="Do not display any indicator of how many YAML files were read " 411 "or rendered into HTML.", 412 ) 413 parser.add_argument( 414 "--max-hottest-remarks-on-index", 415 default=1000, 416 type=int, 417 help="Maximum number of the hottest remarks to appear on the index page", 418 ) 419 parser.add_argument( 420 "--no-highlight", 421 action="store_true", 422 default=False, 423 help="Do not use a syntax highlighter when rendering the source code", 424 ) 425 parser.add_argument( 426 "--demangler", 427 help="Set the demangler to be used (defaults to %s)" 428 % optrecord.Remark.default_demangler, 429 ) 430 431 parser.add_argument( 432 "--filter", 433 default="", 434 help="Only display remarks from passes matching filter expression", 435 ) 436 437 # Do not make this a global variable. Values needed to be propagated through 438 # to individual classes and functions to be portable with multiprocessing across 439 # Windows and non-Windows. 440 args = parser.parse_args() 441 442 print_progress = not args.no_progress_indicator 443 if args.demangler: 444 optrecord.Remark.set_demangler(args.demangler) 445 446 files = optrecord.find_opt_files(*args.yaml_dirs_or_files) 447 if not files: 448 parser.error("No *.opt.yaml files found") 449 sys.exit(1) 450 451 all_remarks, file_remarks, should_display_hotness = optrecord.gather_results( 452 files, args.jobs, print_progress, args.filter 453 ) 454 455 map_remarks(all_remarks) 456 457 generate_report( 458 all_remarks, 459 file_remarks, 460 args.source_dir, 461 args.output_dir, 462 args.no_highlight, 463 should_display_hotness, 464 args.max_hottest_remarks_on_index, 465 args.jobs, 466 print_progress, 467 ) 468 469 470if __name__ == "__main__": 471 main() 472