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