| #!/usr/bin/env python | 
 |  | 
 | from __future__ import print_function | 
 |  | 
 | import argparse | 
 | import errno | 
 | import functools | 
 | import html | 
 | import io | 
 | from multiprocessing import cpu_count | 
 | import os.path | 
 | import re | 
 | import shutil | 
 | import sys | 
 |  | 
 | from pygments import highlight | 
 | from pygments.lexers.c_cpp import CppLexer | 
 | from pygments.formatters import HtmlFormatter | 
 |  | 
 | import optpmap | 
 | import optrecord | 
 |  | 
 |  | 
 | desc = """Generate HTML output to visualize optimization records from the YAML files | 
 | generated with -fsave-optimization-record and -fdiagnostics-show-hotness. | 
 |  | 
 | The tools requires PyYAML and Pygments Python packages.""" | 
 |  | 
 |  | 
 | # This allows passing the global context to the child processes. | 
 | class Context: | 
 |     def __init__(self, caller_loc=dict()): | 
 |         # Map function names to their source location for function where inlining happened | 
 |         self.caller_loc = caller_loc | 
 |  | 
 |  | 
 | context = Context() | 
 |  | 
 |  | 
 | def suppress(remark): | 
 |     if remark.Name == "sil.Specialized": | 
 |         return remark.getArgDict()["Function"][0].startswith('"Swift.') | 
 |     elif remark.Name == "sil.Inlined": | 
 |         return remark.getArgDict()["Callee"][0].startswith( | 
 |             ('"Swift.', '"specialized Swift.') | 
 |         ) | 
 |     return False | 
 |  | 
 |  | 
 | class SourceFileRenderer: | 
 |     def __init__(self, source_dir, output_dir, filename, no_highlight): | 
 |         self.filename = filename | 
 |         existing_filename = None | 
 |         if os.path.exists(filename): | 
 |             existing_filename = filename | 
 |         else: | 
 |             fn = os.path.join(source_dir, filename) | 
 |             if os.path.exists(fn): | 
 |                 existing_filename = fn | 
 |  | 
 |         self.no_highlight = no_highlight | 
 |         self.stream = io.open( | 
 |             os.path.join(output_dir, optrecord.html_file_name(filename)), | 
 |             "w", | 
 |             encoding="utf-8", | 
 |         ) | 
 |         if existing_filename: | 
 |             self.source_stream = io.open(existing_filename, encoding="utf-8") | 
 |         else: | 
 |             self.source_stream = None | 
 |             print( | 
 |                 """ | 
 | <html> | 
 | <h1>Unable to locate file {}</h1> | 
 | </html> | 
 |             """.format( | 
 |                     filename | 
 |                 ), | 
 |                 file=self.stream, | 
 |             ) | 
 |  | 
 |         self.html_formatter = HtmlFormatter(encoding="utf-8") | 
 |         self.cpp_lexer = CppLexer(stripnl=False) | 
 |  | 
 |     def render_source_lines(self, stream, line_remarks): | 
 |         file_text = stream.read() | 
 |  | 
 |         if self.no_highlight: | 
 |             html_highlighted = file_text | 
 |         else: | 
 |             html_highlighted = highlight(file_text, self.cpp_lexer, self.html_formatter) | 
 |  | 
 |             # Note that the API is different between Python 2 and 3.  On | 
 |             # Python 3, pygments.highlight() returns a bytes object, so we | 
 |             # have to decode.  On Python 2, the output is str but since we | 
 |             # support unicode characters and the output streams is unicode we | 
 |             # decode too. | 
 |             html_highlighted = html_highlighted.decode("utf-8") | 
 |  | 
 |             # Take off the header and footer, these must be | 
 |             #   reapplied line-wise, within the page structure | 
 |             html_highlighted = html_highlighted.replace( | 
 |                 '<div class="highlight"><pre>', "" | 
 |             ) | 
 |             html_highlighted = html_highlighted.replace("</pre></div>", "") | 
 |  | 
 |         for (linenum, html_line) in enumerate(html_highlighted.split("\n"), start=1): | 
 |             print( | 
 |                 """ | 
 | <tr> | 
 | <td><a name=\"L{linenum}\">{linenum}</a></td> | 
 | <td></td> | 
 | <td></td> | 
 | <td><div class="highlight"><pre>{html_line}</pre></div></td> | 
 | </tr>""".format( | 
 |                     **locals() | 
 |                 ), | 
 |                 file=self.stream, | 
 |             ) | 
 |  | 
 |             for remark in line_remarks.get(linenum, []): | 
 |                 if not suppress(remark): | 
 |                     self.render_inline_remarks(remark, html_line) | 
 |  | 
 |     def render_inline_remarks(self, r, line): | 
 |         inlining_context = r.DemangledFunctionName | 
 |         dl = context.caller_loc.get(r.Function) | 
 |         if dl: | 
 |             dl_dict = dict(list(dl)) | 
 |             link = optrecord.make_link(dl_dict["File"], dl_dict["Line"] - 2) | 
 |             inlining_context = "<a href={link}>{r.DemangledFunctionName}</a>".format( | 
 |                 **locals() | 
 |             ) | 
 |  | 
 |         # Column is the number of characters *including* tabs, keep those and | 
 |         # replace everything else with spaces. | 
 |         indent = line[: max(r.Column, 1) - 1] | 
 |         indent = re.sub("\S", " ", indent) | 
 |  | 
 |         # Create expanded message and link if we have a multiline message. | 
 |         lines = r.message.split("\n") | 
 |         if len(lines) > 1: | 
 |             expand_link = '<a style="text-decoration: none;" href="" onclick="toggleExpandedMessage(this); return false;">+</a>' | 
 |             message = lines[0] | 
 |             expand_message = """ | 
 | <div class="full-info" style="display:none;"> | 
 |   <div class="col-left"><pre style="display:inline">{}</pre></div> | 
 |   <div class="expanded col-left"><pre>{}</pre></div> | 
 | </div>""".format( | 
 |                 indent, "\n".join(lines[1:]) | 
 |             ) | 
 |         else: | 
 |             expand_link = "" | 
 |             expand_message = "" | 
 |             message = r.message | 
 |         print( | 
 |             """ | 
 | <tr> | 
 | <td></td> | 
 | <td>{r.RelativeHotness}</td> | 
 | <td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td> | 
 | <td><pre style="display:inline">{indent}</pre><span class=\"column-entry-yellow\">{expand_link} {message} </span>{expand_message}</td> | 
 | <td class=\"column-entry-yellow\">{inlining_context}</td> | 
 | </tr>""".format( | 
 |                 **locals() | 
 |             ), | 
 |             file=self.stream, | 
 |         ) | 
 |  | 
 |     def render(self, line_remarks): | 
 |         if not self.source_stream: | 
 |             return | 
 |  | 
 |         print( | 
 |             """ | 
 | <html> | 
 | <title>{}</title> | 
 | <meta charset="utf-8" /> | 
 | <head> | 
 | <link rel='stylesheet' type='text/css' href='style.css'> | 
 | <script type="text/javascript"> | 
 | /* Simple helper to show/hide the expanded message of a remark. */ | 
 | function toggleExpandedMessage(e) {{ | 
 |   var FullTextElems = e.parentElement.parentElement.getElementsByClassName("full-info"); | 
 |   if (!FullTextElems || FullTextElems.length < 1) {{ | 
 |       return false; | 
 |   }} | 
 |   var FullText = FullTextElems[0]; | 
 |   if (FullText.style.display == 'none') {{ | 
 |     e.innerHTML = '-'; | 
 |     FullText.style.display = 'block'; | 
 |   }} else {{ | 
 |     e.innerHTML = '+'; | 
 |     FullText.style.display = 'none'; | 
 |   }} | 
 | }} | 
 | </script> | 
 | </head> | 
 | <body> | 
 | <div class="centered"> | 
 | <table class="source"> | 
 | <thead> | 
 | <tr> | 
 | <th style="width: 2%">Line</td> | 
 | <th style="width: 3%">Hotness</td> | 
 | <th style="width: 10%">Optimization</td> | 
 | <th style="width: 70%">Source</td> | 
 | <th style="width: 15%">Inline Context</td> | 
 | </tr> | 
 | </thead> | 
 | <tbody>""".format( | 
 |                 os.path.basename(self.filename) | 
 |             ), | 
 |             file=self.stream, | 
 |         ) | 
 |         self.render_source_lines(self.source_stream, line_remarks) | 
 |  | 
 |         print( | 
 |             """ | 
 | </tbody> | 
 | </table> | 
 | </body> | 
 | </html>""", | 
 |             file=self.stream, | 
 |         ) | 
 |  | 
 |  | 
 | class IndexRenderer: | 
 |     def __init__( | 
 |         self, output_dir, should_display_hotness, max_hottest_remarks_on_index | 
 |     ): | 
 |         self.stream = io.open( | 
 |             os.path.join(output_dir, "index.html"), "w", encoding="utf-8" | 
 |         ) | 
 |         self.should_display_hotness = should_display_hotness | 
 |         self.max_hottest_remarks_on_index = max_hottest_remarks_on_index | 
 |  | 
 |     def render_entry(self, r, odd): | 
 |         escaped_name = html.escape(r.DemangledFunctionName) | 
 |         print( | 
 |             """ | 
 | <tr> | 
 | <td class=\"column-entry-{odd}\"><a href={r.Link}>{r.DebugLocString}</a></td> | 
 | <td class=\"column-entry-{odd}\">{r.RelativeHotness}</td> | 
 | <td class=\"column-entry-{odd}\">{escaped_name}</td> | 
 | <td class=\"column-entry-{r.color}\">{r.PassWithDiffPrefix}</td> | 
 | </tr>""".format( | 
 |                 **locals() | 
 |             ), | 
 |             file=self.stream, | 
 |         ) | 
 |  | 
 |     def render(self, all_remarks): | 
 |         print( | 
 |             """ | 
 | <html> | 
 | <meta charset="utf-8" /> | 
 | <head> | 
 | <link rel='stylesheet' type='text/css' href='style.css'> | 
 | </head> | 
 | <body> | 
 | <div class="centered"> | 
 | <table> | 
 | <tr> | 
 | <td>Source Location</td> | 
 | <td>Hotness</td> | 
 | <td>Function</td> | 
 | <td>Pass</td> | 
 | </tr>""", | 
 |             file=self.stream, | 
 |         ) | 
 |  | 
 |         max_entries = None | 
 |         if self.should_display_hotness: | 
 |             max_entries = self.max_hottest_remarks_on_index | 
 |  | 
 |         for i, remark in enumerate(all_remarks[:max_entries]): | 
 |             if not suppress(remark): | 
 |                 self.render_entry(remark, i % 2) | 
 |         print( | 
 |             """ | 
 | </table> | 
 | </body> | 
 | </html>""", | 
 |             file=self.stream, | 
 |         ) | 
 |  | 
 |  | 
 | def _render_file(source_dir, output_dir, ctx, no_highlight, entry, filter_): | 
 |     global context | 
 |     context = ctx | 
 |     filename, remarks = entry | 
 |     SourceFileRenderer(source_dir, output_dir, filename, no_highlight).render(remarks) | 
 |  | 
 |  | 
 | def map_remarks(all_remarks): | 
 |     # Set up a map between function names and their source location for | 
 |     # function where inlining happened | 
 |     for remark in optrecord.itervalues(all_remarks): | 
 |         if ( | 
 |             isinstance(remark, optrecord.Passed) | 
 |             and remark.Pass == "inline" | 
 |             and remark.Name == "Inlined" | 
 |         ): | 
 |             for arg in remark.Args: | 
 |                 arg_dict = dict(list(arg)) | 
 |                 caller = arg_dict.get("Caller") | 
 |                 if caller: | 
 |                     try: | 
 |                         context.caller_loc[caller] = arg_dict["DebugLoc"] | 
 |                     except KeyError: | 
 |                         pass | 
 |  | 
 |  | 
 | def generate_report( | 
 |     all_remarks, | 
 |     file_remarks, | 
 |     source_dir, | 
 |     output_dir, | 
 |     no_highlight, | 
 |     should_display_hotness, | 
 |     max_hottest_remarks_on_index, | 
 |     num_jobs, | 
 |     should_print_progress, | 
 | ): | 
 |     try: | 
 |         os.makedirs(output_dir) | 
 |     except OSError as e: | 
 |         if e.errno == errno.EEXIST and os.path.isdir(output_dir): | 
 |             pass | 
 |         else: | 
 |             raise | 
 |  | 
 |     if should_print_progress: | 
 |         print("Rendering index page...") | 
 |     if should_display_hotness: | 
 |         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, | 
 |         ) | 
 |     else: | 
 |         sorted_remarks = sorted( | 
 |             optrecord.itervalues(all_remarks), | 
 |             key=lambda r: ( | 
 |                 r.File, | 
 |                 r.Line, | 
 |                 r.Column, | 
 |                 r.PassWithDiffPrefix, | 
 |                 r.yaml_tag, | 
 |                 r.Function, | 
 |             ), | 
 |         ) | 
 |     IndexRenderer( | 
 |         output_dir, should_display_hotness, max_hottest_remarks_on_index | 
 |     ).render(sorted_remarks) | 
 |  | 
 |     shutil.copy( | 
 |         os.path.join(os.path.dirname(os.path.realpath(__file__)), "style.css"), | 
 |         output_dir, | 
 |     ) | 
 |  | 
 |     _render_file_bound = functools.partial( | 
 |         _render_file, source_dir, output_dir, context, no_highlight | 
 |     ) | 
 |     if should_print_progress: | 
 |         print("Rendering HTML files...") | 
 |     optpmap.pmap( | 
 |         _render_file_bound, file_remarks.items(), num_jobs, should_print_progress | 
 |     ) | 
 |  | 
 |  | 
 | def main(): | 
 |     parser = argparse.ArgumentParser(description=desc) | 
 |     parser.add_argument( | 
 |         "yaml_dirs_or_files", | 
 |         nargs="+", | 
 |         help="List of optimization record files or directories searched " | 
 |         "for optimization record files.", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--output-dir", | 
 |         "-o", | 
 |         default="html", | 
 |         help="Path to a directory where generated HTML files will be output. " | 
 |         "If the directory does not already exist, it will be created. " | 
 |         '"%(default)s" by default.', | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--jobs", | 
 |         "-j", | 
 |         default=None, | 
 |         type=int, | 
 |         help="Max job count (defaults to %(default)s, the current CPU count)", | 
 |     ) | 
 |     parser.add_argument("--source-dir", "-s", default="", help="set source directory") | 
 |     parser.add_argument( | 
 |         "--no-progress-indicator", | 
 |         "-n", | 
 |         action="store_true", | 
 |         default=False, | 
 |         help="Do not display any indicator of how many YAML files were read " | 
 |         "or rendered into HTML.", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--max-hottest-remarks-on-index", | 
 |         default=1000, | 
 |         type=int, | 
 |         help="Maximum number of the hottest remarks to appear on the index page", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--no-highlight", | 
 |         action="store_true", | 
 |         default=False, | 
 |         help="Do not use a syntax highlighter when rendering the source code", | 
 |     ) | 
 |     parser.add_argument( | 
 |         "--demangler", | 
 |         help="Set the demangler to be used (defaults to %s)" | 
 |         % optrecord.Remark.default_demangler, | 
 |     ) | 
 |  | 
 |     parser.add_argument( | 
 |         "--filter", | 
 |         default="", | 
 |         help="Only display remarks from passes matching filter expression", | 
 |     ) | 
 |  | 
 |     # Do not make this a global variable.  Values needed to be propagated through | 
 |     # to individual classes and functions to be portable with multiprocessing across | 
 |     # Windows and non-Windows. | 
 |     args = parser.parse_args() | 
 |  | 
 |     print_progress = not args.no_progress_indicator | 
 |     if args.demangler: | 
 |         optrecord.Remark.set_demangler(args.demangler) | 
 |  | 
 |     files = optrecord.find_opt_files(*args.yaml_dirs_or_files) | 
 |     if not files: | 
 |         parser.error("No *.opt.yaml files found") | 
 |         sys.exit(1) | 
 |  | 
 |     all_remarks, file_remarks, should_display_hotness = optrecord.gather_results( | 
 |         files, args.jobs, print_progress, args.filter | 
 |     ) | 
 |  | 
 |     map_remarks(all_remarks) | 
 |  | 
 |     generate_report( | 
 |         all_remarks, | 
 |         file_remarks, | 
 |         args.source_dir, | 
 |         args.output_dir, | 
 |         args.no_highlight, | 
 |         should_display_hotness, | 
 |         args.max_hottest_remarks_on_index, | 
 |         args.jobs, | 
 |         print_progress, | 
 |     ) | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |     main() |