Index: llvm/trunk/CMakeLists.txt =================================================================== --- llvm/trunk/CMakeLists.txt +++ llvm/trunk/CMakeLists.txt @@ -660,6 +660,8 @@ message(FATAL_ERROR "Python 2.7 or newer is required") endif() +get_filename_component(PYTHON_BASENAME ${PYTHON_EXECUTABLE} NAME) + ###### # LLVMBuild Integration # Index: llvm/trunk/tools/opt-viewer/CMakeLists.txt =================================================================== --- llvm/trunk/tools/opt-viewer/CMakeLists.txt +++ llvm/trunk/tools/opt-viewer/CMakeLists.txt @@ -1,13 +1,28 @@ set (files + "optpmap.py" + "style.css") + +set (generated_files "opt-diff.py" "opt-stats.py" "opt-viewer.py" - "optpmap.py" - "optrecord.py" - "style.css") + "optrecord.py") + +foreach (file ${generated_files}) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/${file}.in + ${CMAKE_CURRENT_BINARY_DIR}/${file}) +endforeach (file) foreach (file ${files}) install(PROGRAMS ${file} DESTINATION share/opt-viewer COMPONENT opt-viewer) endforeach (file) + + +foreach (file ${generated_files}) + install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/${file} + DESTINATION share/opt-viewer + COMPONENT opt-viewer) +endforeach (file) Index: llvm/trunk/tools/opt-viewer/opt-diff.py =================================================================== --- llvm/trunk/tools/opt-viewer/opt-diff.py +++ llvm/trunk/tools/opt-viewer/opt-diff.py @@ -1,75 +0,0 @@ -#!/usr/bin/env python - -from __future__ import print_function - -desc = '''Generate the difference of two YAML files into a new YAML file (works on -pair of directories too). A new attribute 'Added' is set to True or False -depending whether the entry is added or removed from the first input to the -next. - -The tools requires PyYAML.''' - -import yaml -# Try to use the C parser. -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader - -import optrecord -import argparse -from collections import defaultdict - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=desc) - parser.add_argument( - 'yaml_dir_or_file_1', - help='An optimization record file or a directory searched for optimization ' - 'record files that are used as the old version for the comparison') - parser.add_argument( - 'yaml_dir_or_file_2', - help='An optimization record file or a directory searched for optimization ' - 'record files that are used as the new version for the comparison') - parser.add_argument( - '--jobs', - '-j', - default=None, - type=int, - help='Max job count (defaults to %(default)s, the current CPU count)') - parser.add_argument( - '--max-size', - '-m', - default=100000, - type=int, - help='Maximum number of remarks stored in an output file') - 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.') - parser.add_argument('--output', '-o', default='diff{}.opt.yaml') - args = parser.parse_args() - - files1 = optrecord.find_opt_files(args.yaml_dir_or_file_1) - files2 = optrecord.find_opt_files(args.yaml_dir_or_file_2) - - print_progress = not args.no_progress_indicator - all_remarks1, _, _ = optrecord.gather_results(files1, args.jobs, print_progress) - all_remarks2, _, _ = optrecord.gather_results(files2, args.jobs, print_progress) - - added = set(all_remarks2.values()) - set(all_remarks1.values()) - removed = set(all_remarks1.values()) - set(all_remarks2.values()) - - for r in added: - r.Added = True - for r in removed: - r.Added = False - - result = list(added | removed) - for r in result: - r.recover_yaml_structure() - - for i in range(0, len(result), args.max_size): - with open(args.output.format(i / args.max_size), 'w') as stream: - yaml.dump_all(result[i:i + args.max_size], stream) Index: llvm/trunk/tools/opt-viewer/opt-diff.py.in =================================================================== --- llvm/trunk/tools/opt-viewer/opt-diff.py.in +++ llvm/trunk/tools/opt-viewer/opt-diff.py.in @@ -0,0 +1,75 @@ +#!/usr/bin/env @PYTHON_BASENAME@ + +from __future__ import print_function + +desc = '''Generate the difference of two YAML files into a new YAML file (works on +pair of directories too). A new attribute 'Added' is set to True or False +depending whether the entry is added or removed from the first input to the +next. + +The tools requires PyYAML.''' + +import yaml +# Try to use the C parser. +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + +import optrecord +import argparse +from collections import defaultdict + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=desc) + parser.add_argument( + 'yaml_dir_or_file_1', + help='An optimization record file or a directory searched for optimization ' + 'record files that are used as the old version for the comparison') + parser.add_argument( + 'yaml_dir_or_file_2', + help='An optimization record file or a directory searched for optimization ' + 'record files that are used as the new version for the comparison') + parser.add_argument( + '--jobs', + '-j', + default=None, + type=int, + help='Max job count (defaults to %(default)s, the current CPU count)') + parser.add_argument( + '--max-size', + '-m', + default=100000, + type=int, + help='Maximum number of remarks stored in an output file') + 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.') + parser.add_argument('--output', '-o', default='diff{}.opt.yaml') + args = parser.parse_args() + + files1 = optrecord.find_opt_files(args.yaml_dir_or_file_1) + files2 = optrecord.find_opt_files(args.yaml_dir_or_file_2) + + print_progress = not args.no_progress_indicator + all_remarks1, _, _ = optrecord.gather_results(files1, args.jobs, print_progress) + all_remarks2, _, _ = optrecord.gather_results(files2, args.jobs, print_progress) + + added = set(all_remarks2.values()) - set(all_remarks1.values()) + removed = set(all_remarks1.values()) - set(all_remarks2.values()) + + for r in added: + r.Added = True + for r in removed: + r.Added = False + + result = list(added | removed) + for r in result: + r.recover_yaml_structure() + + for i in range(0, len(result), args.max_size): + with open(args.output.format(i / args.max_size), 'w') as stream: + yaml.dump_all(result[i:i + args.max_size], stream) Index: llvm/trunk/tools/opt-viewer/opt-stats.py =================================================================== --- llvm/trunk/tools/opt-viewer/opt-stats.py +++ llvm/trunk/tools/opt-viewer/opt-stats.py @@ -1,78 +0,0 @@ -#!/usr/bin/env python - -from __future__ import print_function - -desc = '''Generate statistics about optimization records from the YAML files -generated with -fsave-optimization-record and -fdiagnostics-show-hotness. - -The tools requires PyYAML and Pygments Python packages.''' - -import optrecord -import argparse -import operator -from collections import defaultdict -from multiprocessing import cpu_count, Pool - -try: - from guppy import hpy - hp = hpy() -except ImportError: - print("Memory consumption not shown because guppy is not installed") - hp = None - -if __name__ == '__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( - '--jobs', - '-j', - default=None, - type=int, - help='Max job count (defaults to %(default)s, the current CPU count)') - 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.') - args = parser.parse_args() - - print_progress = not args.no_progress_indicator - - 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, _ = optrecord.gather_results( - files, args.jobs, print_progress) - if print_progress: - print('\n') - - bypass = defaultdict(int) - byname = defaultdict(int) - for r in optrecord.itervalues(all_remarks): - bypass[r.Pass] += 1 - byname[r.Pass + "/" + r.Name] += 1 - - total = len(all_remarks) - print("{:24s} {:10d}".format("Total number of remarks", total)) - if hp: - h = hp.heap() - print("{:24s} {:10d}".format("Memory per remark", - h.size / len(all_remarks))) - print('\n') - - print("Top 10 remarks by pass:") - for (passname, count) in sorted(bypass.items(), key=operator.itemgetter(1), - reverse=True)[:10]: - print(" {:30s} {:2.0f}%". format(passname, count * 100. / total)) - - print("\nTop 10 remarks:") - for (name, count) in sorted(byname.items(), key=operator.itemgetter(1), - reverse=True)[:10]: - print(" {:30s} {:2.0f}%". format(name, count * 100. / total)) Index: llvm/trunk/tools/opt-viewer/opt-stats.py.in =================================================================== --- llvm/trunk/tools/opt-viewer/opt-stats.py.in +++ llvm/trunk/tools/opt-viewer/opt-stats.py.in @@ -0,0 +1,78 @@ +#!/usr/bin/env @PYTHON_BASENAME@ + +from __future__ import print_function + +desc = '''Generate statistics about optimization records from the YAML files +generated with -fsave-optimization-record and -fdiagnostics-show-hotness. + +The tools requires PyYAML and Pygments Python packages.''' + +import optrecord +import argparse +import operator +from collections import defaultdict +from multiprocessing import cpu_count, Pool + +try: + from guppy import hpy + hp = hpy() +except ImportError: + print("Memory consumption not shown because guppy is not installed") + hp = None + +if __name__ == '__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( + '--jobs', + '-j', + default=None, + type=int, + help='Max job count (defaults to %(default)s, the current CPU count)') + 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.') + args = parser.parse_args() + + print_progress = not args.no_progress_indicator + + 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, _ = optrecord.gather_results( + files, args.jobs, print_progress) + if print_progress: + print('\n') + + bypass = defaultdict(int) + byname = defaultdict(int) + for r in optrecord.itervalues(all_remarks): + bypass[r.Pass] += 1 + byname[r.Pass + "/" + r.Name] += 1 + + total = len(all_remarks) + print("{:24s} {:10d}".format("Total number of remarks", total)) + if hp: + h = hp.heap() + print("{:24s} {:10d}".format("Memory per remark", + h.size / len(all_remarks))) + print('\n') + + print("Top 10 remarks by pass:") + for (passname, count) in sorted(bypass.items(), key=operator.itemgetter(1), + reverse=True)[:10]: + print(" {:30s} {:2.0f}%". format(passname, count * 100. / total)) + + print("\nTop 10 remarks:") + for (name, count) in sorted(byname.items(), key=operator.itemgetter(1), + reverse=True)[:10]: + print(" {:30s} {:2.0f}%". format(name, count * 100. / total)) Index: llvm/trunk/tools/opt-viewer/opt-viewer.py =================================================================== --- llvm/trunk/tools/opt-viewer/opt-viewer.py +++ llvm/trunk/tools/opt-viewer/opt-viewer.py @@ -1,382 +0,0 @@ -#!/usr/bin/env python - -from __future__ import print_function - -import argparse -import cgi -import codecs -import errno -import functools -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 = codecs.open(os.path.join(output_dir, optrecord.html_file_name(filename)), 'w', encoding='utf-8') - if existing_filename: - self.source_stream = open(existing_filename) - else: - self.source_stream = None - print(''' - -

Unable to locate file {}

- - '''.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: - if sys.version_info.major >= 3: - html_highlighted = file_text - else: - html_highlighted = file_text.decode('utf-8') - 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('
', '')
-            html_highlighted = html_highlighted.replace('
', '') - - for (linenum, html_line) in enumerate(html_highlighted.split('\n'), start=1): - print(u''' - -{linenum} - - -
{html_line}
-'''.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 = "{r.DemangledFunctionName}".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 = '+' - message = lines[0] - expand_message = u''' -'''.format(indent, '\n'.join(lines[1:])) - else: - expand_link = '' - expand_message = '' - message = r.message - print(u''' - - -{r.RelativeHotness} -{r.PassWithDiffPrefix} -
{indent}
{expand_link} {message} {expand_message} -{inlining_context} -'''.format(**locals()), file=self.stream) - - def render(self, line_remarks): - if not self.source_stream: - return - - print(''' - -{} - - - - - - -
- - - - - -'''.format(os.path.basename(self.filename)), file=self.stream) - self.render_source_lines(self.source_stream, line_remarks) - - print(''' - -
Line -Hotness -Optimization -Source -Inline Context -
- -''', file=self.stream) - - -class IndexRenderer: - def __init__(self, output_dir, should_display_hotness, max_hottest_remarks_on_index): - self.stream = codecs.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 = cgi.escape(r.DemangledFunctionName) - print(u''' - -{r.DebugLocString} -{r.RelativeHotness} -{escaped_name} -{r.PassWithDiffPrefix} -'''.format(**locals()), file=self.stream) - - def render(self, all_remarks): - print(''' - - - - - - -
- - - - - - -''', 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(''' -
Source LocationHotnessFunctionPass
- -''', 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() Index: llvm/trunk/tools/opt-viewer/opt-viewer.py.in =================================================================== --- llvm/trunk/tools/opt-viewer/opt-viewer.py.in +++ llvm/trunk/tools/opt-viewer/opt-viewer.py.in @@ -0,0 +1,382 @@ +#!/usr/bin/env @PYTHON_BASENAME@ + +from __future__ import print_function + +import argparse +import cgi +import codecs +import errno +import functools +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 = codecs.open(os.path.join(output_dir, optrecord.html_file_name(filename)), 'w', encoding='utf-8') + if existing_filename: + self.source_stream = open(existing_filename) + else: + self.source_stream = None + print(''' + +

Unable to locate file {}

+ + '''.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: + if sys.version_info.major >= 3: + html_highlighted = file_text + else: + html_highlighted = file_text.decode('utf-8') + 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('
', '')
+            html_highlighted = html_highlighted.replace('
', '') + + for (linenum, html_line) in enumerate(html_highlighted.split('\n'), start=1): + print(u''' + +{linenum} + + +
{html_line}
+'''.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 = "{r.DemangledFunctionName}".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 = '+' + message = lines[0] + expand_message = u''' +'''.format(indent, '\n'.join(lines[1:])) + else: + expand_link = '' + expand_message = '' + message = r.message + print(u''' + + +{r.RelativeHotness} +{r.PassWithDiffPrefix} +
{indent}
{expand_link} {message} {expand_message} +{inlining_context} +'''.format(**locals()), file=self.stream) + + def render(self, line_remarks): + if not self.source_stream: + return + + print(''' + +{} + + + + + + +
+ + + + + +'''.format(os.path.basename(self.filename)), file=self.stream) + self.render_source_lines(self.source_stream, line_remarks) + + print(''' + +
Line +Hotness +Optimization +Source +Inline Context +
+ +''', file=self.stream) + + +class IndexRenderer: + def __init__(self, output_dir, should_display_hotness, max_hottest_remarks_on_index): + self.stream = codecs.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 = cgi.escape(r.DemangledFunctionName) + print(u''' + +{r.DebugLocString} +{r.RelativeHotness} +{escaped_name} +{r.PassWithDiffPrefix} +'''.format(**locals()), file=self.stream) + + def render(self, all_remarks): + print(''' + + + + + + +
+ + + + + + +''', 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(''' +
Source LocationHotnessFunctionPass
+ +''', 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() Index: llvm/trunk/tools/opt-viewer/optrecord.py =================================================================== --- llvm/trunk/tools/opt-viewer/optrecord.py +++ llvm/trunk/tools/opt-viewer/optrecord.py @@ -1,345 +0,0 @@ -#!/usr/bin/env python - -from __future__ import print_function - -import yaml -# Try to use the C parser. -try: - from yaml import CLoader as Loader -except ImportError: - print("For faster parsing, you may want to install libYAML for PyYAML") - from yaml import Loader - -import cgi -from collections import defaultdict -import fnmatch -import functools -from multiprocessing import Lock -import os, os.path -import subprocess -try: - # The previously builtin function `intern()` was moved - # to the `sys` module in Python 3. - from sys import intern -except: - pass - -import re - -import optpmap - -try: - dict.iteritems -except AttributeError: - # Python 3 - def itervalues(d): - return iter(d.values()) - def iteritems(d): - return iter(d.items()) -else: - # Python 2 - def itervalues(d): - return d.itervalues() - def iteritems(d): - return d.iteritems() - - -def html_file_name(filename): - return filename.replace('/', '_').replace('#', '_') + ".html" - - -def make_link(File, Line): - return "\"{}#L{}\"".format(html_file_name(File), Line) - - -class Remark(yaml.YAMLObject): - # Work-around for http://pyyaml.org/ticket/154. - yaml_loader = Loader - - default_demangler = 'c++filt -n' - demangler_proc = None - - @classmethod - def set_demangler(cls, demangler): - cls.demangler_proc = subprocess.Popen(demangler.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) - cls.demangler_lock = Lock() - - @classmethod - def demangle(cls, name): - with cls.demangler_lock: - cls.demangler_proc.stdin.write((name + '\n').encode('utf-8')) - cls.demangler_proc.stdin.flush() - return cls.demangler_proc.stdout.readline().rstrip().decode('utf-8') - - # Intern all strings since we have lot of duplication across filenames, - # remark text. - # - # Change Args from a list of dicts to a tuple of tuples. This saves - # memory in two ways. One, a small tuple is significantly smaller than a - # small dict. Two, using tuple instead of list allows Args to be directly - # used as part of the key (in Python only immutable types are hashable). - def _reduce_memory(self): - self.Pass = intern(self.Pass) - self.Name = intern(self.Name) - try: - # Can't intern unicode strings. - self.Function = intern(self.Function) - except: - pass - - def _reduce_memory_dict(old_dict): - new_dict = dict() - for (k, v) in iteritems(old_dict): - if type(k) is str: - k = intern(k) - - if type(v) is str: - v = intern(v) - elif type(v) is dict: - # This handles [{'Caller': ..., 'DebugLoc': { 'File': ... }}] - v = _reduce_memory_dict(v) - new_dict[k] = v - return tuple(new_dict.items()) - - self.Args = tuple([_reduce_memory_dict(arg_dict) for arg_dict in self.Args]) - - # The inverse operation of the dictonary-related memory optimization in - # _reduce_memory_dict. E.g. - # (('DebugLoc', (('File', ...) ... ))) -> [{'DebugLoc': {'File': ...} ....}] - def recover_yaml_structure(self): - def tuple_to_dict(t): - d = dict() - for (k, v) in t: - if type(v) is tuple: - v = tuple_to_dict(v) - d[k] = v - return d - - self.Args = [tuple_to_dict(arg_tuple) for arg_tuple in self.Args] - - def canonicalize(self): - if not hasattr(self, 'Hotness'): - self.Hotness = 0 - if not hasattr(self, 'Args'): - self.Args = [] - self._reduce_memory() - - @property - def File(self): - return self.DebugLoc['File'] - - @property - def Line(self): - return int(self.DebugLoc['Line']) - - @property - def Column(self): - return self.DebugLoc['Column'] - - @property - def DebugLocString(self): - return "{}:{}:{}".format(self.File, self.Line, self.Column) - - @property - def DemangledFunctionName(self): - return self.demangle(self.Function) - - @property - def Link(self): - return make_link(self.File, self.Line) - - def getArgString(self, mapping): - mapping = dict(list(mapping)) - dl = mapping.get('DebugLoc') - if dl: - del mapping['DebugLoc'] - - assert(len(mapping) == 1) - (key, value) = list(mapping.items())[0] - - if key == 'Caller' or key == 'Callee' or key == 'DirectCallee': - value = cgi.escape(self.demangle(value)) - - if dl and key != 'Caller': - dl_dict = dict(list(dl)) - return u"{}".format( - make_link(dl_dict['File'], dl_dict['Line']), value) - else: - return value - - # Return a cached dictionary for the arguments. The key for each entry is - # the argument key (e.g. 'Callee' for inlining remarks. The value is a - # list containing the value (e.g. for 'Callee' the function) and - # optionally a DebugLoc. - def getArgDict(self): - if hasattr(self, 'ArgDict'): - return self.ArgDict - self.ArgDict = {} - for arg in self.Args: - if len(arg) == 2: - if arg[0][0] == 'DebugLoc': - dbgidx = 0 - else: - assert(arg[1][0] == 'DebugLoc') - dbgidx = 1 - - key = arg[1 - dbgidx][0] - entry = (arg[1 - dbgidx][1], arg[dbgidx][1]) - else: - arg = arg[0] - key = arg[0] - entry = (arg[1], ) - - self.ArgDict[key] = entry - return self.ArgDict - - def getDiffPrefix(self): - if hasattr(self, 'Added'): - if self.Added: - return '+' - else: - return '-' - return '' - - @property - def PassWithDiffPrefix(self): - return self.getDiffPrefix() + self.Pass - - @property - def message(self): - # Args is a list of mappings (dictionaries) - values = [self.getArgString(mapping) for mapping in self.Args] - return "".join(values) - - @property - def RelativeHotness(self): - if self.max_hotness: - return "{0:.2f}%".format(self.Hotness * 100. / self.max_hotness) - else: - return '' - - @property - def key(self): - return (self.__class__, self.PassWithDiffPrefix, self.Name, self.File, - self.Line, self.Column, self.Function, self.Args) - - def __hash__(self): - return hash(self.key) - - def __eq__(self, other): - return self.key == other.key - - def __repr__(self): - return str(self.key) - - -class Analysis(Remark): - yaml_tag = '!Analysis' - - @property - def color(self): - return "white" - - -class AnalysisFPCommute(Analysis): - yaml_tag = '!AnalysisFPCommute' - - -class AnalysisAliasing(Analysis): - yaml_tag = '!AnalysisAliasing' - - -class Passed(Remark): - yaml_tag = '!Passed' - - @property - def color(self): - return "green" - - -class Missed(Remark): - yaml_tag = '!Missed' - - @property - def color(self): - return "red" - -class Failure(Missed): - yaml_tag = '!Failure' - -def get_remarks(input_file, filter_=None): - max_hotness = 0 - all_remarks = dict() - file_remarks = defaultdict(functools.partial(defaultdict, list)) - - with open(input_file) as f: - docs = yaml.load_all(f, Loader=Loader) - - filter_e = None - if filter_: - filter_e = re.compile(filter_) - for remark in docs: - remark.canonicalize() - # Avoid remarks withoug debug location or if they are duplicated - if not hasattr(remark, 'DebugLoc') or remark.key in all_remarks: - continue - - if filter_e and not filter_e.search(remark.Pass): - continue - - all_remarks[remark.key] = remark - - file_remarks[remark.File][remark.Line].append(remark) - - # If we're reading a back a diff yaml file, max_hotness is already - # captured which may actually be less than the max hotness found - # in the file. - if hasattr(remark, 'max_hotness'): - max_hotness = remark.max_hotness - max_hotness = max(max_hotness, remark.Hotness) - - return max_hotness, all_remarks, file_remarks - - -def gather_results(filenames, num_jobs, should_print_progress, filter_=None): - if should_print_progress: - print('Reading YAML files...') - if not Remark.demangler_proc: - Remark.set_demangler(Remark.default_demangler) - remarks = optpmap.pmap( - get_remarks, filenames, num_jobs, should_print_progress, filter_) - max_hotness = max(entry[0] for entry in remarks) - - def merge_file_remarks(file_remarks_job, all_remarks, merged): - for filename, d in iteritems(file_remarks_job): - for line, remarks in iteritems(d): - for remark in remarks: - # Bring max_hotness into the remarks so that - # RelativeHotness does not depend on an external global. - remark.max_hotness = max_hotness - if remark.key not in all_remarks: - merged[filename][line].append(remark) - - all_remarks = dict() - file_remarks = defaultdict(functools.partial(defaultdict, list)) - for _, all_remarks_job, file_remarks_job in remarks: - merge_file_remarks(file_remarks_job, all_remarks, file_remarks) - all_remarks.update(all_remarks_job) - - return all_remarks, file_remarks, max_hotness != 0 - - -def find_opt_files(*dirs_or_files): - all = [] - for dir_or_file in dirs_or_files: - if os.path.isfile(dir_or_file): - all.append(dir_or_file) - else: - for dir, subdirs, files in os.walk(dir_or_file): - # Exclude mounted directories and symlinks (os.walk default). - subdirs[:] = [d for d in subdirs - if not os.path.ismount(os.path.join(dir, d))] - for file in files: - if fnmatch.fnmatch(file, "*.opt.yaml*"): - all.append(os.path.join(dir, file)) - return all Index: llvm/trunk/tools/opt-viewer/optrecord.py.in =================================================================== --- llvm/trunk/tools/opt-viewer/optrecord.py.in +++ llvm/trunk/tools/opt-viewer/optrecord.py.in @@ -0,0 +1,345 @@ +#!/usr/bin/env @PYTHON_BASENAME@ + +from __future__ import print_function + +import yaml +# Try to use the C parser. +try: + from yaml import CLoader as Loader +except ImportError: + print("For faster parsing, you may want to install libYAML for PyYAML") + from yaml import Loader + +import cgi +from collections import defaultdict +import fnmatch +import functools +from multiprocessing import Lock +import os, os.path +import subprocess +try: + # The previously builtin function `intern()` was moved + # to the `sys` module in Python 3. + from sys import intern +except: + pass + +import re + +import optpmap + +try: + dict.iteritems +except AttributeError: + # Python 3 + def itervalues(d): + return iter(d.values()) + def iteritems(d): + return iter(d.items()) +else: + # Python 2 + def itervalues(d): + return d.itervalues() + def iteritems(d): + return d.iteritems() + + +def html_file_name(filename): + return filename.replace('/', '_').replace('#', '_') + ".html" + + +def make_link(File, Line): + return "\"{}#L{}\"".format(html_file_name(File), Line) + + +class Remark(yaml.YAMLObject): + # Work-around for http://pyyaml.org/ticket/154. + yaml_loader = Loader + + default_demangler = 'c++filt -n' + demangler_proc = None + + @classmethod + def set_demangler(cls, demangler): + cls.demangler_proc = subprocess.Popen(demangler.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE) + cls.demangler_lock = Lock() + + @classmethod + def demangle(cls, name): + with cls.demangler_lock: + cls.demangler_proc.stdin.write((name + '\n').encode('utf-8')) + cls.demangler_proc.stdin.flush() + return cls.demangler_proc.stdout.readline().rstrip().decode('utf-8') + + # Intern all strings since we have lot of duplication across filenames, + # remark text. + # + # Change Args from a list of dicts to a tuple of tuples. This saves + # memory in two ways. One, a small tuple is significantly smaller than a + # small dict. Two, using tuple instead of list allows Args to be directly + # used as part of the key (in Python only immutable types are hashable). + def _reduce_memory(self): + self.Pass = intern(self.Pass) + self.Name = intern(self.Name) + try: + # Can't intern unicode strings. + self.Function = intern(self.Function) + except: + pass + + def _reduce_memory_dict(old_dict): + new_dict = dict() + for (k, v) in iteritems(old_dict): + if type(k) is str: + k = intern(k) + + if type(v) is str: + v = intern(v) + elif type(v) is dict: + # This handles [{'Caller': ..., 'DebugLoc': { 'File': ... }}] + v = _reduce_memory_dict(v) + new_dict[k] = v + return tuple(new_dict.items()) + + self.Args = tuple([_reduce_memory_dict(arg_dict) for arg_dict in self.Args]) + + # The inverse operation of the dictonary-related memory optimization in + # _reduce_memory_dict. E.g. + # (('DebugLoc', (('File', ...) ... ))) -> [{'DebugLoc': {'File': ...} ....}] + def recover_yaml_structure(self): + def tuple_to_dict(t): + d = dict() + for (k, v) in t: + if type(v) is tuple: + v = tuple_to_dict(v) + d[k] = v + return d + + self.Args = [tuple_to_dict(arg_tuple) for arg_tuple in self.Args] + + def canonicalize(self): + if not hasattr(self, 'Hotness'): + self.Hotness = 0 + if not hasattr(self, 'Args'): + self.Args = [] + self._reduce_memory() + + @property + def File(self): + return self.DebugLoc['File'] + + @property + def Line(self): + return int(self.DebugLoc['Line']) + + @property + def Column(self): + return self.DebugLoc['Column'] + + @property + def DebugLocString(self): + return "{}:{}:{}".format(self.File, self.Line, self.Column) + + @property + def DemangledFunctionName(self): + return self.demangle(self.Function) + + @property + def Link(self): + return make_link(self.File, self.Line) + + def getArgString(self, mapping): + mapping = dict(list(mapping)) + dl = mapping.get('DebugLoc') + if dl: + del mapping['DebugLoc'] + + assert(len(mapping) == 1) + (key, value) = list(mapping.items())[0] + + if key == 'Caller' or key == 'Callee' or key == 'DirectCallee': + value = cgi.escape(self.demangle(value)) + + if dl and key != 'Caller': + dl_dict = dict(list(dl)) + return u"{}".format( + make_link(dl_dict['File'], dl_dict['Line']), value) + else: + return value + + # Return a cached dictionary for the arguments. The key for each entry is + # the argument key (e.g. 'Callee' for inlining remarks. The value is a + # list containing the value (e.g. for 'Callee' the function) and + # optionally a DebugLoc. + def getArgDict(self): + if hasattr(self, 'ArgDict'): + return self.ArgDict + self.ArgDict = {} + for arg in self.Args: + if len(arg) == 2: + if arg[0][0] == 'DebugLoc': + dbgidx = 0 + else: + assert(arg[1][0] == 'DebugLoc') + dbgidx = 1 + + key = arg[1 - dbgidx][0] + entry = (arg[1 - dbgidx][1], arg[dbgidx][1]) + else: + arg = arg[0] + key = arg[0] + entry = (arg[1], ) + + self.ArgDict[key] = entry + return self.ArgDict + + def getDiffPrefix(self): + if hasattr(self, 'Added'): + if self.Added: + return '+' + else: + return '-' + return '' + + @property + def PassWithDiffPrefix(self): + return self.getDiffPrefix() + self.Pass + + @property + def message(self): + # Args is a list of mappings (dictionaries) + values = [self.getArgString(mapping) for mapping in self.Args] + return "".join(values) + + @property + def RelativeHotness(self): + if self.max_hotness: + return "{0:.2f}%".format(self.Hotness * 100. / self.max_hotness) + else: + return '' + + @property + def key(self): + return (self.__class__, self.PassWithDiffPrefix, self.Name, self.File, + self.Line, self.Column, self.Function, self.Args) + + def __hash__(self): + return hash(self.key) + + def __eq__(self, other): + return self.key == other.key + + def __repr__(self): + return str(self.key) + + +class Analysis(Remark): + yaml_tag = '!Analysis' + + @property + def color(self): + return "white" + + +class AnalysisFPCommute(Analysis): + yaml_tag = '!AnalysisFPCommute' + + +class AnalysisAliasing(Analysis): + yaml_tag = '!AnalysisAliasing' + + +class Passed(Remark): + yaml_tag = '!Passed' + + @property + def color(self): + return "green" + + +class Missed(Remark): + yaml_tag = '!Missed' + + @property + def color(self): + return "red" + +class Failure(Missed): + yaml_tag = '!Failure' + +def get_remarks(input_file, filter_=None): + max_hotness = 0 + all_remarks = dict() + file_remarks = defaultdict(functools.partial(defaultdict, list)) + + with open(input_file) as f: + docs = yaml.load_all(f, Loader=Loader) + + filter_e = None + if filter_: + filter_e = re.compile(filter_) + for remark in docs: + remark.canonicalize() + # Avoid remarks withoug debug location or if they are duplicated + if not hasattr(remark, 'DebugLoc') or remark.key in all_remarks: + continue + + if filter_e and not filter_e.search(remark.Pass): + continue + + all_remarks[remark.key] = remark + + file_remarks[remark.File][remark.Line].append(remark) + + # If we're reading a back a diff yaml file, max_hotness is already + # captured which may actually be less than the max hotness found + # in the file. + if hasattr(remark, 'max_hotness'): + max_hotness = remark.max_hotness + max_hotness = max(max_hotness, remark.Hotness) + + return max_hotness, all_remarks, file_remarks + + +def gather_results(filenames, num_jobs, should_print_progress, filter_=None): + if should_print_progress: + print('Reading YAML files...') + if not Remark.demangler_proc: + Remark.set_demangler(Remark.default_demangler) + remarks = optpmap.pmap( + get_remarks, filenames, num_jobs, should_print_progress, filter_) + max_hotness = max(entry[0] for entry in remarks) + + def merge_file_remarks(file_remarks_job, all_remarks, merged): + for filename, d in iteritems(file_remarks_job): + for line, remarks in iteritems(d): + for remark in remarks: + # Bring max_hotness into the remarks so that + # RelativeHotness does not depend on an external global. + remark.max_hotness = max_hotness + if remark.key not in all_remarks: + merged[filename][line].append(remark) + + all_remarks = dict() + file_remarks = defaultdict(functools.partial(defaultdict, list)) + for _, all_remarks_job, file_remarks_job in remarks: + merge_file_remarks(file_remarks_job, all_remarks, file_remarks) + all_remarks.update(all_remarks_job) + + return all_remarks, file_remarks, max_hotness != 0 + + +def find_opt_files(*dirs_or_files): + all = [] + for dir_or_file in dirs_or_files: + if os.path.isfile(dir_or_file): + all.append(dir_or_file) + else: + for dir, subdirs, files in os.walk(dir_or_file): + # Exclude mounted directories and symlinks (os.walk default). + subdirs[:] = [d for d in subdirs + if not os.path.ismount(os.path.join(dir, d))] + for file in files: + if fnmatch.fnmatch(file, "*.opt.yaml*"): + all.append(os.path.join(dir, file)) + return all