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