]> git.cworth.org Git - apitrace/blob - scripts/tracediff.py
0b720057e7bf6eb43260411e1f410c22bcdb61ef
[apitrace] / scripts / tracediff.py
1 #!/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
28 import difflib
29 import itertools
30 import optparse
31 import os.path
32 import platform
33 import shutil
34 import subprocess
35 import sys
36 import tempfile
37
38
39 ##########################################################################/
40 #
41 # Abstract interface
42 #
43
44
45 class 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
67 class AsciiDumper:
68
69     def __init__(self, apitrace, trace, calls):
70         self.output = tempfile.NamedTemporaryFile()
71
72         dump_args = [
73             apitrace,
74             'dump',
75             '--color=never',
76             '--call-nos=no',
77             '--arg-names=no',
78             '--calls=' + calls,
79             trace
80         ]
81
82         self.dump = subprocess.Popen(
83             args = dump_args,
84             stdout = self.output,
85             universal_newlines = True,
86         )
87
88
89 class ExternalDiffer(Differ):
90
91     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'
101
102     def __init__(self, apitrace, tool, width=None):
103         Differ.__init__(self, apitrace)
104         self.diff_args = [tool]
105         if tool == 'diff':
106             self.diff_args += [
107                 '--speed-large-files',
108             ]
109             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,
121                 '--speed-large-files',
122             ]
123         elif tool == 'wdiff':
124             self.diff_args += [
125                 #'--terminal',
126                 '--avoid-wraps',
127             ]
128             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
137
138     def setRefTrace(self, ref_trace, ref_calls):
139         self.ref_dumper = AsciiDumper(self.apitrace, ref_trace, ref_calls)
140
141     def setSrcTrace(self, src_trace, src_calls):
142         self.src_dumper = AsciiDumper(self.apitrace, src_trace, src_calls)
143
144     def diff(self):
145         diff_args = self.diff_args + [
146             self.ref_dumper.output.name,
147             self.src_dumper.output.name,
148         ]
149
150         self.ref_dumper.dump.wait()
151         self.src_dumper.dump.wait()
152
153         less = None
154         if self.isatty:
155             less = subprocess.Popen(
156                 args = ['less', '-FRXn'],
157                 stdin = subprocess.PIPE
158             )
159
160             diff_stdout = less.stdin
161         else:
162             diff_stdout = None
163
164         diff = subprocess.Popen(
165             args = diff_args,
166             stdout = diff_stdout,
167             universal_newlines = True,
168         )
169
170         diff.wait()
171
172         if less is not None:
173             less.stdin.close()
174             less.wait()
175
176
177 ##########################################################################/
178 #
179 # Python diff
180 #
181
182 from unpickle import Unpickler, Dumper, Rebuilder
183 from highlight import PlainHighlighter, LessHighlighter
184
185
186 ignoredFunctionNames = set([
187     'glGetString',
188     'glXGetClientString',
189     'glXGetCurrentDisplay',
190     'glXGetCurrentContext',
191     'glXGetProcAddress',
192     'glXGetProcAddressARB',
193     'wglGetProcAddress',
194 ])
195
196
197 class Blob:
198     '''Data-less proxy for bytearrays, to save memory.'''
199
200     def __init__(self, size, hash):
201         self.size = size
202         self.hash = hash
203
204     def __repr__(self):
205         return 'blob(%u)' % self.size
206
207     def __eq__(self, other):
208         return isinstance(other, Blob) and self.size == other.size and self.hash == other.hash
209
210     def __hash__(self):
211         return self.hash
212
213
214 class BlobReplacer(Rebuilder):
215     '''Replace blobs with proxys.'''
216
217     def visitByteArray(self, obj):
218         return Blob(len(obj), hash(str(obj)))
219
220     def visitCall(self, call):
221         call.args = map(self.visit, call.args)
222         call.ret = self.visit(call.ret)
223
224
225 class Loader(Unpickler):
226
227     def __init__(self, stream):
228         Unpickler.__init__(self, stream)
229         self.calls = []
230         self.rebuilder = BlobReplacer()
231
232     def handleCall(self, call):
233         if call.functionName not in ignoredFunctionNames:
234             self.rebuilder.visitCall(call)
235             self.calls.append(call)
236
237
238 class PythonDiffer(Differ):
239
240     def __init__(self, apitrace, callNos = False):
241         Differ.__init__(self, apitrace)
242         self.a = None
243         self.b = None
244         if self.isatty:
245             self.highlighter = LessHighlighter()
246         else:
247             self.highlighter = PlainHighlighter()
248         self.delete_color = self.highlighter.red
249         self.insert_color = self.highlighter.green
250         self.callNos = callNos
251         self.aSpace = 0
252         self.bSpace = 0
253         self.dumper = Dumper()
254
255     def setRefTrace(self, ref_trace, ref_calls):
256         self.a = self.readTrace(ref_trace, ref_calls)
257
258     def setSrcTrace(self, src_trace, src_calls):
259         self.b = self.readTrace(src_trace, src_calls)
260
261     def readTrace(self, trace, calls):
262         p = subprocess.Popen(
263             args = [
264                 self.apitrace,
265                 'pickle',
266                 '--symbolic',
267                 '--calls=' + calls,
268                 trace
269             ],
270             stdout = subprocess.PIPE,
271         )
272
273         parser = Loader(p.stdout)
274         parser.parse()
275         return parser.calls
276
277     def diff(self):
278         try:
279             self._diff()
280         except IOError:
281             pass
282
283     def _diff(self):
284         matcher = difflib.SequenceMatcher(self.isjunk, self.a, self.b)
285         for tag, alo, ahi, blo, bhi in matcher.get_opcodes():
286             if tag == 'replace':
287                 self.replace(alo, ahi, blo, bhi)
288             elif tag == 'delete':
289                 self.delete(alo, ahi, blo, bhi)
290             elif tag == 'insert':
291                 self.insert(alo, ahi, blo, bhi)
292             elif tag == 'equal':
293                 self.equal(alo, ahi, blo, bhi)
294             else:
295                 raise ValueError, 'unknown tag %s' % (tag,)
296
297     def isjunk(self, call):
298         return call.functionName == 'glGetError' and call.ret in ('GL_NO_ERROR', 0)
299
300     def replace(self, alo, ahi, blo, bhi):
301         assert alo < ahi and blo < bhi
302
303         a_names = [call.functionName for call in self.a[alo:ahi]]
304         b_names = [call.functionName for call in self.b[blo:bhi]]
305
306         matcher = difflib.SequenceMatcher(None, a_names, b_names)
307         for tag, _alo, _ahi, _blo, _bhi in matcher.get_opcodes():
308             _alo += alo
309             _ahi += alo
310             _blo += blo
311             _bhi += blo
312             if tag == 'replace':
313                 self.replace_dissimilar(_alo, _ahi, _blo, _bhi)
314             elif tag == 'delete':
315                 self.delete(_alo, _ahi, _blo, _bhi)
316             elif tag == 'insert':
317                 self.insert(_alo, _ahi, _blo, _bhi)
318             elif tag == 'equal':
319                 self.replace_similar(_alo, _ahi, _blo, _bhi)
320             else:
321                 raise ValueError, 'unknown tag %s' % (tag,)
322
323     def replace_similar(self, alo, ahi, blo, bhi):
324         assert alo < ahi and blo < bhi
325         assert ahi - alo == bhi - blo
326         for i in xrange(0, bhi - blo):
327             self.highlighter.write('| ')
328             a_call = self.a[alo + i]
329             b_call = self.b[blo + i]
330             assert a_call.functionName == b_call.functionName
331             self.dumpCallNos(a_call.no, b_call.no)
332             self.highlighter.bold(True)
333             self.highlighter.write(b_call.functionName)
334             self.highlighter.bold(False)
335             self.highlighter.write('(')
336             sep = ''
337             numArgs = max(len(a_call.args), len(b_call.args))
338             for j in xrange(numArgs):
339                 self.highlighter.write(sep)
340                 try:
341                     a_arg = a_call.args[j]
342                 except IndexError:
343                     pass
344                 try:
345                     b_arg = b_call.args[j]
346                 except IndexError:
347                     pass
348                 self.replace_value(a_arg, b_arg)
349                 sep = ', '
350             self.highlighter.write(')')
351             if a_call.ret is not None or b_call.ret is not None:
352                 self.highlighter.write(' = ')
353                 self.replace_value(a_call.ret, b_call.ret)
354             self.highlighter.write('\n')
355
356     def replace_dissimilar(self, alo, ahi, blo, bhi):
357         assert alo < ahi and blo < bhi
358         if bhi - blo < ahi - alo:
359             self.insert(alo, alo, blo, bhi)
360             self.delete(alo, ahi, bhi, bhi)
361         else:
362             self.delete(alo, ahi, blo, blo)
363             self.insert(ahi, ahi, blo, bhi)
364
365     def replace_value(self, a, b):
366         if b == a:
367             self.highlighter.write(self.dumper.visit(b))
368         else:
369             self.highlighter.strike()
370             self.highlighter.color(self.delete_color)
371             self.highlighter.write(self.dumper.visit(a))
372             self.highlighter.normal()
373             self.highlighter.write(" ")
374             self.highlighter.color(self.insert_color)
375             self.highlighter.write(self.dumper.visit(b))
376             self.highlighter.normal()
377
378     escape = "\33["
379
380     def delete(self, alo, ahi, blo, bhi):
381         assert alo < ahi
382         assert blo == bhi
383         for i in xrange(alo, ahi):
384             call = self.a[i]
385             self.highlighter.write('- ')
386             self.dumpCallNos(call.no, None)
387             self.highlighter.strike()
388             self.highlighter.color(self.delete_color)
389             self.dumpCall(call)
390
391     def insert(self, alo, ahi, blo, bhi):
392         assert alo == ahi
393         assert blo < bhi
394         for i in xrange(blo, bhi):
395             call = self.b[i]
396             self.highlighter.write('+ ')
397             self.dumpCallNos(None, call.no)
398             self.highlighter.color(self.insert_color)
399             self.dumpCall(call)
400
401     def equal(self, alo, ahi, blo, bhi):
402         assert alo < ahi and blo < bhi
403         assert ahi - alo == bhi - blo
404         for i in xrange(0, bhi - blo):
405             self.highlighter.write('  ')
406             a_call = self.a[alo + i]
407             b_call = self.b[blo + i]
408             assert a_call.functionName == b_call.functionName
409             assert len(a_call.args) == len(b_call.args)
410             self.dumpCallNos(a_call.no, b_call.no)
411             self.dumpCall(b_call)
412
413     def dumpCallNos(self, aNo, bNo):
414         if not self.callNos:
415             return
416
417         if aNo is None:
418             self.highlighter.write(' '*self.aSpace)
419         else:
420             aNoStr = str(aNo)
421             self.highlighter.strike()
422             self.highlighter.color(self.delete_color)
423             self.highlighter.write(aNoStr)
424             self.highlighter.normal()
425             self.aSpace = len(aNoStr)
426         self.highlighter.write(' ')
427         if bNo is None:
428             self.highlighter.write(' '*self.bSpace)
429         else:
430             bNoStr = str(bNo)
431             self.highlighter.color(self.insert_color)
432             self.highlighter.write(bNoStr)
433             self.highlighter.normal()
434             self.bSpace = len(bNoStr)
435         self.highlighter.write(' ')
436
437     def dumpCall(self, call):
438         self.highlighter.bold(True)
439         self.highlighter.write(call.functionName)
440         self.highlighter.bold(False)
441         self.highlighter.write('(' + ', '.join(itertools.imap(self.dumper.visit, call.args)) + ')')
442         if call.ret is not None:
443             self.highlighter.write(' = ' + self.dumper.visit(call.ret))
444         self.highlighter.normal()
445         self.highlighter.write('\n')
446
447
448
449 ##########################################################################/
450 #
451 # Main program
452 #
453
454
455 def which(executable):
456     '''Search for the executable on the PATH.'''
457
458     if platform.system() == 'Windows':
459         exts = ['.exe']
460     else:
461         exts = ['']
462     dirs = os.environ['PATH'].split(os.path.pathsep)
463     for dir in dirs:
464         path = os.path.join(dir, executable)
465         for ext in exts:
466             if os.path.exists(path + ext):
467                 return True
468     return False
469
470
471 def main():
472     '''Main program.
473     '''
474
475     # Parse command line options
476     optparser = optparse.OptionParser(
477         usage='\n\t%prog [options] TRACE TRACE',
478         version='%%prog')
479     optparser.add_option(
480         '-a', '--apitrace', metavar='PROGRAM',
481         type='string', dest='apitrace', default='apitrace',
482         help='apitrace command [default: %default]')
483     optparser.add_option(
484         '-d', '--diff',
485         type="choice", choices=('diff', 'sdiff', 'wdiff', 'python'),
486         dest="diff", default=None,
487         help="diff program: wdiff, sdiff, or diff [default: auto]")
488     optparser.add_option(
489         '-c', '--calls', metavar='CALLSET',
490         type="string", dest="calls", default='0-10000',
491         help="calls to compare [default: %default]")
492     optparser.add_option(
493         '--ref-calls', metavar='CALLSET',
494         type="string", dest="ref_calls", default=None,
495         help="calls to compare from reference trace")
496     optparser.add_option(
497         '--src-calls', metavar='CALLSET',
498         type="string", dest="src_calls", default=None,
499         help="calls to compare from source trace")
500     optparser.add_option(
501         '--call-nos',
502         action="store_true",
503         dest="call_nos", default=False,
504         help="dump call numbers")
505     optparser.add_option(
506         '-w', '--width', metavar='NUM',
507         type="int", dest="width",
508         help="columns [default: auto]")
509
510     (options, args) = optparser.parse_args(sys.argv[1:])
511     if len(args) != 2:
512         optparser.error("incorrect number of arguments")
513
514     if options.diff is None:
515         if platform.system() == 'Windows':
516             options.diff = 'python'
517         else:
518             if which('wdiff'):
519                 options.diff = 'wdiff'
520             else:
521                 sys.stderr.write('warning: wdiff not found\n')
522                 if which('sdiff'):
523                     options.diff = 'sdiff'
524                 else:
525                     sys.stderr.write('warning: sdiff not found\n')
526                     options.diff = 'diff'
527
528     if options.ref_calls is None:
529         options.ref_calls = options.calls
530     if options.src_calls is None:
531         options.src_calls = options.calls
532
533     ref_trace, src_trace = args
534
535     if options.diff == 'python':
536         differ = PythonDiffer(options.apitrace, options.call_nos)
537     else:
538         differ = ExternalDiffer(options.apitrace, options.diff, options.width)
539     differ.setRefTrace(ref_trace, options.ref_calls)
540     differ.setSrcTrace(src_trace, options.src_calls)
541     differ.diff()
542
543
544 if __name__ == '__main__':
545     main()