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}&nbsp;</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