1#!/usr/bin/env python3
2##########################################################################
3#
4# Copyright 2008 VMware, Inc.
5# All Rights Reserved.
6#
7# Permission is hereby granted, free of charge, to any person obtaining a
8# copy of this software and associated documentation files (the
9# "Software"), to deal in the Software without restriction, including
10# without limitation the rights to use, copy, modify, merge, publish,
11# distribute, sub license, and/or sell copies of the Software, and to
12# permit persons to whom the Software is furnished to do so, subject to
13# the following conditions:
14#
15# The above copyright notice and this permission notice (including the
16# next paragraph) shall be included in all copies or substantial portions
17# of the Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.
22# IN NO EVENT SHALL VMWARE AND/OR ITS SUPPLIERS BE LIABLE FOR
23# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26#
27##########################################################################
28
29
30import io
31import sys
32import xml.parsers.expat as xpat
33import argparse
34
35import format
36from model import *
37
38
39ELEMENT_START, ELEMENT_END, CHARACTER_DATA, EOF = range(4)
40
41
42class XmlToken:
43
44    def __init__(self, type, name_or_data, attrs = None, line = None, column = None):
45        assert type in (ELEMENT_START, ELEMENT_END, CHARACTER_DATA, EOF)
46        self.type = type
47        self.name_or_data = name_or_data
48        self.attrs = attrs
49        self.line = line
50        self.column = column
51
52    def __str__(self):
53        if self.type == ELEMENT_START:
54            return '<' + self.name_or_data + ' ...>'
55        if self.type == ELEMENT_END:
56            return '</' + self.name_or_data + '>'
57        if self.type == CHARACTER_DATA:
58            return self.name_or_data
59        if self.type == EOF:
60            return 'end of file'
61        assert 0
62
63
64class XmlTokenizer:
65    """Expat based XML tokenizer."""
66
67    def __init__(self, fp, skip_ws = True):
68        self.fp = fp
69        self.tokens = []
70        self.index = 0
71        self.final = False
72        self.skip_ws = skip_ws
73
74        self.character_pos = 0, 0
75        self.character_data = ''
76
77        self.parser = xpat.ParserCreate()
78        self.parser.StartElementHandler  = self.handle_element_start
79        self.parser.EndElementHandler    = self.handle_element_end
80        self.parser.CharacterDataHandler = self.handle_character_data
81
82    def handle_element_start(self, name, attributes):
83        self.finish_character_data()
84        line, column = self.pos()
85        token = XmlToken(ELEMENT_START, name, attributes, line, column)
86        self.tokens.append(token)
87
88    def handle_element_end(self, name):
89        self.finish_character_data()
90        line, column = self.pos()
91        token = XmlToken(ELEMENT_END, name, None, line, column)
92        self.tokens.append(token)
93
94    def handle_character_data(self, data):
95        if not self.character_data:
96            self.character_pos = self.pos()
97        self.character_data += data
98
99    def finish_character_data(self):
100        if self.character_data:
101            if not self.skip_ws or not self.character_data.isspace():
102                line, column = self.character_pos
103                token = XmlToken(CHARACTER_DATA, self.character_data, None, line, column)
104                self.tokens.append(token)
105            self.character_data = ''
106
107    def next(self):
108        size = 16*1024
109        while self.index >= len(self.tokens) and not self.final:
110            self.tokens = []
111            self.index = 0
112            data = self.fp.read(size)
113            self.final = len(data) < size
114            data = data.rstrip('\0')
115            try:
116                self.parser.Parse(data, self.final)
117            except xpat.ExpatError as e:
118                #if e.code == xpat.errors.XML_ERROR_NO_ELEMENTS:
119                if e.code == 3:
120                    pass
121                else:
122                    raise e
123        if self.index >= len(self.tokens):
124            line, column = self.pos()
125            token = XmlToken(EOF, None, None, line, column)
126        else:
127            token = self.tokens[self.index]
128            self.index += 1
129        return token
130
131    def pos(self):
132        return self.parser.CurrentLineNumber, self.parser.CurrentColumnNumber
133
134
135class TokenMismatch(Exception):
136
137    def __init__(self, expected, found):
138        self.expected = expected
139        self.found = found
140
141    def __str__(self):
142        return '%u:%u: %s expected, %s found' % (self.found.line, self.found.column, str(self.expected), str(self.found))
143
144
145
146class XmlParser:
147    """Base XML document parser."""
148
149    def __init__(self, fp):
150        self.tokenizer = XmlTokenizer(fp)
151        self.consume()
152
153    def consume(self):
154        self.token = self.tokenizer.next()
155
156    def match_element_start(self, name):
157        return self.token.type == ELEMENT_START and self.token.name_or_data == name
158
159    def match_element_end(self, name):
160        return self.token.type == ELEMENT_END and self.token.name_or_data == name
161
162    def element_start(self, name):
163        while self.token.type == CHARACTER_DATA:
164            self.consume()
165        if self.token.type != ELEMENT_START:
166            raise TokenMismatch(XmlToken(ELEMENT_START, name), self.token)
167        if self.token.name_or_data != name:
168            raise TokenMismatch(XmlToken(ELEMENT_START, name), self.token)
169        attrs = self.token.attrs
170        self.consume()
171        return attrs
172
173    def element_end(self, name):
174        while self.token.type == CHARACTER_DATA:
175            self.consume()
176        if self.token.type != ELEMENT_END:
177            raise TokenMismatch(XmlToken(ELEMENT_END, name), self.token)
178        if self.token.name_or_data != name:
179            raise TokenMismatch(XmlToken(ELEMENT_END, name), self.token)
180        self.consume()
181
182    def character_data(self, strip = True):
183        data = ''
184        while self.token.type == CHARACTER_DATA:
185            data += self.token.name_or_data
186            self.consume()
187        if strip:
188            data = data.strip()
189        return data
190
191
192class TraceParser(XmlParser):
193
194    def __init__(self, fp):
195        XmlParser.__init__(self, fp)
196        self.last_call_no = 0
197
198    def parse(self):
199        self.element_start('trace')
200        while self.token.type not in (ELEMENT_END, EOF):
201            call = self.parse_call()
202            self.handle_call(call)
203        if self.token.type != EOF:
204            self.element_end('trace')
205
206    def parse_call(self):
207        attrs = self.element_start('call')
208        try:
209            no = int(attrs['no'])
210        except KeyError as e:
211            self.last_call_no += 1
212            no = self.last_call_no
213        else:
214            self.last_call_no = no
215        klass = attrs['class']
216        method = attrs['method']
217        args = []
218        ret = None
219        time = None
220        while self.token.type == ELEMENT_START:
221            if self.token.name_or_data == 'arg':
222                arg = self.parse_arg()
223                args.append(arg)
224            elif self.token.name_or_data == 'ret':
225                ret = self.parse_ret()
226            elif self.token.name_or_data == 'call':
227                # ignore nested function calls
228                self.parse_call()
229            elif self.token.name_or_data == 'time':
230                time = self.parse_time()
231            else:
232                raise TokenMismatch("<arg ...> or <ret ...>", self.token)
233        self.element_end('call')
234
235        return Call(no, klass, method, args, ret, time)
236
237    def parse_arg(self):
238        attrs = self.element_start('arg')
239        name = attrs['name']
240        value = self.parse_value(name)
241        self.element_end('arg')
242
243        return name, value
244
245    def parse_ret(self):
246        attrs = self.element_start('ret')
247        value = self.parse_value('ret')
248        self.element_end('ret')
249
250        return value
251
252    def parse_time(self):
253        attrs = self.element_start('time')
254        time = self.parse_value('time');
255        self.element_end('time')
256        return time
257
258    def parse_value(self, name):
259        expected_tokens = ('null', 'bool', 'int', 'uint', 'float', 'string', 'enum', 'array', 'struct', 'ptr', 'bytes')
260        if self.token.type == ELEMENT_START:
261            if self.token.name_or_data in expected_tokens:
262                method = getattr(self, 'parse_' +  self.token.name_or_data)
263                return method(name)
264        raise TokenMismatch(" or " .join(expected_tokens), self.token)
265
266    def parse_null(self, pname):
267        self.element_start('null')
268        self.element_end('null')
269        return Literal(None)
270
271    def parse_bool(self, pname):
272        self.element_start('bool')
273        value = int(self.character_data())
274        self.element_end('bool')
275        return Literal(value)
276
277    def parse_int(self, pname):
278        self.element_start('int')
279        value = int(self.character_data())
280        self.element_end('int')
281        return Literal(value)
282
283    def parse_uint(self, pname):
284        self.element_start('uint')
285        value = int(self.character_data())
286        self.element_end('uint')
287        return Literal(value)
288
289    def parse_float(self, pname):
290        self.element_start('float')
291        value = float(self.character_data())
292        self.element_end('float')
293        return Literal(value)
294
295    def parse_enum(self, pname):
296        self.element_start('enum')
297        name = self.character_data()
298        self.element_end('enum')
299        return NamedConstant(name)
300
301    def parse_string(self, pname):
302        self.element_start('string')
303        value = self.character_data()
304        self.element_end('string')
305        return Literal(value)
306
307    def parse_bytes(self, pname):
308        self.element_start('bytes')
309        value = self.character_data()
310        self.element_end('bytes')
311        return Blob(value)
312
313    def parse_array(self, pname):
314        self.element_start('array')
315        elems = []
316        while self.token.type != ELEMENT_END:
317            elems.append(self.parse_elem('array'))
318        self.element_end('array')
319        return Array(elems)
320
321    def parse_elem(self, pname):
322        self.element_start('elem')
323        value = self.parse_value('elem')
324        self.element_end('elem')
325        return value
326
327    def parse_struct(self, pname):
328        attrs = self.element_start('struct')
329        name = attrs['name']
330        members = []
331        while self.token.type != ELEMENT_END:
332            members.append(self.parse_member(name))
333        self.element_end('struct')
334        return Struct(name, members)
335
336    def parse_member(self, pname):
337        attrs = self.element_start('member')
338        name = attrs['name']
339        value = self.parse_value(name)
340        self.element_end('member')
341
342        return name, value
343
344    def parse_ptr(self, pname):
345        self.element_start('ptr')
346        address = self.character_data()
347        self.element_end('ptr')
348
349        return Pointer(address, pname)
350
351    def handle_call(self, call):
352        pass
353
354
355class SimpleTraceDumper(TraceParser):
356
357    def __init__(self, fp, options, formatter):
358        TraceParser.__init__(self, fp)
359        self.options = options
360        self.formatter = formatter
361        self.pretty_printer = PrettyPrinter(self.formatter, options)
362
363    def handle_call(self, call):
364        call.visit(self.pretty_printer)
365        self.formatter.newline()
366
367
368class TraceDumper(SimpleTraceDumper):
369
370    def __init__(self, fp, options, formatter):
371        SimpleTraceDumper.__init__(self, fp, options, formatter)
372        self.call_stack = []
373
374    def handle_call(self, call):
375        if self.options.named_ptrs:
376            self.call_stack.append(call)
377        else:
378            call.visit(self.pretty_printer)
379            self.formatter.newline()
380
381    def dump_calls(self):
382        for call in self.call_stack:
383            call.visit(self.pretty_printer)
384            self.formatter.newline()
385
386
387class Main:
388    '''Common main class for all retrace command line utilities.'''
389
390    def __init__(self):
391        pass
392
393    def main(self):
394        optparser = self.get_optparser()
395        args = optparser.parse_args()
396
397        for fname in args.filename:
398            try:
399                if fname.endswith('.gz'):
400                    from gzip import GzipFile
401                    stream = io.TextIOWrapper(GzipFile(fname, 'rb'))
402                elif fname.endswith('.bz2'):
403                    from bz2 import BZ2File
404                    stream = io.TextIOWrapper(BZ2File(fname, 'rb'))
405                else:
406                    stream = open(fname, 'rt')
407            except Exception as e:
408                print("ERROR: {}".format(str(e)))
409                sys.exit(1)
410
411            self.process_arg(stream, args)
412
413    def get_optparser(self):
414        optparser = argparse.ArgumentParser(
415            description="Parse and dump Gallium trace(s)")
416        optparser.add_argument("filename", action="extend", nargs="+",
417            type=str, metavar="filename", help="Gallium trace filename (plain or .gz, .bz2)")
418        optparser.add_argument("-p", "--plain",
419            action="store_const", const=True, default=False,
420            dest="plain", help="disable ANSI color etc. formatting")
421        optparser.add_argument("-S", "--suppress",
422            action="store_const", const=True, default=False,
423            dest="suppress_variants", help="suppress some variants in output for better diffability")
424        optparser.add_argument("-N", "--named",
425            action="store_const", const=True, default=False,
426            dest="named_ptrs", help="generate symbolic names for raw pointer values")
427        optparser.add_argument("-M", "--method-only",
428            action="store_const", const=True, default=False,
429            dest="method_only", help="output only call names without arguments")
430
431        return optparser
432
433    def process_arg(self, stream, options):
434        if options.plain:
435            formatter = format.Formatter(sys.stdout)
436        else:
437            formatter = format.DefaultFormatter(sys.stdout)
438
439        parser = TraceDumper(stream, options, formatter)
440        parser.parse()
441
442        if options.named_ptrs:
443            parser.dump_calls()
444
445
446if __name__ == '__main__':
447    Main().main()
448