]> git.cworth.org Git - apitrace/blob - xml2txt.py
Basic Linux/GLX tracing support.
[apitrace] / xml2txt.py
1 #!/usr/bin/env python
2 ##########################################################################
3 #
4 # Copyright 2008-2009 VMware, Inc.
5 # All Rights Reserved.
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining a copy
8 # of this software and associated documentation files (the "Software"), to deal
9 # in the Software without restriction, including without limitation the rights
10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 # copies of the Software, and to permit persons to whom the Software is
12 # furnished to do so, subject to the following conditions:
13 #
14 # The above copyright notice and this permission notice shall be included in
15 # all copies or substantial portions of the Software.
16 #
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 # THE SOFTWARE.
24 #
25 ##########################################################################/
26
27
28 import sys
29 import optparse
30 import xml.parsers.expat
31 import gzip
32
33
34 ELEMENT_START, ELEMENT_END, CHARACTER_DATA, EOF = range(4)
35
36
37 class XmlToken:
38
39     def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
40         assert type in (ELEMENT_START, ELEMENT_END, CHARACTER_DATA, EOF)
41         self.type = type
42         self.name_or_data = name_or_data
43         self.attrs = attrs
44         self.line = line
45         self.column = column
46
47     def __str__(self):
48         if self.type == ELEMENT_START:
49             return '<' + self.name_or_data + ' ...>'
50         if self.type == ELEMENT_END:
51             return '</' + self.name_or_data + '>'
52         if self.type == CHARACTER_DATA:
53             return self.name_or_data
54         if self.type == EOF:
55             return 'end of file'
56         assert 0
57
58
59 class XmlTokenizer:
60     """Expat based XML tokenizer."""
61
62     def __init__(self, fp, skip_ws = True):
63         self.fp = fp
64         self.tokens = []
65         self.index = 0
66         self.final = False
67         self.skip_ws = skip_ws
68         
69         self.character_pos = 0, 0
70         self.character_data = ''
71         
72         self.parser = xml.parsers.expat.ParserCreate()
73         self.parser.StartElementHandler  = self.handle_element_start
74         self.parser.EndElementHandler    = self.handle_element_end
75         self.parser.CharacterDataHandler = self.handle_character_data
76     
77     def handle_element_start(self, name, attributes):
78         self.finish_character_data()
79         line, column = self.pos()
80         token = XmlToken(ELEMENT_START, name, attributes, line, column)
81         self.tokens.append(token)
82     
83     def handle_element_end(self, name):
84         self.finish_character_data()
85         line, column = self.pos()
86         token = XmlToken(ELEMENT_END, name, None, line, column)
87         self.tokens.append(token)
88
89     def handle_character_data(self, data):
90         if not self.character_data:
91             self.character_pos = self.pos()
92         self.character_data += data
93     
94     def finish_character_data(self):
95         if self.character_data:
96             if not self.skip_ws or not self.character_data.isspace(): 
97                 line, column = self.character_pos
98                 token = XmlToken(CHARACTER_DATA, self.character_data, None, line, column)
99                 self.tokens.append(token)
100             self.character_data = ''
101     
102     def next(self):
103         size = 16*1024
104         while self.index >= len(self.tokens) and not self.final:
105             self.tokens = []
106             self.index = 0
107             data = self.fp.read(size)
108             self.final = len(data) < size
109             data = data.rstrip('\0')
110             try:
111                 self.parser.Parse(data, self.final)
112             except xml.parsers.expat.ExpatError, e:
113                 #if e.code == xml.parsers.expat.errors.XML_ERROR_NO_ELEMENTS:
114                 if e.code == 3:
115                     pass
116                 else:
117                     raise e
118         if self.index >= len(self.tokens):
119             line, column = self.pos()
120             token = XmlToken(EOF, None, None, line, column)
121         else:
122             token = self.tokens[self.index]
123             self.index += 1
124         return token
125
126     def pos(self):
127         return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
128
129
130 class TokenMismatch(Exception):
131
132     def __init__(self, expected, found):
133         self.expected = expected
134         self.found = found
135
136     def __str__(self):
137         return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
138
139
140
141 class XmlParser:
142     """Base XML document parser."""
143
144     def __init__(self, fp):
145         self.tokenizer = XmlTokenizer(fp)
146         self.consume()
147     
148     def consume(self):
149         self.token = self.tokenizer.next()
150
151     def match_element_start(self, name):
152         return self.token.type == ELEMENT_START and self.token.name_or_data == name
153     
154     def match_element_end(self, name):
155         return self.token.type == ELEMENT_END and self.token.name_or_data == name
156
157     def element_start(self, name):
158         while self.token.type == CHARACTER_DATA:
159             self.consume()
160         if self.token.type != ELEMENT_START:
161             raise TokenMismatch(XmlToken(ELEMENT_START, name), self.token)
162         if self.token.name_or_data != name:
163             raise TokenMismatch(XmlToken(ELEMENT_START, name), self.token)
164         attrs = self.token.attrs
165         self.consume()
166         return attrs
167     
168     def element_end(self, name):
169         while self.token.type == CHARACTER_DATA:
170             self.consume()
171         if self.token.type != ELEMENT_END:
172             raise TokenMismatch(XmlToken(ELEMENT_END, name), self.token)
173         if self.token.name_or_data != name:
174             raise TokenMismatch(XmlToken(ELEMENT_END, name), self.token)
175         self.consume()
176
177     def character_data(self, strip = True):
178         data = ''
179         while self.token.type == CHARACTER_DATA:
180             data += self.token.name_or_data
181             self.consume()
182         if strip:
183             data = data.strip()
184         return data
185
186
187 class GzipFile(gzip.GzipFile):
188
189     def _read_eof(self):
190         # Ignore incomplete files
191         try:
192             gzip.GzipFile._read_eof(self)
193         except IOError:
194             pass
195
196
197 class Formatter:
198     
199     def function(self, name):
200         return name
201         
202     def variable(self, name):
203         return name
204
205     def literal(self, value):
206         return str(value)
207     
208     def address(self, addr):
209         return addr
210
211
212 class AnsiFormatter(Formatter):
213     '''Formatter for plain-text files which outputs ANSI escape codes. See
214     http://en.wikipedia.org/wiki/ANSI_escape_code for more information
215     concerning ANSI escape codes.
216     '''
217
218     _csi = '\33['
219
220     _normal = '0m'
221     _bold = '1m'
222     _italic = '3m'
223     _red = '31m'
224     _green = '32m'
225     _blue = '34m'
226
227     def _escape(self, code, text):
228         return self._csi + code + text + self._csi + self._normal
229
230     def function(self, name):
231         text = Formatter.function(self, name)
232         return self._escape(self._bold, text)
233         
234     def variable(self, name):
235         text = Formatter.variable(self, name)
236         return self._escape(self._italic, text)
237
238     def literal(self, value):
239         text = Formatter.literal(self, value)
240         return self._escape(self._blue, text)
241     
242     def address(self, value):
243         text = Formatter.address(self, value)
244         return self._escape(self._green, text)
245
246
247 def DefaultFormatter():
248     if sys.platform in ('linux2', 'cygwin'):
249         return AnsiFormatter()
250     else:
251         return Formatter()
252
253
254 class TraceParser(XmlParser):
255
256     def __init__(self, stream, formatter):
257         XmlParser.__init__(self, stream)
258         self.formatter = formatter
259
260     def parse(self):
261         self.element_start('trace')
262         while self.token.type not in (ELEMENT_END, EOF):
263             self.parse_call()
264         if self.token.type != EOF:
265             self.element_end('trace')
266
267     def parse_call(self):
268         attrs = self.element_start('call')
269         name = attrs['name']
270         args = []
271         ret = None
272         duration = None
273         while self.token.type == ELEMENT_START:
274             if self.token.name_or_data == 'arg':
275                 arg = self.parse_arg()
276                 args.append(arg)
277             elif self.token.name_or_data == 'ret':
278                 ret = self.parse_ret()
279             elif self.token.name_or_data == 'duration':
280                 duration = self.parse_duration()
281             elif self.token.name_or_data == 'call':
282                 # ignore nested function calls
283                 self.parse_call()
284             else:
285                 raise TokenMismatch("<arg ...> or <ret ...>", self.token)
286         self.element_end('call')
287         
288         self.handle_call(name, args, ret, duration)
289
290     def parse_arg(self):
291         attrs = self.element_start('arg')
292         name = attrs['name']
293         value = self.parse_value()
294         self.element_end('arg')
295
296         return name, value
297
298     def parse_ret(self):
299         attrs = self.element_start('ret')
300         value = self.parse_value()
301         self.element_end('ret')
302
303         return value
304
305     def parse_duration(self):
306         attrs = self.element_start('duration')
307         value = int(self.character_data())
308         self.element_end('duration')
309         return value
310
311     def parse_value(self):
312         if self.token.type == CHARACTER_DATA:
313             return self.formatter.literal(self.character_data())
314         if self.token.type == ELEMENT_START:
315             if self.token.name_or_data == 'elem':
316                 return self.parse_elems()
317             if self.token.name_or_data == 'ref':
318                 return self.parse_ref()
319         raise TokenMismatch("<elem ...>, <ref ...>, or text", self.token)
320
321     def parse_elems(self):
322         elems = [self.parse_elem()]
323         while self.token.type != ELEMENT_END:
324             elems.append(self.parse_elem())
325         return '{' + ', '.join(elems) + '}'
326
327     def parse_elem(self):
328         attrs = self.element_start('elem')
329         value = self.parse_value()
330         self.element_end('elem')
331
332         try:
333             name = attrs['name']
334         except KeyError:
335             pass
336         else:
337             value = name + ' = ' + value
338
339         return value
340
341     def parse_ref(self):
342         attrs = self.element_start('ref')
343         if self.token.type != ELEMENT_END:
344             value = '&' + self.parse_value()
345         else:
346             value = self.formatter.address(attrs['addr'])
347         self.element_end('ref')
348
349         return value
350
351     def handle_call(self, name, args, ret, duration):
352         s = ''
353
354         #if duration is not None:
355         #    s += '%8u ' % (duration)
356
357         s += self.formatter.function(name)
358         s += '(' + ', '.join([self.formatter.variable(name) + ' = ' + value for name, value in args]) + ')'
359         if ret is not None:
360             s += ' = ' + ret
361         s += '\n'
362         
363         try:
364             sys.stdout.write(s)
365         except IOError: 
366             # catch broken pipe
367             sys.exit(0)
368
369
370 class StatsTraceParser(TraceParser):
371
372     def __init__(self, stream, formatter):
373         TraceParser.__init__(self, stream, formatter)
374         self.stats = {}
375
376     def parse(self):
377         TraceParser.parse(self)
378
379         sys.stdout.write('%s\t%s\t%s\n' % ("name", "calls", "duration"))
380         for name, (calls, duration) in self.stats.iteritems():
381             sys.stdout.write('%s\t%u\t%f\n' % (name, calls, duration/1000000.0))
382
383     def handle_call(self, name, args, ret, duration):
384         try:
385             nr_calls, total_duration = self.stats[name]
386         except KeyError:
387             nr_calls = 1
388             total_duration = duration
389         else:
390             nr_calls += 1
391             if duration is not None:
392                 total_duration += duration
393         self.stats[name] = nr_calls, total_duration
394
395
396 def main():
397     parser = optparse.OptionParser(
398         usage="\n\t%prog [options] [file] ...")
399     parser.add_option(
400         '-s', '--stats',
401         action="store_true",
402         dest="stats", default=False,
403         help="generate statistics instead")
404     parser.add_option(
405         '--color', '--colour',
406         type="choice", choices=('never', 'always', 'auto'), metavar='WHEN',
407         dest="color", default="always",
408         help="coloring: never, always, or auto [default: %default]")
409     (options, args) = parser.parse_args(sys.argv[1:])
410
411     if options.color == 'always' or options.color == 'auto' and sys.stdout.isatty():
412         formatter = DefaultFormatter()
413     else:
414         formatter = Formatter()
415     
416     if options.stats:
417         factory = StatsTraceParser
418     else:
419         factory = TraceParser
420
421     if args:
422         for arg in args:
423             if arg.endswith('.gz'):
424                 stream = GzipFile(arg, 'rb')
425             elif arg.endswith('.bz2'):
426                 from bz2 import BZ2File
427                 stream = BZ2File(arg, 'rt')
428             else:
429                 stream = open(arg, 'rt')
430             parser = factory(stream, formatter)
431             parser.parse()
432     else:
433             parser = factory(sys.stdin, formatter)
434             parser.parse()
435
436
437 if __name__ == '__main__':
438     main()