]> git.cworth.org Git - apitrace/blob - scripts/retracediff.py
71e9f06473d38f38dd2e9f8259ad1b1104b10bef
[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     elif magic == 'PX':
152         channels = 4
153         bytesPerChannel = 4
154         mode = 'RGB'
155     else:
156         raise Exception('Unsupported magic `%s`' % magic)
157     comment = ''
158     line = stream.readline()
159     while line.startswith('#'):
160         comment += line[1:]
161         line = stream.readline()
162     width, height = map(int, line.strip().split())
163     maximum = int(stream.readline().strip())
164     if bytesPerChannel == 1:
165         assert maximum == 255
166     else:
167         assert maximum == 1
168     data = stream.read(height * width * channels * bytesPerChannel)
169     if bytesPerChannel == 4:
170         # Image magic only supports single channel floating point images, so
171         # represent the image as numpy arrays
172
173         import numpy
174         pixels = numpy.fromstring(data, dtype=numpy.float32)
175         pixels.resize((height, width, channels))
176         return pixels, comment
177
178     image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
179     return image, comment
180
181
182 def parse_env(optparser, entries):
183     '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
184
185     if not entries:
186         return None
187
188     env = os.environ.copy()
189     for entry in entries:
190         try:
191             name, var = entry.split('=', 1)
192         except Exception:
193             optparser.error('invalid environment entry %r' % entry)
194         env[name] = var
195     return env
196
197
198 def main():
199     '''Main program.
200     '''
201
202     global options
203
204     # Parse command line options
205     optparser = optparse.OptionParser(
206         usage='\n\t%prog [options] -- [glretrace options] <trace>',
207         version='%%prog')
208     optparser.add_option(
209         '-r', '--retrace', metavar='PROGRAM',
210         type='string', dest='retrace', default='glretrace',
211         help='retrace command [default: %default]')
212     optparser.add_option(
213         '--ref-driver', metavar='DRIVER',
214         type='string', dest='ref_driver', default=None,
215         help='force reference driver')
216     optparser.add_option(
217         '--src-driver', metavar='DRIVER',
218         type='string', dest='src_driver', default=None,
219         help='force source driver')
220     optparser.add_option(
221         '--ref-arg', metavar='OPTION',
222         type='string', action='append', dest='ref_args', default=[],
223         help='pass argument to reference retrace')
224     optparser.add_option(
225         '--src-arg', metavar='OPTION',
226         type='string', action='append', dest='src_args', default=[],
227         help='pass argument to source retrace')
228     optparser.add_option(
229         '--ref-env', metavar='NAME=VALUE',
230         type='string', action='append', dest='ref_env', default=[],
231         help='add variable to reference environment')
232     optparser.add_option(
233         '--src-env', metavar='NAME=VALUE',
234         type='string', action='append', dest='src_env', default=[],
235         help='add variable to source environment')
236     optparser.add_option(
237         '--diff-prefix', metavar='PATH',
238         type='string', dest='diff_prefix', default='.',
239         help='prefix for the difference images')
240     optparser.add_option(
241         '-t', '--threshold', metavar='BITS',
242         type="float", dest="threshold", default=12.0,
243         help="threshold precision  [default: %default]")
244     optparser.add_option(
245         '-S', '--snapshot-frequency', metavar='CALLSET',
246         type="string", dest="snapshot_frequency", default='draw',
247         help="calls to compare [default: %default]")
248     optparser.add_option(
249         '--diff-state',
250         action='store_true', dest='diff_state', default=False,
251         help='diff state between failing calls')
252     optparser.add_option(
253         '-o', '--output', metavar='FILE',
254         type="string", dest="output",
255         help="output file [default: stdout]")
256
257     (options, args) = optparser.parse_args(sys.argv[1:])
258     ref_env = parse_env(optparser, options.ref_env)
259     src_env = parse_env(optparser, options.src_env)
260     if not args:
261         optparser.error("incorrect number of arguments")
262     
263     if options.ref_driver:
264         options.ref_args.insert(0, '--driver=' + options.ref_driver)
265     if options.src_driver:
266         options.src_args.insert(0, '--driver=' + options.src_driver)
267
268     refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
269     srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
270
271     if options.output:
272         output = open(options.output, 'wt')
273     else:
274         output = sys.stdout
275
276     highligher = AutoHighlighter(output)
277
278     highligher.write('call\tprecision\n')
279
280     last_bad = -1
281     last_good = 0
282     refRun = refRetracer.snapshot(options.snapshot_frequency)
283     try:
284         srcRun = srcRetracer.snapshot(options.snapshot_frequency)
285         try:
286             while True:
287                 # Get the reference image
288                 refImage, refCallNo = refRun.nextSnapshot()
289                 if refImage is None:
290                     break
291
292                 # Get the source image
293                 srcImage, srcCallNo = srcRun.nextSnapshot()
294                 if srcImage is None:
295                     break
296
297                 assert refCallNo == srcCallNo
298                 callNo = refCallNo
299
300                 # Compare the two images
301                 if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image):
302                     # Using PIL
303                     numpyImages = False
304                     comparer = Comparer(refImage, srcImage)
305                     precision = comparer.precision()
306                 else:
307                     # Using numpy (for floating point images)
308                     # TODO: drop PIL when numpy path becomes general enough
309                     import numpy
310                     assert not isinstance(refImage, Image.Image)
311                     assert not isinstance(srcImage, Image.Image)
312                     numpyImages = True
313                     assert refImage.shape == srcImage.shape
314                     diffImage = numpy.square(srcImage - refImage)
315                     match = numpy.all(diffImage == 0)
316                     if match:
317                         precision = 24
318                     else:
319                         precision = 0
320
321                 mismatch = precision < options.threshold
322
323                 if mismatch:
324                     highligher.color(highligher.red)
325                     highligher.bold()
326                 highligher.write('%u\t%f\n' % (callNo, precision))
327                 if mismatch:
328                     highligher.normal()
329
330                 if mismatch:
331                     if options.diff_prefix and not numpyImages:
332                         prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
333                         prefix_dir = os.path.dirname(prefix)
334                         if not os.path.isdir(prefix_dir):
335                             os.makedirs(prefix_dir)
336                         refImage.save(prefix + '.ref.png')
337                         srcImage.save(prefix + '.src.png')
338                         comparer.write_diff(prefix + '.diff.png')
339                     if last_bad < last_good and options.diff_state:
340                         srcRetracer.diff_state(last_good, callNo, output)
341                     last_bad = callNo
342                 else:
343                     last_good = callNo
344
345                 highligher.flush()
346         finally:
347             srcRun.terminate()
348     finally:
349         refRun.terminate()
350
351
352 if __name__ == '__main__':
353     main()