xref: /aosp_15_r20/external/mesa3d/src/gallium/tools/trace/pytracediff.py (revision 6104692788411f58d303aa86923a9ff6ecaded22)
1#!/usr/bin/env python3
2# coding=utf-8
3##########################################################################
4#
5# pytracediff - Compare Gallium XML trace files
6# (C) Copyright 2022 Matti 'ccr' Hämäläinen <[email protected]>
7#
8# Permission is hereby granted, free of charge, to any person obtaining a copy
9# of this software and associated documentation files (the "Software"), to deal
10# in the Software without restriction, including without limitation the rights
11# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12# copies of the Software, and to permit persons to whom the Software is
13# furnished to do so, subject to the following conditions:
14#
15# The above copyright notice and this permission notice shall be included in
16# all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24# THE SOFTWARE.
25#
26##########################################################################
27
28from parse import *
29import os
30import sys
31import re
32import signal
33import functools
34import argparse
35import difflib
36import subprocess
37
38assert sys.version_info >= (3, 6), 'Python >= 3.6 required'
39
40
41###
42### ANSI color codes
43###
44PKK_ANSI_ESC       = '\33['
45PKK_ANSI_NORMAL    = '0m'
46PKK_ANSI_RED       = '31m'
47PKK_ANSI_GREEN     = '32m'
48PKK_ANSI_YELLOW    = '33m'
49PKK_ANSI_PURPLE    = '35m'
50PKK_ANSI_BOLD      = '1m'
51PKK_ANSI_ITALIC    = '3m'
52
53
54###
55### Utility functions and classes
56###
57def pkk_fatal(msg):
58    print(f"ERROR: {msg}", file=sys.stderr)
59    if outpipe is not None:
60        outpipe.terminate()
61    sys.exit(1)
62
63
64def pkk_info(msg):
65    print(msg, file=sys.stderr)
66
67
68def pkk_output(outpipe, msg):
69    if outpipe is not None:
70        print(msg, file=outpipe.stdin)
71    else:
72        print(msg)
73
74
75def pkk_signal_handler(signal, frame):
76    print("\nQuitting due to SIGINT / Ctrl+C!")
77    if outpipe is not None:
78        outpipe.terminate()
79    sys.exit(1)
80
81
82def pkk_arg_range(vstr, vmin, vmax):
83    try:
84        value = int(vstr)
85    except Exception as e:
86        raise argparse.ArgumentTypeError(f"value '{vstr}' is not an integer")
87
88    value = int(vstr)
89    if value < vmin or value > vmax:
90        raise argparse.ArgumentTypeError(f"value {value} not in range {vmin}-{vmax}")
91    else:
92        return value
93
94
95class PKKArgumentParser(argparse.ArgumentParser):
96    def print_help(self):
97        print("pytracediff - Compare Gallium XML trace files\n"
98        "(C) Copyright 2022 Matti 'ccr' Hämäläinen <[email protected]>\n")
99        super().print_help()
100        print("\nList of junk calls:")
101        for klass, call in sorted(trace_ignore_calls):
102            print(f"  {klass}::{call}")
103
104    def error(self, msg):
105        self.print_help()
106        print(f"\nERROR: {msg}", file=sys.stderr)
107        sys.exit(2)
108
109
110class PKKTraceParser(TraceParser):
111    def __init__(self, stream, options, state):
112        TraceParser.__init__(self, stream, options, state)
113        self.call_stack = []
114
115    def handle_call(self, call):
116        self.call_stack.append(call)
117
118
119class PKKPrettyPrinter(PrettyPrinter):
120    def __init__(self, options):
121        self.options = options
122
123    def entry_start(self, show_args):
124        self.data = []
125        self.line = ""
126        self.show_args = show_args
127
128    def entry_get(self):
129        if self.line != "":
130            self.data.append(self.line)
131        return self.data
132
133    def text(self, text):
134        self.line += text
135
136    def literal(self, text):
137        self.line += text
138
139    def function(self, text):
140        self.line += text
141
142    def variable(self, text):
143        self.line += text
144
145    def address(self, text):
146        self.line += text
147
148    def newline(self):
149        self.data.append(self.line)
150        self.line = ""
151
152
153    def visit_literal(self, node):
154        if node.value is None:
155            self.literal("NULL")
156        elif isinstance(node.value, str):
157            self.literal('"' + node.value + '"')
158        else:
159            self.literal(repr(node.value))
160
161    def visit_blob(self, node):
162        self.address("blob()")
163
164    def visit_named_constant(self, node):
165        self.literal(node.name)
166
167    def visit_array(self, node):
168        self.text("{")
169        sep = ""
170        for value in node.elements:
171            self.text(sep)
172            if sep != "":
173                self.newline()
174            value.visit(self)
175            sep = ", "
176        self.text("}")
177
178    def visit_struct(self, node):
179        self.text("{")
180        sep = ""
181        for name, value in node.members:
182            self.text(sep)
183            if sep != "":
184                self.newline()
185            self.variable(name)
186            self.text(" = ")
187            value.visit(self)
188            sep = ", "
189        self.text("}")
190
191    def visit_pointer(self, node):
192        if self.options.named_ptrs:
193            self.address(node.named_address())
194        else:
195            self.address(node.address)
196
197    def visit_call(self, node):
198        if not self.options.suppress_variants:
199            self.text(f"[{node.no:8d}] ")
200
201        if node.klass is not None:
202            self.function(node.klass +"::"+ node.method)
203        else:
204            self.function(node.method)
205
206        if not self.options.method_only or self.show_args:
207            self.text("(")
208            if len(node.args):
209                self.newline()
210                sep = ""
211                for name, value in node.args:
212                    self.text(sep)
213                    if sep != "":
214                        self.newline()
215                    self.variable(name)
216                    self.text(" = ")
217                    value.visit(self)
218                    sep = ", "
219                self.newline()
220
221            self.text(")")
222
223            if node.ret is not None:
224                self.text(" = ")
225                node.ret.visit(self)
226
227        if not self.options.suppress_variants and node.time is not None:
228            self.text(" // time ")
229            node.time.visit(self)
230
231
232def pkk_parse_trace(filename, options, state):
233    pkk_info(f"Parsing {filename} ...")
234    try:
235        if filename.endswith(".gz"):
236            from gzip import GzipFile
237            stream = io.TextIOWrapper(GzipFile(filename, "rb"))
238        elif filename.endswith(".bz2"):
239            from bz2 import BZ2File
240            stream = io.TextIOWrapper(BZ2File(filename, "rb"))
241        else:
242            stream = open(filename, "rt")
243
244    except OSError as e:
245        pkk_fatal(str(e))
246
247    parser = PKKTraceParser(stream, options, state)
248    parser.parse()
249
250    return parser.call_stack
251
252
253def pkk_get_line(data, nline):
254    if nline < len(data):
255        return data[nline]
256    else:
257        return None
258
259
260def pkk_format_line(line, indent, width):
261    if line is not None:
262        tmp = indent + line
263        if len(tmp) > width:
264            return tmp[0:(width - 3)] + "..."
265        else:
266            return tmp
267    else:
268        return ""
269
270
271###
272### Main program starts
273###
274if __name__ == "__main__":
275    ### Check if output is a terminal
276    outpipe = None
277    redirect = False
278
279    try:
280        defwidth = os.get_terminal_size().columns
281        redirect = True
282    except OSError:
283        defwidth = 80
284
285    signal.signal(signal.SIGINT, pkk_signal_handler)
286
287    ### Parse arguments
288    optparser = PKKArgumentParser(
289        usage="%(prog)s [options] <tracefile #1> <tracefile #2>\n")
290
291    optparser.add_argument("filename1",
292        type=str, action="store",
293        metavar="<tracefile #1>",
294        help="Gallium trace XML filename (plain or .gz, .bz2)")
295
296    optparser.add_argument("filename2",
297        type=str, action="store",
298        metavar="<tracefile #2>",
299        help="Gallium trace XML filename (plain or .gz, .bz2)")
300
301    optparser.add_argument("-p", "--plain",
302        dest="plain",
303        action="store_true",
304        help="disable ANSI color etc. formatting")
305
306    optparser.add_argument("-S", "--sup-variants",
307        dest="suppress_variants",
308        action="store_true",
309        help="suppress some variants in output")
310
311    optparser.add_argument("-C", "--sup-common",
312        dest="suppress_common",
313        action="store_true",
314        help="suppress common sections completely")
315
316    optparser.add_argument("-N", "--named",
317        dest="named_ptrs",
318        action="store_true",
319        help="generate symbolic names for raw pointer values")
320
321    optparser.add_argument("-M", "--method-only",
322        dest="method_only",
323        action="store_true",
324        help="output only call names without arguments")
325
326    optparser.add_argument("-I", "--ignore-junk",
327        dest="ignore_junk",
328        action="store_true",
329        help="filter out/ignore junk calls (see below)")
330
331    optparser.add_argument("-w", "--width",
332        dest="output_width",
333        type=functools.partial(pkk_arg_range, vmin=16, vmax=512), default=defwidth,
334        metavar="N",
335        help="output width (default: %(default)s)")
336
337    options = optparser.parse_args()
338
339    ### Parse input files
340    stack1 = pkk_parse_trace(options.filename1, options, TraceStateData())
341    stack2 = pkk_parse_trace(options.filename2, options, TraceStateData())
342
343    ### Perform diffing
344    pkk_info("Matching trace sequences ...")
345    sequence = difflib.SequenceMatcher(lambda x : x.is_junk, stack1, stack2, autojunk=False)
346
347    pkk_info("Sequencing diff ...")
348    opcodes = sequence.get_opcodes()
349    if len(opcodes) == 1 and opcodes[0][0] == "equal":
350        print("The files are identical.")
351        sys.exit(0)
352
353    ### Redirect output to 'less' if stdout is a tty
354    try:
355        if redirect:
356            outpipe = subprocess.Popen(["less", "-S", "-R"], stdin=subprocess.PIPE, encoding="utf8")
357
358        ### Output results
359        pkk_info("Outputting diff ...")
360        colwidth = int((options.output_width - 3) / 2)
361        colfmt   = "{}{:"+ str(colwidth) +"s}{} {}{}{} {}{:"+ str(colwidth) +"s}{}"
362
363        printer = PKKPrettyPrinter(options)
364
365        prevtag = ""
366        for tag, start1, end1, start2, end2 in opcodes:
367            if tag == "equal":
368                show_args = False
369                if options.suppress_common:
370                    if tag != prevtag:
371                        pkk_output(outpipe, "[...]")
372                    continue
373
374                sep = "|"
375                ansi1 = ansi2 = ansiend = ""
376                show_args = False
377            elif tag == "insert":
378                sep = "+"
379                ansi1 = ""
380                ansi2 = PKK_ANSI_ESC + PKK_ANSI_GREEN
381                show_args = True
382            elif tag == "delete":
383                sep = "-"
384                ansi1 = PKK_ANSI_ESC + PKK_ANSI_RED
385                ansi2 = ""
386                show_args = True
387            elif tag == "replace":
388                sep = ">"
389                ansi1 = ansi2 = PKK_ANSI_ESC + PKK_ANSI_BOLD
390                show_args = True
391            else:
392                pkk_fatal(f"Internal error, unsupported difflib.SequenceMatcher operation '{tag}'.")
393
394            # No ANSI, please
395            if options.plain:
396                ansi1 = ansisep = ansi2 = ansiend = ""
397            else:
398                ansisep = PKK_ANSI_ESC + PKK_ANSI_PURPLE
399                ansiend = PKK_ANSI_ESC + PKK_ANSI_NORMAL
400
401
402            # Print out the block
403            ncall1 = start1
404            ncall2 = start2
405            last1 = last2 = False
406            while True:
407                # Get line data
408                if ncall1 < end1:
409                    if not options.ignore_junk or not stack1[ncall1].is_junk:
410                        printer.entry_start(show_args)
411                        stack1[ncall1].visit(printer)
412                        data1 = printer.entry_get()
413                    else:
414                        data1 = []
415                    ncall1 += 1
416                else:
417                    data1 = []
418                    last1 = True
419
420                if ncall2 < end2:
421                    if not options.ignore_junk or not stack2[ncall2].is_junk:
422                        printer.entry_start(show_args)
423                        stack2[ncall2].visit(printer)
424                        data2 = printer.entry_get()
425                    else:
426                        data2 = []
427                    ncall2 += 1
428                else:
429                    data2 = []
430                    last2 = True
431
432                # Check if we are at last call of both
433                if last1 and last2:
434                    break
435
436                nline = 0
437                while nline < len(data1) or nline < len(data2):
438                    # Determine line start indentation
439                    if nline > 0:
440                        if options.suppress_variants:
441                            indent = " "*8
442                        else:
443                            indent = " "*12
444                    else:
445                        indent = ""
446
447                    line1 = pkk_get_line(data1, nline)
448                    line2 = pkk_get_line(data2, nline)
449
450                    # Highlight differing lines if not plain
451                    if not options.plain and line1 != line2:
452                        if tag == "insert" or tag == "delete":
453                            ansi1 = ansi1 + PKK_ANSI_ESC + PKK_ANSI_BOLD
454                        elif tag == "replace":
455                            ansi1 = ansi2 = ansi1 + PKK_ANSI_ESC + PKK_ANSI_YELLOW
456
457                    # Output line
458                    pkk_output(outpipe, colfmt.format(
459                        ansi1, pkk_format_line(line1, indent, colwidth), ansiend,
460                        ansisep, sep, ansiend,
461                        ansi2, pkk_format_line(line2, indent, colwidth), ansiend).
462                        rstrip())
463
464                    nline += 1
465
466            if tag == "equal" and options.suppress_common:
467                pkk_output(outpipe, "[...]")
468
469            prevtag = tag
470
471    except Exception as e:
472        pkk_fatal(str(e))
473
474    if outpipe is not None:
475        outpipe.communicate()
476