• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python2.7
2
3from __future__ import print_function
4
5import yaml
6# Try to use the C parser.
7try:
8    from yaml import CLoader as Loader
9except ImportError:
10    print("For faster parsing, you may want to install libYAML for PyYAML")
11    from yaml import Loader
12
13import cgi
14from collections import defaultdict
15import fnmatch
16import functools
17from multiprocessing import Lock
18import os, os.path
19import subprocess
20try:
21    # The previously builtin function `intern()` was moved
22    # to the `sys` module in Python 3.
23    from sys import intern
24except:
25    pass
26
27import optpmap
28
29try:
30    dict.iteritems
31except AttributeError:
32    # Python 3
33    def itervalues(d):
34        return iter(d.values())
35    def iteritems(d):
36        return iter(d.items())
37else:
38    # Python 2
39    def itervalues(d):
40        return d.itervalues()
41    def iteritems(d):
42        return d.iteritems()
43
44
45def html_file_name(filename):
46    return filename.replace('/', '_').replace('#', '_') + ".html"
47
48
49def make_link(File, Line):
50    return "\"{}#L{}\"".format(html_file_name(File), Line)
51
52
53class Remark(yaml.YAMLObject):
54    # Work-around for http://pyyaml.org/ticket/154.
55    yaml_loader = Loader
56
57    default_demangler = 'c++filt -n'
58    demangler_proc = None
59
60    @classmethod
61    def set_demangler(cls, demangler):
62        cls.demangler_proc = subprocess.Popen(demangler.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
63        cls.demangler_lock = Lock()
64
65    @classmethod
66    def demangle(cls, name):
67        with cls.demangler_lock:
68            cls.demangler_proc.stdin.write((name + '\n').encode('utf-8'))
69            cls.demangler_proc.stdin.flush()
70            return cls.demangler_proc.stdout.readline().rstrip().decode('utf-8')
71
72    # Intern all strings since we have lot of duplication across filenames,
73    # remark text.
74    #
75    # Change Args from a list of dicts to a tuple of tuples.  This saves
76    # memory in two ways.  One, a small tuple is significantly smaller than a
77    # small dict.  Two, using tuple instead of list allows Args to be directly
78    # used as part of the key (in Python only immutable types are hashable).
79    def _reduce_memory(self):
80        self.Pass = intern(self.Pass)
81        self.Name = intern(self.Name)
82        try:
83            # Can't intern unicode strings.
84            self.Function = intern(self.Function)
85        except:
86            pass
87
88        def _reduce_memory_dict(old_dict):
89            new_dict = dict()
90            for (k, v) in iteritems(old_dict):
91                if type(k) is str:
92                    k = intern(k)
93
94                if type(v) is str:
95                    v = intern(v)
96                elif type(v) is dict:
97                    # This handles [{'Caller': ..., 'DebugLoc': { 'File': ... }}]
98                    v = _reduce_memory_dict(v)
99                new_dict[k] = v
100            return tuple(new_dict.items())
101
102        self.Args = tuple([_reduce_memory_dict(arg_dict) for arg_dict in self.Args])
103
104    # The inverse operation of the dictonary-related memory optimization in
105    # _reduce_memory_dict.  E.g.
106    #     (('DebugLoc', (('File', ...) ... ))) -> [{'DebugLoc': {'File': ...} ....}]
107    def recover_yaml_structure(self):
108        def tuple_to_dict(t):
109            d = dict()
110            for (k, v) in t:
111                if type(v) is tuple:
112                    v = tuple_to_dict(v)
113                d[k] = v
114            return d
115
116        self.Args = [tuple_to_dict(arg_tuple) for arg_tuple in self.Args]
117
118    def canonicalize(self):
119        if not hasattr(self, 'Hotness'):
120            self.Hotness = 0
121        if not hasattr(self, 'Args'):
122            self.Args = []
123        self._reduce_memory()
124
125    @property
126    def File(self):
127        return self.DebugLoc['File']
128
129    @property
130    def Line(self):
131        return int(self.DebugLoc['Line'])
132
133    @property
134    def Column(self):
135        return self.DebugLoc['Column']
136
137    @property
138    def DebugLocString(self):
139        return "{}:{}:{}".format(self.File, self.Line, self.Column)
140
141    @property
142    def DemangledFunctionName(self):
143        return self.demangle(self.Function)
144
145    @property
146    def Link(self):
147        return make_link(self.File, self.Line)
148
149    def getArgString(self, mapping):
150        mapping = dict(list(mapping))
151        dl = mapping.get('DebugLoc')
152        if dl:
153            del mapping['DebugLoc']
154
155        assert(len(mapping) == 1)
156        (key, value) = list(mapping.items())[0]
157
158        if key == 'Caller' or key == 'Callee' or key == 'DirectCallee':
159            value = cgi.escape(self.demangle(value))
160
161        if dl and key != 'Caller':
162            dl_dict = dict(list(dl))
163            return u"<a href={}>{}</a>".format(
164                make_link(dl_dict['File'], dl_dict['Line']), value)
165        else:
166            return value
167
168    # Return a cached dictionary for the arguments.  The key for each entry is
169    # the argument key (e.g. 'Callee' for inlining remarks.  The value is a
170    # list containing the value (e.g. for 'Callee' the function) and
171    # optionally a DebugLoc.
172    def getArgDict(self):
173        if hasattr(self, 'ArgDict'):
174            return self.ArgDict
175        self.ArgDict = {}
176        for arg in self.Args:
177            if len(arg) == 2:
178                if arg[0][0] == 'DebugLoc':
179                    dbgidx = 0
180                else:
181                    assert(arg[1][0] == 'DebugLoc')
182                    dbgidx = 1
183
184                key = arg[1 - dbgidx][0]
185                entry = (arg[1 - dbgidx][1], arg[dbgidx][1])
186            else:
187                arg = arg[0]
188                key = arg[0]
189                entry = (arg[1], )
190
191            self.ArgDict[key] = entry
192        return self.ArgDict
193
194    def getDiffPrefix(self):
195        if hasattr(self, 'Added'):
196            if self.Added:
197                return '+'
198            else:
199                return '-'
200        return ''
201
202    @property
203    def PassWithDiffPrefix(self):
204        return self.getDiffPrefix() + self.Pass
205
206    @property
207    def message(self):
208        # Args is a list of mappings (dictionaries)
209        values = [self.getArgString(mapping) for mapping in self.Args]
210        return "".join(values)
211
212    @property
213    def RelativeHotness(self):
214        if self.max_hotness:
215            return "{0:.2f}%".format(self.Hotness * 100. / self.max_hotness)
216        else:
217            return ''
218
219    @property
220    def key(self):
221        return (self.__class__, self.PassWithDiffPrefix, self.Name, self.File,
222                self.Line, self.Column, self.Function, self.Args)
223
224    def __hash__(self):
225        return hash(self.key)
226
227    def __eq__(self, other):
228        return self.key == other.key
229
230    def __repr__(self):
231        return str(self.key)
232
233
234class Analysis(Remark):
235    yaml_tag = '!Analysis'
236
237    @property
238    def color(self):
239        return "white"
240
241
242class AnalysisFPCommute(Analysis):
243    yaml_tag = '!AnalysisFPCommute'
244
245
246class AnalysisAliasing(Analysis):
247    yaml_tag = '!AnalysisAliasing'
248
249
250class Passed(Remark):
251    yaml_tag = '!Passed'
252
253    @property
254    def color(self):
255        return "green"
256
257
258class Missed(Remark):
259    yaml_tag = '!Missed'
260
261    @property
262    def color(self):
263        return "red"
264
265
266def get_remarks(input_file):
267    max_hotness = 0
268    all_remarks = dict()
269    file_remarks = defaultdict(functools.partial(defaultdict, list))
270
271    with open(input_file) as f:
272        docs = yaml.load_all(f, Loader=Loader)
273        for remark in docs:
274            remark.canonicalize()
275            # Avoid remarks withoug debug location or if they are duplicated
276            if not hasattr(remark, 'DebugLoc') or remark.key in all_remarks:
277                continue
278            all_remarks[remark.key] = remark
279
280            file_remarks[remark.File][remark.Line].append(remark)
281
282            # If we're reading a back a diff yaml file, max_hotness is already
283            # captured which may actually be less than the max hotness found
284            # in the file.
285            if hasattr(remark, 'max_hotness'):
286                max_hotness = remark.max_hotness
287            max_hotness = max(max_hotness, remark.Hotness)
288
289    return max_hotness, all_remarks, file_remarks
290
291
292def gather_results(filenames, num_jobs, should_print_progress):
293    if should_print_progress:
294        print('Reading YAML files...')
295    if not Remark.demangler_proc:
296        Remark.set_demangler(Remark.default_demangler)
297    remarks = optpmap.pmap(
298        get_remarks, filenames, num_jobs, should_print_progress)
299    max_hotness = max(entry[0] for entry in remarks)
300
301    def merge_file_remarks(file_remarks_job, all_remarks, merged):
302        for filename, d in iteritems(file_remarks_job):
303            for line, remarks in iteritems(d):
304                for remark in remarks:
305                    # Bring max_hotness into the remarks so that
306                    # RelativeHotness does not depend on an external global.
307                    remark.max_hotness = max_hotness
308                    if remark.key not in all_remarks:
309                        merged[filename][line].append(remark)
310
311    all_remarks = dict()
312    file_remarks = defaultdict(functools.partial(defaultdict, list))
313    for _, all_remarks_job, file_remarks_job in remarks:
314        merge_file_remarks(file_remarks_job, all_remarks, file_remarks)
315        all_remarks.update(all_remarks_job)
316
317    return all_remarks, file_remarks, max_hotness != 0
318
319
320def find_opt_files(*dirs_or_files):
321    all = []
322    for dir_or_file in dirs_or_files:
323        if os.path.isfile(dir_or_file):
324            all.append(dir_or_file)
325        else:
326            for dir, subdirs, files in os.walk(dir_or_file):
327                # Exclude mounted directories and symlinks (os.walk default).
328                subdirs[:] = [d for d in subdirs
329                              if not os.path.ismount(os.path.join(dir, d))]
330                for file in files:
331                    if fnmatch.fnmatch(file, "*.opt.yaml*"):
332                        all.append(os.path.join(dir, file))
333    return all
334