Convert to a python package
[hiprofile] / hiprofile / __init__.py
1 #!/usr/bin/env python
2
3 import subprocess
4 import re
5 import os
6 import socket
7 import pkg_resources
8 from xml.dom.minidom import parse as parseXML
9 from jinja2 import Environment, PackageLoader
10
11 b_id = 0
12 s_id = 0
13
14 try:
15     __version__ = pkg_resources.get_distribution('hiprofile').version
16 except Exception:
17     __version__ = 'unknown'
18
19 def _get_count(node):
20     """ Utility function: return the number in a 'count' element contained in
21         the current node"""
22     countnode = node.getElementsByTagName('count')
23     return int(countnode.item(0).firstChild.data)
24
25 def _threshold_list(list, fn, threshold):
26     """ Utility function: given a list and an ordering function, return the
27         elements of the list that are above the threshold.
28
29         If threshold is a percentage (ie, ends with '%'), then the items
30         that fn returns above that percentage of the total. If it's an absolute
31         number, then that number of items is returned"""
32
33     if threshold.endswith('%'):
34         percentage = float(threshold[0:-1]) / 100
35         total = reduce(int.__add__, map(fn, list))
36         list = [ x for x in list if fn(x) > (total * percentage) ]
37         list.sort(key = fn, reverse = True)
38
39     else:
40         count = int(threshold)
41         list.sort(key = fn, reverse = True)
42         list = list[0:count]
43
44     return list
45
46 class Connection(object):
47     def __init__(self, host):
48         self.host = host
49
50     def execute(self, command, input = None):
51
52         if self.host:
53             command = ['ssh', self.host] + command
54
55         if input:
56             stdin = subprocess.PIPE
57         else:
58             stdin = None
59
60         null_fd = open('/dev/null', 'w')
61         proc = subprocess.Popen(command, stdin = stdin,
62                 stdout = subprocess.PIPE, stderr = null_fd)
63
64         if input:
65             proc.stdin.write(input)
66
67         return proc.stdout
68
69 class SymbolInstruction(object):
70     def __init__(self, addr, asm, source = None):
71         self.addr = addr
72         self.asm = asm
73         self.percentage = 0
74
75         if source is None:
76             source = ''
77         self.source = source
78
79     def set_samples(self, samples, total):
80         self.samples = samples
81         self.percentage = 100 * float(samples) / total
82
83     def colour(self):
84         if not self.percentage:
85             return '#ffc000'
86
87         if self.percentage * 40 > 0xc0:
88             return '#ff0000'
89
90         return '#ff%02x00' % (0xc0 - self.percentage * 40)
91
92
93 # 13283  0.7260 :1031b6e0:       stwu    r1,-48(r1)
94 sampled_asm_re = re.compile( \
95     '^\s*(?P<samples>\d+)\s+\S+\s+:\s*(?P<addr>[0-9a-f]+):\s*(?P<asm>.*)\s*$')
96
97 #               :1031b72c:       addi    r0,r9,4
98 unsampled_asm_re = re.compile( \
99     '^\s*:\s*(?P<addr>[0-9a-f]+):\s*(?P<asm>.*)\s*$')
100
101 #               :AllocSetAlloc(MemoryContext context, Size size)
102 source_re = re.compile( \
103     '^\s*:(?P<source>.*)$')
104
105 class SymbolReference(object):
106
107     def __init__(self, id, name, module):
108         self.id = id
109         self.name = name
110         self.count = 0;
111         self.module = module
112         self.annotations = None
113
114         # annotation parsing buf
115         self.source_buf = ''
116         self.insns = []
117
118     def module_name(self):
119         return self.module.split('/')[-1]
120
121     def filename(self):
122         return 'symbol-%s.html' % self.id
123
124     def annotate(self, line):
125         match = None
126
127         sampled_match = sampled_asm_re.match(line)
128         if sampled_match:
129             match = sampled_match
130
131         unsampled_match = unsampled_asm_re.match(line)
132         if unsampled_match:
133             match = unsampled_match
134
135         if match:
136             insn = SymbolInstruction(match.group('addr'), match.group('asm'),
137                     self.source_buf)
138             if sampled_match:
139                 insn.set_samples(int(match.group('samples')), self.count)
140             self.insns.append(insn)
141             self.source_buf = ''
142             return
143
144         match = source_re.match(line)
145         if match:
146             self.source_buf += match.group('source') + '\n'
147
148     @staticmethod
149     def parse(report, node, module):
150         id = int(node.getAttribute('idref'))
151         ref = SymbolReference(id, report.symtab[id], module)
152
153         ref.count = _get_count(node)
154         return ref
155
156 class Binary(object):
157
158     def __init__(self, name, count):
159         global b_id
160         self.name = name
161         self.count = count
162         self.references = []
163         self.id = b_id = b_id + 1
164
165     def __str__(self):
166         s = '%s: %d' % (self.name, self.count)
167         for ref in self.references:
168             s += '\n' + str(ref)
169         return s
170
171     def shortname(self):
172         return self.name.split('/')[-1]
173
174     def filename(self):
175         return 'binary-%d.html' % self.id
176
177     def threshold(self, thresholds):
178         self.references = _threshold_list(self.references,
179                 lambda r: r.count, thresholds['symbol'])
180         self.reference_dict = dict([ (r.name, r) for r in self.references ])
181
182     def annotate(self, report, conn, options):
183         fn_re = re.compile('^[0-9a-f]+\s+<[^>]+>: /\* (\S+) total:')
184
185         symbols = [ s for s in self.references if s.name != '(no symbols)' ]
186
187         if not symbols:
188             return
189
190         command = [options.opannotate, '--source', '--assembly',
191             '--include-file=' + self.name,
192             '-i', ','.join([ s.name for s in symbols ])]
193
194         fd = conn.execute(command)
195
196         symbol = None
197
198         for line in fd.readlines():
199             match = fn_re.match(line)
200             if match:
201                 if symbol:
202                     symbol.annotate(line)
203                 symname = match.group(1)
204                 if symname in self.reference_dict:
205                     symbol = self.reference_dict[symname]
206                 else:
207                     symbol = None
208             if symbol:
209                 symbol.annotate(line)
210
211     def parse_symbol(self, report, node, module = None):
212         if module is None:
213             module = self.name
214
215         ref = SymbolReference.parse(report, node, module)
216         ref.percentage = 100 * float(ref.count) / self.count
217         self.references.append(ref)
218
219
220     @staticmethod
221     def parse(report, node):
222         name = node.getAttribute('name')
223
224         binary = Binary(name, _get_count(node))
225
226         for child_node in node.childNodes:
227             if child_node.nodeType != node.ELEMENT_NODE:
228                 continue
229
230             if child_node.nodeName == 'symbol':
231                 binary.parse_symbol(report, child_node, None)
232
233             elif child_node.nodeName == 'module':
234                 module_name = child_node.getAttribute('name')
235                 for child_sym_node in child_node.getElementsByTagName('symbol'):
236                     binary.parse_symbol(report, child_sym_node, module_name)
237
238         return binary
239
240 class Report(object):
241     def __init__(self, host, arch, cpu):
242         self.host = host
243         self.arch = arch
244         self.cpu = cpu
245         self.binaries = []
246         self.symtab = []
247         self.total_samples = 0
248
249     def add_binary(self, binary):
250         self.binaries.append(binary)
251         self.total_samples += binary.count
252
253     def threshold(self, thresholds):
254         self.binaries = _threshold_list(self.binaries,
255                 lambda b: b.count, thresholds['binary'])
256
257         for binary in self.binaries:
258             binary.threshold(thresholds)
259
260     def annotate(self, conn, options):
261         for binary in self.binaries:
262             binary.annotate(self, conn, options)
263
264     @staticmethod
265     def parse(doc, hostname):
266         node = doc.documentElement
267
268         cpu = '%s (%s MHz)' % (\
269                    node.getAttribute('processor'),
270                    node.getAttribute('mhz'))
271
272         report = Report(hostname, node.getAttribute('cputype'), cpu)
273
274         # parse symbol table
275         symtab_node = doc.getElementsByTagName('symboltable').item(0)
276
277         for node in symtab_node.childNodes:
278             if node.nodeType != node.ELEMENT_NODE:
279                 continue
280             report.symtab.insert(int(node.getAttribute('id')),
281                                  node.getAttribute('name'))
282
283
284         # parse each binary node
285         for node in doc.getElementsByTagName('binary'):
286              binary = Binary.parse(report, node)
287              report.add_binary(binary)
288
289         # calculate percentages
290         for binary in report.binaries:
291             binary.percentage = 100 * float(binary.count) / report.total_samples
292
293         return report
294
295     @staticmethod
296     def extract(connection, options):
297         fd = connection.execute([options.opreport, '--xml'])
298         doc = parseXML(fd)
299
300         if connection.host:
301             hostname = connection.host
302         else:
303             hostname = socket.gethostname()
304
305         return Report.parse(doc, hostname)
306
307     def __str__(self):
308         return self.machine + '\n' + '\n'.join(map(str, self.binaries))
309
310 def write_report(report, outdir):
311
312     os.mkdir(outdir)
313
314     # set up template engine
315     env = Environment(loader = PackageLoader(__name__, 'resources'),
316                       autoescape = True)
317     templates = {}
318     for name in ['report', 'binary', 'symbol']:
319         templates[name] = env.get_template('%s.html' % name)
320
321     # copy required files over from resources
322     files = ['style.css', 'hiprofile.js', 'bar.png', 'jquery-1.3.1.min.js']
323     for file in files:
324         f = open(os.path.join(outdir, file), 'w')
325         f.write(pkg_resources.resource_string(__name__, 'resources/' + file))
326         f.close()
327
328     reportfile = os.path.join(outdir, 'index.html')
329     templates['report'].stream(version = __version__,
330                                report = report).dump(reportfile)
331
332     for binary in report.binaries:
333         binaryfile = os.path.join(outdir, binary.filename())
334         templates['binary'].stream(version = __version__,
335                                    report = report,
336                                    binary = binary) \
337                                     .dump(binaryfile)
338
339         for symbol in binary.references:
340             symbolfile = os.path.join(outdir, symbol.filename())
341             templates['symbol'].stream(version = __version__,
342                                        report = report, binary = binary,
343                                        symbol = symbol).dump(symbolfile)