blob: d48d2e3f0e7b2d85532b39b6f55f21e733d1926a [file] [log] [blame]
José Fonsecaa65795f2012-02-18 18:15:18 +00001#!/usr/bin/env python
2##########################################################################
3#
4# Copyright 2011 Jose Fonseca
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
José Fonseca8f9116e2012-11-16 22:19:10 +000028import difflib
29import itertools
José Fonsecaa65795f2012-02-18 18:15:18 +000030import optparse
José Fonseca3cf1e9a2012-03-16 15:46:26 +000031import os.path
José Fonseca8f9116e2012-11-16 22:19:10 +000032import platform
José Fonsecaa65795f2012-02-18 18:15:18 +000033import shutil
34import subprocess
35import sys
36import tempfile
37
38
José Fonseca8f9116e2012-11-16 22:19:10 +000039##########################################################################/
40#
41# Abstract interface
42#
José Fonseca3cf1e9a2012-03-16 15:46:26 +000043
José Fonseca8f9116e2012-11-16 22:19:10 +000044
45class Differ:
46
47 def __init__(self, apitrace):
48 self.apitrace = apitrace
49 self.isatty = sys.stdout.isatty()
50
51 def setRefTrace(self, ref_trace, ref_calls):
52 raise NotImplementedError
53
54 def setSrcTrace(self, src_trace, src_calls):
55 raise NotImplementedError
56
57 def diff(self):
58 raise NotImplementedError
59
60
61##########################################################################/
62#
63# External diff tool
64#
65
66
67class AsciiDumper:
68
José Fonseca095fb952014-02-04 14:27:05 +000069 def __init__(self, apitrace, trace, calls, callNos):
José Fonseca3cf1e9a2012-03-16 15:46:26 +000070 self.output = tempfile.NamedTemporaryFile()
71
72 dump_args = [
José Fonseca8f9116e2012-11-16 22:19:10 +000073 apitrace,
José Fonsecaa65795f2012-02-18 18:15:18 +000074 'dump',
75 '--color=never',
José Fonseca095fb952014-02-04 14:27:05 +000076 '--call-nos=' + ('yes' if callNos else 'no'),
José Fonsecaa65795f2012-02-18 18:15:18 +000077 '--arg-names=no',
José Fonseca3cf1e9a2012-03-16 15:46:26 +000078 '--calls=' + calls,
José Fonsecaa65795f2012-02-18 18:15:18 +000079 trace
José Fonseca3cf1e9a2012-03-16 15:46:26 +000080 ]
José Fonsecaa65795f2012-02-18 18:15:18 +000081
José Fonseca3cf1e9a2012-03-16 15:46:26 +000082 self.dump = subprocess.Popen(
83 args = dump_args,
84 stdout = self.output,
85 universal_newlines = True,
86 )
José Fonsecadf1a1812012-03-14 11:06:44 +000087
José Fonsecaa65795f2012-02-18 18:15:18 +000088
José Fonseca8f9116e2012-11-16 22:19:10 +000089class ExternalDiffer(Differ):
José Fonsecac6998962012-03-16 09:56:25 +000090
José Fonseca8f9116e2012-11-16 22:19:10 +000091 if platform.system() == 'Windows':
92 start_delete = ''
93 end_delete = ''
94 start_insert = ''
95 end_insert = ''
96 else:
97 start_delete = '\33[9m\33[31m'
98 end_delete = '\33[0m'
99 start_insert = '\33[32m'
100 end_insert = '\33[0m'
José Fonsecac6998962012-03-16 09:56:25 +0000101
José Fonseca095fb952014-02-04 14:27:05 +0000102 def __init__(self, apitrace, tool, width=None, callNos = False):
José Fonseca8f9116e2012-11-16 22:19:10 +0000103 Differ.__init__(self, apitrace)
104 self.diff_args = [tool]
105 if tool == 'diff':
106 self.diff_args += [
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000107 '--speed-large-files',
Carl Worthf45f1e22012-08-13 13:24:03 -0700108 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000109 if self.isatty:
110 self.diff_args += [
111 '--old-line-format=' + self.start_delete + '%l' + self.end_delete + '\n',
112 '--new-line-format=' + self.start_insert + '%l' + self.end_insert + '\n',
113 ]
114 elif tool == 'sdiff':
115 if width is None:
116 import curses
117 curses.setupterm()
118 width = curses.tigetnum('cols')
119 self.diff_args += [
120 '--width=%u' % width,
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000121 '--speed-large-files',
122 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000123 elif tool == 'wdiff':
124 self.diff_args += [
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000125 #'--terminal',
126 '--avoid-wraps',
Carl Worthf45f1e22012-08-13 13:24:03 -0700127 ]
José Fonseca8f9116e2012-11-16 22:19:10 +0000128 if self.isatty:
129 self.diff_args += [
130 '--start-delete=' + self.start_delete,
131 '--end-delete=' + self.end_delete,
132 '--start-insert=' + self.start_insert,
133 '--end-insert=' + self.end_insert,
134 ]
135 else:
136 assert False
José Fonseca095fb952014-02-04 14:27:05 +0000137 self.callNos = callNos
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000138
José Fonseca8f9116e2012-11-16 22:19:10 +0000139 def setRefTrace(self, ref_trace, ref_calls):
José Fonseca095fb952014-02-04 14:27:05 +0000140 self.ref_dumper = AsciiDumper(self.apitrace, ref_trace, ref_calls, self.callNos)
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000141
José Fonseca8f9116e2012-11-16 22:19:10 +0000142 def setSrcTrace(self, src_trace, src_calls):
José Fonseca095fb952014-02-04 14:27:05 +0000143 self.src_dumper = AsciiDumper(self.apitrace, src_trace, src_calls, self.callNos)
José Fonseca8f9116e2012-11-16 22:19:10 +0000144
145 def diff(self):
146 diff_args = self.diff_args + [
147 self.ref_dumper.output.name,
148 self.src_dumper.output.name,
149 ]
150
151 self.ref_dumper.dump.wait()
152 self.src_dumper.dump.wait()
153
154 less = None
155 if self.isatty:
156 less = subprocess.Popen(
157 args = ['less', '-FRXn'],
158 stdin = subprocess.PIPE
159 )
160
161 diff_stdout = less.stdin
162 else:
163 diff_stdout = None
164
165 diff = subprocess.Popen(
166 args = diff_args,
167 stdout = diff_stdout,
168 universal_newlines = True,
José Fonsecaa65795f2012-02-18 18:15:18 +0000169 )
170
José Fonseca8f9116e2012-11-16 22:19:10 +0000171 diff.wait()
José Fonsecaa65795f2012-02-18 18:15:18 +0000172
José Fonseca8f9116e2012-11-16 22:19:10 +0000173 if less is not None:
174 less.stdin.close()
175 less.wait()
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000176
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000177
José Fonseca8f9116e2012-11-16 22:19:10 +0000178##########################################################################/
179#
180# Python diff
181#
182
183from unpickle import Unpickler, Dumper, Rebuilder
José Fonseca975f1242012-12-06 07:11:29 +0000184from highlight import PlainHighlighter, LessHighlighter
José Fonseca8f9116e2012-11-16 22:19:10 +0000185
186
187ignoredFunctionNames = set([
188 'glGetString',
189 'glXGetClientString',
190 'glXGetCurrentDisplay',
191 'glXGetCurrentContext',
192 'glXGetProcAddress',
193 'glXGetProcAddressARB',
194 'wglGetProcAddress',
195])
196
197
198class Blob:
199 '''Data-less proxy for bytearrays, to save memory.'''
200
201 def __init__(self, size, hash):
202 self.size = size
203 self.hash = hash
204
205 def __repr__(self):
206 return 'blob(%u)' % self.size
207
208 def __eq__(self, other):
209 return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash
210
211 def __hash__(self):
212 return self.hash
213
214
215class BlobReplacer(Rebuilder):
216 '''Replace blobs with proxys.'''
217
218 def visitByteArray(self, obj):
219 return Blob(len(obj), hash(str(obj)))
220
221 def visitCall(self, call):
222 call.args = map(self.visit, call.args)
223 call.ret = self.visit(call.ret)
224
225
226class Loader(Unpickler):
227
228 def __init__(self, stream):
229 Unpickler.__init__(self, stream)
230 self.calls = []
231 self.rebuilder = BlobReplacer()
232
233 def handleCall(self, call):
234 if call.functionName not in ignoredFunctionNames:
235 self.rebuilder.visitCall(call)
236 self.calls.append(call)
237
238
239class PythonDiffer(Differ):
240
241 def __init__(self, apitrace, callNos = False):
242 Differ.__init__(self, apitrace)
243 self.a = None
244 self.b = None
245 if self.isatty:
246 self.highlighter = LessHighlighter()
247 else:
José Fonseca975f1242012-12-06 07:11:29 +0000248 self.highlighter = PlainHighlighter()
José Fonseca8f9116e2012-11-16 22:19:10 +0000249 self.delete_color = self.highlighter.red
250 self.insert_color = self.highlighter.green
251 self.callNos = callNos
252 self.aSpace = 0
253 self.bSpace = 0
254 self.dumper = Dumper()
255
256 def setRefTrace(self, ref_trace, ref_calls):
257 self.a = self.readTrace(ref_trace, ref_calls)
258
259 def setSrcTrace(self, src_trace, src_calls):
260 self.b = self.readTrace(src_trace, src_calls)
261
262 def readTrace(self, trace, calls):
263 p = subprocess.Popen(
264 args = [
265 self.apitrace,
266 'pickle',
267 '--symbolic',
268 '--calls=' + calls,
269 trace
270 ],
271 stdout = subprocess.PIPE,
272 )
273
274 parser = Loader(p.stdout)
275 parser.parse()
276 return parser.calls
277
278 def diff(self):
279 try:
280 self._diff()
281 except IOError:
282 pass
283
284 def _diff(self):
285 matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b)
286 for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
287 if tag == 'replace':
288 self.replace(alo, ahi, blo, bhi)
289 elif tag == 'delete':
290 self.delete(alo, ahi, blo, bhi)
291 elif tag == 'insert':
292 self.insert(alo, ahi, blo, bhi)
293 elif tag == 'equal':
294 self.equal(alo, ahi, blo, bhi)
295 else:
296 raise ValueError, 'unknown tag %s' % (tag,)
297
298 def isjunk(self, call):
299 return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0)
300
301 def replace(self, alo, ahi, blo, bhi):
302 assert alo < ahi and blo < bhi
303
304 a_names = [call.functionName for call in self.a[alo:ahi]]
305 b_names = [call.functionName for call in self.b[blo:bhi]]
306
307 matcher = difflib.SequenceMatcher(None, a_names, b_names)
308 for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes():
309 _alo += alo
310 _ahi += alo
311 _blo += blo
312 _bhi += blo
313 if tag == 'replace':
314 self.replace_dissimilar(_alo, _ahi, _blo, _bhi)
315 elif tag == 'delete':
316 self.delete(_alo, _ahi, _blo, _bhi)
317 elif tag == 'insert':
318 self.insert(_alo, _ahi, _blo, _bhi)
319 elif tag == 'equal':
320 self.replace_similar(_alo, _ahi, _blo, _bhi)
321 else:
322 raise ValueError, 'unknown tag %s' % (tag,)
323
324 def replace_similar(self, alo, ahi, blo, bhi):
325 assert alo < ahi and blo < bhi
326 assert ahi - alo == bhi - blo
327 for i in xrange(0, bhi - blo):
328 self.highlighter.write('| ')
329 a_call = self.a[alo + i]
330 b_call = self.b[blo + i]
331 assert a_call.functionName == b_call.functionName
332 self.dumpCallNos(a_call.no, b_call.no)
333 self.highlighter.bold(True)
334 self.highlighter.write(b_call.functionName)
335 self.highlighter.bold(False)
336 self.highlighter.write('(')
337 sep = ''
338 numArgs = max(len(a_call.args), len(b_call.args))
339 for j in xrange(numArgs):
340 self.highlighter.write(sep)
341 try:
342 a_arg = a_call.args[j]
343 except IndexError:
344 pass
345 try:
346 b_arg = b_call.args[j]
347 except IndexError:
348 pass
349 self.replace_value(a_arg, b_arg)
350 sep = ', '
351 self.highlighter.write(')')
352 if a_call.ret is not None or b_call.ret is not None:
353 self.highlighter.write(' = ')
354 self.replace_value(a_call.ret, b_call.ret)
355 self.highlighter.write('\n')
356
357 def replace_dissimilar(self, alo, ahi, blo, bhi):
358 assert alo < ahi and blo < bhi
359 if bhi - blo < ahi - alo:
360 self.insert(alo, alo, blo, bhi)
361 self.delete(alo, ahi, bhi, bhi)
362 else:
363 self.delete(alo, ahi, blo, blo)
364 self.insert(ahi, ahi, blo, bhi)
365
366 def replace_value(self, a, b):
367 if b == a:
368 self.highlighter.write(self.dumper.visit(b))
369 else:
370 self.highlighter.strike()
371 self.highlighter.color(self.delete_color)
372 self.highlighter.write(self.dumper.visit(a))
373 self.highlighter.normal()
374 self.highlighter.write(" ")
375 self.highlighter.color(self.insert_color)
376 self.highlighter.write(self.dumper.visit(b))
377 self.highlighter.normal()
378
379 escape = "\33["
380
381 def delete(self, alo, ahi, blo, bhi):
382 assert alo < ahi
383 assert blo == bhi
384 for i in xrange(alo, ahi):
385 call = self.a[i]
386 self.highlighter.write('- ')
387 self.dumpCallNos(call.no, None)
388 self.highlighter.strike()
389 self.highlighter.color(self.delete_color)
390 self.dumpCall(call)
391
392 def insert(self, alo, ahi, blo, bhi):
393 assert alo == ahi
394 assert blo < bhi
395 for i in xrange(blo, bhi):
396 call = self.b[i]
397 self.highlighter.write('+ ')
398 self.dumpCallNos(None, call.no)
399 self.highlighter.color(self.insert_color)
400 self.dumpCall(call)
401
402 def equal(self, alo, ahi, blo, bhi):
403 assert alo < ahi and blo < bhi
404 assert ahi - alo == bhi - blo
405 for i in xrange(0, bhi - blo):
406 self.highlighter.write(' ')
407 a_call = self.a[alo + i]
408 b_call = self.b[blo + i]
409 assert a_call.functionName == b_call.functionName
410 assert len(a_call.args) == len(b_call.args)
411 self.dumpCallNos(a_call.no, b_call.no)
412 self.dumpCall(b_call)
413
414 def dumpCallNos(self, aNo, bNo):
415 if not self.callNos:
416 return
417
418 if aNo is None:
419 self.highlighter.write(' '*self.aSpace)
420 else:
421 aNoStr = str(aNo)
422 self.highlighter.strike()
423 self.highlighter.color(self.delete_color)
424 self.highlighter.write(aNoStr)
425 self.highlighter.normal()
426 self.aSpace = len(aNoStr)
427 self.highlighter.write(' ')
428 if bNo is None:
429 self.highlighter.write(' '*self.bSpace)
430 else:
431 bNoStr = str(bNo)
432 self.highlighter.color(self.insert_color)
433 self.highlighter.write(bNoStr)
434 self.highlighter.normal()
435 self.bSpace = len(bNoStr)
436 self.highlighter.write(' ')
437
438 def dumpCall(self, call):
439 self.highlighter.bold(True)
440 self.highlighter.write(call.functionName)
441 self.highlighter.bold(False)
442 self.highlighter.write('(' + ', '.join(itertools.imap(self.dumper.visit, call.args)) + ')')
443 if call.ret is not None:
444 self.highlighter.write(' = ' + self.dumper.visit(call.ret))
445 self.highlighter.normal()
446 self.highlighter.write('\n')
447
448
449
450##########################################################################/
451#
452# Main program
453#
José Fonsecaa65795f2012-02-18 18:15:18 +0000454
455
José Fonsecac6998962012-03-16 09:56:25 +0000456def which(executable):
457 '''Search for the executable on the PATH.'''
458
459 if platform.system() == 'Windows':
460 exts = ['.exe']
461 else:
462 exts = ['']
463 dirs = os.environ['PATH'].split(os.path.pathsep)
464 for dir in dirs:
465 path = os.path.join(dir, executable)
466 for ext in exts:
467 if os.path.exists(path + ext):
468 return True
469 return False
470
471
José Fonsecaa65795f2012-02-18 18:15:18 +0000472def main():
473 '''Main program.
474 '''
475
José Fonsecaa65795f2012-02-18 18:15:18 +0000476 # Parse command line options
477 optparser = optparse.OptionParser(
José Fonseca8f9116e2012-11-16 22:19:10 +0000478 usage='\n\t%prog [options] TRACE TRACE',
José Fonsecaa65795f2012-02-18 18:15:18 +0000479 version='%%prog')
480 optparser.add_option(
481 '-a', '--apitrace', metavar='PROGRAM',
482 type='string', dest='apitrace', default='apitrace',
483 help='apitrace command [default: %default]')
484 optparser.add_option(
José Fonseca155380f2013-10-18 17:15:17 -0700485 '-t', '--tool', metavar='TOOL',
José Fonseca8f9116e2012-11-16 22:19:10 +0000486 type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
José Fonseca155380f2013-10-18 17:15:17 -0700487 dest="tool", default=None,
488 help="diff tool: diff, sdiff, wdiff, or python [default: auto]")
José Fonsecac6998962012-03-16 09:56:25 +0000489 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000490 '-c', '--calls', metavar='CALLSET',
José Fonseca8f9116e2012-11-16 22:19:10 +0000491 type="string", dest="calls", default='0-10000',
José Fonsecaa65795f2012-02-18 18:15:18 +0000492 help="calls to compare [default: %default]")
493 optparser.add_option(
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000494 '--ref-calls', metavar='CALLSET',
495 type="string", dest="ref_calls", default=None,
496 help="calls to compare from reference trace")
497 optparser.add_option(
498 '--src-calls', metavar='CALLSET',
499 type="string", dest="src_calls", default=None,
500 help="calls to compare from source trace")
501 optparser.add_option(
José Fonseca8f9116e2012-11-16 22:19:10 +0000502 '--call-nos',
503 action="store_true",
504 dest="call_nos", default=False,
505 help="dump call numbers")
506 optparser.add_option(
José Fonsecaa65795f2012-02-18 18:15:18 +0000507 '-w', '--width', metavar='NUM',
José Fonseca8f9116e2012-11-16 22:19:10 +0000508 type="int", dest="width",
509 help="columns [default: auto]")
José Fonsecaa65795f2012-02-18 18:15:18 +0000510
José Fonsecaa65795f2012-02-18 18:15:18 +0000511 (options, args) = optparser.parse_args(sys.argv[1:])
512 if len(args) != 2:
513 optparser.error("incorrect number of arguments")
514
José Fonseca155380f2013-10-18 17:15:17 -0700515 if options.tool is None:
José Fonseca8f9116e2012-11-16 22:19:10 +0000516 if platform.system() == 'Windows':
José Fonseca155380f2013-10-18 17:15:17 -0700517 options.tool = 'python'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000518 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000519 if which('wdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700520 options.tool = 'wdiff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000521 else:
José Fonseca8f9116e2012-11-16 22:19:10 +0000522 sys.stderr.write('warning: wdiff not found\n')
523 if which('sdiff'):
José Fonseca155380f2013-10-18 17:15:17 -0700524 options.tool = 'sdiff'
José Fonseca8f9116e2012-11-16 22:19:10 +0000525 else:
526 sys.stderr.write('warning: sdiff not found\n')
José Fonseca155380f2013-10-18 17:15:17 -0700527 options.tool = 'diff'
José Fonseca3cf1e9a2012-03-16 15:46:26 +0000528
529 if options.ref_calls is None:
530 options.ref_calls = options.calls
531 if options.src_calls is None:
532 options.src_calls = options.calls
533
José Fonseca8f9116e2012-11-16 22:19:10 +0000534 ref_trace, src_trace = args
535
José Fonseca155380f2013-10-18 17:15:17 -0700536 if options.tool == 'python':
José Fonseca8f9116e2012-11-16 22:19:10 +0000537 differ = PythonDiffer(options.apitrace, options.call_nos)
538 else:
José Fonseca095fb952014-02-04 14:27:05 +0000539 differ = ExternalDiffer(options.apitrace, options.tool, options.width, options.call_nos)
José Fonseca8f9116e2012-11-16 22:19:10 +0000540 differ.setRefTrace(ref_trace, options.ref_calls)
541 differ.setSrcTrace(src_trace, options.src_calls)
542 differ.diff()
José Fonsecaa65795f2012-02-18 18:15:18 +0000543
544
545if __name__ == '__main__':
546 main()