]> git.cworth.org Git - apitrace/blob - scripts/retracediff.py
retracediff: Only diff state if specified by command line option.
[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:', 'wt')
47 else:
48     NULL = open('/dev/null', 'wt')
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         mode = 'L'
138     elif magic == 'P6':
139         channels = 3
140         mode = 'RGB'
141     else:
142         raise Exception('Unsupported magic `%s`' % magic)
143     comment = ''
144     line = stream.readline()
145     while line.startswith('#'):
146         comment += line[1:]
147         line = stream.readline()
148     width, height = map(int, line.strip().split())
149     maximum = int(stream.readline().strip())
150     assert maximum == 255
151     data = stream.read(height * width * channels)
152     image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
153     return image, comment
154
155
156 def parse_env(optparser, entries):
157     '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
158
159     if not entries:
160         return None
161
162     env = os.environ.copy()
163     for entry in entries:
164         try:
165             name, var = entry.split('=', 1)
166         except Exception:
167             optparser.error('invalid environment entry %r' % entry)
168         env[name] = var
169     return env
170
171
172 def main():
173     '''Main program.
174     '''
175
176     global options
177
178     # Parse command line options
179     optparser = optparse.OptionParser(
180         usage='\n\t%prog [options] -- [glretrace options] <trace>',
181         version='%%prog')
182     optparser.add_option(
183         '-r', '--retrace', metavar='PROGRAM',
184         type='string', dest='retrace', default='glretrace',
185         help='retrace command [default: %default]')
186     optparser.add_option(
187         '--ref-driver', metavar='DRIVER',
188         type='string', dest='ref_driver', default=None,
189         help='force reference driver')
190     optparser.add_option(
191         '--src-driver', metavar='DRIVER',
192         type='string', dest='src_driver', default=None,
193         help='force source driver')
194     optparser.add_option(
195         '--ref-arg', metavar='OPTION',
196         type='string', action='append', dest='ref_args', default=[],
197         help='pass argument to reference retrace')
198     optparser.add_option(
199         '--src-arg', metavar='OPTION',
200         type='string', action='append', dest='src_args', default=[],
201         help='pass argument to source retrace')
202     optparser.add_option(
203         '--ref-env', metavar='NAME=VALUE',
204         type='string', action='append', dest='ref_env', default=[],
205         help='add variable to reference environment')
206     optparser.add_option(
207         '--src-env', metavar='NAME=VALUE',
208         type='string', action='append', dest='src_env', default=[],
209         help='add variable to source environment')
210     optparser.add_option(
211         '--diff-prefix', metavar='PATH',
212         type='string', dest='diff_prefix', default='.',
213         help='prefix for the difference images')
214     optparser.add_option(
215         '-t', '--threshold', metavar='BITS',
216         type="float", dest="threshold", default=12.0,
217         help="threshold precision  [default: %default]")
218     optparser.add_option(
219         '-S', '--snapshot-frequency', metavar='CALLSET',
220         type="string", dest="snapshot_frequency", default='draw',
221         help="calls to compare [default: %default]")
222     optparser.add_option(
223         '--diff-state',
224         action='store_true', dest='diff_state', default=False,
225         help='diff state between failing calls')
226     optparser.add_option(
227         '-o', '--output', metavar='FILE',
228         type="string", dest="output",
229         help="output file [default: stdout]")
230
231     (options, args) = optparser.parse_args(sys.argv[1:])
232     ref_env = parse_env(optparser, options.ref_env)
233     src_env = parse_env(optparser, options.src_env)
234     if not args:
235         optparser.error("incorrect number of arguments")
236     
237     if options.ref_driver:
238         options.ref_args.insert(0, '--driver=' + options.ref_driver)
239     if options.src_driver:
240         options.src_args.insert(0, '--driver=' + options.src_driver)
241
242     refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env)
243     srcRetracer = Retracer(options.retrace, options.src_args + args, src_env)
244
245     if options.output:
246         output = open(options.output, 'wt')
247     else:
248         output = sys.stdout
249
250     highligher = AutoHighlighter(output)
251
252     highligher.write('call\tprecision\n')
253
254     last_bad = -1
255     last_good = 0
256     refRun = refRetracer.snapshot(options.snapshot_frequency)
257     try:
258         srcRun = srcRetracer.snapshot(options.snapshot_frequency)
259         try:
260             while True:
261                 # Get the reference image
262                 refImage, refCallNo = refRun.nextSnapshot()
263                 if refImage is None:
264                     break
265
266                 # Get the source image
267                 srcImage, srcCallNo = srcRun.nextSnapshot()
268                 if srcImage is None:
269                     break
270
271                 assert refCallNo == srcCallNo
272                 callNo = refCallNo
273
274                 # Compare the two images
275                 comparer = Comparer(refImage, srcImage)
276                 precision = comparer.precision()
277
278                 mismatch = precision < options.threshold
279
280                 if mismatch:
281                     highligher.color(highligher.red)
282                     highligher.bold()
283                 highligher.write('%u\t%f\n' % (callNo, precision))
284                 if mismatch:
285                     highligher.normal()
286
287                 if mismatch:
288                     if options.diff_prefix:
289                         prefix = os.path.join(options.diff_prefix, '%010u' % callNo)
290                         prefix_dir = os.path.dirname(prefix)
291                         if not os.path.isdir(prefix_dir):
292                             os.makedirs(prefix_dir)
293                         refImage.save(prefix + '.ref.png')
294                         srcImage.save(prefix + '.src.png')
295                         comparer.write_diff(prefix + '.diff.png')
296                     if last_bad < last_good and options.diff_state:
297                         srcRetracer.diff_state(last_good, callNo, output)
298                     last_bad = callNo
299                 else:
300                     last_good = callNo
301
302                 highligher.flush()
303         finally:
304             srcRun.terminate()
305     finally:
306         refRun.terminate()
307
308
309 if __name__ == '__main__':
310     main()