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