]> git.cworth.org Git - apitrace/blob - scripts/retracediff.py
89aac5a526313524e957acb31a0ababe0068bf61
[apitrace] / scripts / retracediff.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 '''Run two retrace instances in parallel, comparing generated snapshots.
28 '''
29
30
31 import optparse
32 import os.path
33 import subprocess
34 import platform
35 import sys
36
37 from PIL import Image
38
39 from snapdiff import Comparer
40 from highlight import AutoHighlighter
41 import jsondiff
42
43
44 # Null file, to use when we're not interested in subprocesses output
45 if platform.system() == 'Windows':
46     NULL = open('NUL:', 'wb')
47 else:
48     NULL = open('/dev/null', 'wb')
49
50
51 class RetraceRun:
52
53     def __init__(self, process):
54         self.process = process
55
56     def nextSnapshot(self):
57         image, comment = read_pnm(self.process.stdout)
58         if image is None:
59             return None, None
60
61         callNo = int(comment.strip())
62
63         return image, callNo
64
65     def terminate(self):
66         try:
67             self.process.terminate()
68         except OSError:
69             # Avoid http://bugs.python.org/issue14252
70             pass
71
72
73 class Retracer:
74
75     def __init__(self, retraceExe, args, env=None):
76         self.retraceExe = retraceExe
77         self.args = args
78         self.env = env
79
80     def _retrace(self, args, stdout=subprocess.PIPE):
81         cmd = [
82             self.retraceExe,
83         ] + args + self.args
84         if self.env:
85             for name, value in self.env.iteritems():
86                 sys.stderr.write('%s=%s ' % (name, value))
87         sys.stderr.write(' '.join(cmd) + '\n')
88         try:
89             return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL)
90         except OSError, ex:
91             sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
92             sys.exit(1)
93
94     def retrace(self, args):
95         p = self._retrace([])
96         p.wait()
97         return p.returncode
98
99     def snapshot(self, call_nos):
100         process = self._retrace([
101             '-s', '-',
102             '-S', call_nos,
103         ])
104         return RetraceRun(process)
105
106     def dump_state(self, call_no):
107         '''Get the state dump at the specified call no.'''
108
109         p = self._retrace([
110             '-D', str(call_no),
111         ])
112         state = jsondiff.load(p.stdout)
113         p.wait()
114         return state.get('parameters', {})
115
116     def diff_state(self, ref_call_no, src_call_no, stream):
117         '''Compare the state between two calls.'''
118
119         ref_state = self.dump_state(ref_call_no)
120         src_state = self.dump_state(src_call_no)
121
122         stream.flush()
123         differ = jsondiff.Differ(stream)
124         differ.visit(ref_state, src_state)
125         stream.write('\n')
126
127
128 def read_pnm(stream):
129     '''Read a PNM from the stream, and return the image object, and the comment.'''
130
131     magic = stream.readline()
132     if not magic:
133         return None, None
134     magic = magic.rstrip()
135     if magic == 'P5':
136         channels = 1
137         bytesPerChannel = 1
138         mode = 'L'
139     elif magic == 'P6':
140         channels = 3
141         bytesPerChannel = 1
142         mode = 'RGB'
143     elif magic == 'Pf':
144         channels = 1
145         bytesPerChannel = 4
146         mode = 'R'
147     elif magic == 'PF':
148         channels = 3
149         bytesPerChannel = 4
150         mode = 'RGB'
151     else:
152         raise Exception('Unsupported magic `%s`' % magic)
153     comment = ''
154     line = stream.readline()
155     while line.startswith('#'):
156         comment += line[1:]
157         line = stream.readline()
158     width, height = map(int, line.strip().split())
159     if bytesPerChannel == 1:
160         maximum = int(stream.readline().strip())
161         assert maximum == 255
162     data = stream.read(height * width * channels * bytesPerChannel)
163     if magic == 'PF':
164         # XXX: Image magic only supports single channel floating point images,
165         # so convert to 8bit RGB
166         pixels = array('f', data)
167         pixels *= 255
168         pixels = array('B', pixels)
169         data = pixels.tostring()
170
171     image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
172     return image, comment
173
174
175 def parse_env(optparser, entries):
176     '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
177
178     if not entries:
179         return None
180
181     env = os.environ.copy()
182     for entry in entries:
183         try:
184             name, var = entry.split('=', 1)
185         except Exception:
186             optparser.error('invalid environment entry %r' % entry)
187         env[name] = var
188     return env
189
190
191 def main():
192     '''Main program.
193     '''
194
195     global options
196
197     # Parse command line options
198     optparser = optparse.OptionParser(
199         usage='\n\t%prog [options] -- [glretrace options] <trace>',
200         version='%%prog')
201     optparser.add_option(
202         '-r', '--retrace', metavar='PROGRAM',
203         type='string', dest='retrace', default='glretrace',
204         help='retrace command [default: %default]')
205     optparser.add_option(
206         '--ref-driver', metavar='DRIVER',
207         type='string', dest='ref_driver', default=None,
208         help='force reference driver')
209     optparser.add_option(
210         '--src-driver', metavar='DRIVER',
211         type='string', dest='src_driver', default=None,
212         help='force source driver')
213     optparser.add_option(
214         '--ref-arg', metavar='OPTION',
215         type='string', action='append', dest='ref_args', default=[],
216         help='pass argument to reference retrace')
217     optparser.add_option(
218         '--src-arg', metavar='OPTION',
219         type='string', action='append', dest='src_args', default=[],
220         help='pass argument to source retrace')
221     optparser.add_option(
222         '--ref-env', metavar='NAME=VALUE',
223         type='string', action='append', dest='ref_env', default=[],
224         help='add variable to reference environment')
225     optparser.add_option(
226         '--src-env', metavar='NAME=VALUE',
227         type='string', action='append', dest='src_env', default=[],
228         help='add variable to source environment')
229     optparser.add_option(
230         '--diff-prefix', metavar='PATH',
231         type='string', dest='diff_prefix', default='.',
232         help='prefix for the difference images')
233     optparser.add_option(
234         '-t', '--threshold', metavar='BITS',
235         type="float", dest="threshold", default=12.0,
236         help="threshold precision  [default: %default]")
237     optparser.add_option(
238         '-S', '--snapshot-frequency', metavar='CALLSET',
239         type="string", dest="snapshot_frequency", default='draw',
240         help="calls to compare [default: %default]")
241     optparser.add_option(
242         '--diff-state',
243         action='store_true', dest='diff_state', default=False,
244         help='diff state between failing calls')
245     optparser.add_option(
246         '-o', '--output', metavar='FILE',
247         type="string", dest="output",
248         help="output file [default: stdout]")
249
250     (options, args) = optparser.parse_args(sys.argv[1:])
251     ref_env = parse_env(optparser, options.ref_env)
252     src_env = parse_env(optparser, options.src_env)
253     if not args:
254         optparser.error("incorrect number of arguments")
255     
256     if options.ref_driver:
257         options.ref_args.insert(0, '--driver=' + options.ref_driver)
258     if options.src_driver:
259         options.src_args.insert(0, '--driver=' + options.src_driver)
260
261     refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
262     srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
263
264     if options.output:
265         output = open(options.output, 'wt')
266     else:
267         output = sys.stdout
268
269     highligher = AutoHighlighter(output)
270
271     highligher.write('call\tprecision\n')
272
273     last_bad = -1
274     last_good = 0
275     refRun = refRetracer.snapshot(options.snapshot_frequency)
276     try:
277         srcRun = srcRetracer.snapshot(options.snapshot_frequency)
278         try:
279             while True:
280                 # Get the reference image
281                 refImage, refCallNo = refRun.nextSnapshot()
282                 if refImage is None:
283                     break
284
285                 # Get the source image
286                 srcImage, srcCallNo = srcRun.nextSnapshot()
287                 if srcImage is None:
288                     break
289
290                 assert refCallNo == srcCallNo
291                 callNo = refCallNo
292
293                 # Compare the two images
294                 comparer = Comparer(refImage, srcImage)
295                 precision = comparer.precision()
296
297                 mismatch = precision < options.threshold
298
299                 if mismatch:
300                     highligher.color(highligher.red)
301                     highligher.bold()
302                 highligher.write('%u\t%f\n' % (callNo, precision))
303                 if mismatch:
304                     highligher.normal()
305
306                 if mismatch:
307                     if options.diff_prefix:
308                         prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
309                         prefix_dir = os.path.dirname(prefix)
310                         if not os.path.isdir(prefix_dir):
311                             os.makedirs(prefix_dir)
312                         refImage.save(prefix + '.ref.png')
313                         srcImage.save(prefix + '.src.png')
314                         comparer.write_diff(prefix + '.diff.png')
315                     if last_bad < last_good and options.diff_state:
316                         srcRetracer.diff_state(last_good, callNo, output)
317                     last_bad = callNo
318                 else:
319                     last_good = callNo
320
321                 highligher.flush()
322         finally:
323             srcRun.terminate()
324     finally:
325         refRun.terminate()
326
327
328 if __name__ == '__main__':
329     main()