]> git.cworth.org Git - apitrace/blob - scripts/retracediff.py
Use skiplist-based FastCallSet within trace::CallSet
[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 Setup:
52
53     def __init__(self, args, env=None):
54         self.args = args
55         self.env = env
56
57     def _retrace(self, args):
58         cmd = [
59             options.retrace,
60         ] + args + self.args
61         if self.env:
62             for name, value in self.env.iteritems():
63                 sys.stderr.write('%s=%s ' % (name, value))
64         sys.stderr.write(' '.join(cmd) + '\n')
65         try:
66             return subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL)
67         except OSError, ex:
68             sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror))
69             sys.exit(1)
70
71     def retrace(self):
72         return self._retrace([
73             '-s', '-',
74             '-S', options.snapshot_frequency,
75         ])
76
77     def dump_state(self, call_no):
78         '''Get the state dump at the specified call no.'''
79
80         p = self._retrace([
81             '-D', str(call_no),
82         ])
83         state = jsondiff.load(p.stdout)
84         p.wait()
85         return state.get('parameters', {})
86
87     def diff_state(self, ref_call_no, src_call_no, stream):
88         '''Compare the state between two calls.'''
89
90         ref_state = self.dump_state(ref_call_no)
91         src_state = self.dump_state(src_call_no)
92
93         stream.flush()
94         differ = jsondiff.Differ(stream)
95         differ.visit(ref_state, src_state)
96         stream.write('\n')
97
98
99 def read_pnm(stream):
100     '''Read a PNM from the stream, and return the image object, and the comment.'''
101
102     magic = stream.readline()
103     if not magic:
104         return None, None
105     magic = magic.rstrip()
106     if magic == 'P5':
107         channels = 1
108         mode = 'L'
109     elif magic == 'P6':
110         channels = 3
111         mode = 'RGB'
112     else:
113         raise Exception('Unsupported magic `%s`' % magic)
114     comment = ''
115     line = stream.readline()
116     while line.startswith('#'):
117         comment += line[1:]
118         line = stream.readline()
119     width, height = map(int, line.strip().split())
120     maximum = int(stream.readline().strip())
121     assert maximum == 255
122     data = stream.read(height * width * channels)
123     image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1)
124     return image, comment
125
126
127 def parse_env(optparser, entries):
128     '''Translate a list of NAME=VALUE entries into an environment dictionary.'''
129
130     if not entries:
131         return None
132
133     env = os.environ.copy()
134     for entry in entries:
135         try:
136             name, var = entry.split('=', 1)
137         except Exception:
138             optparser.error('invalid environment entry %r' % entry)
139         env[name] = var
140     return env
141
142
143 def main():
144     '''Main program.
145     '''
146
147     global options
148
149     # Parse command line options
150     optparser = optparse.OptionParser(
151         usage='\n\t%prog [options] -- [glretrace options] <trace>',
152         version='%%prog')
153     optparser.add_option(
154         '-r', '--retrace', metavar='PROGRAM',
155         type='string', dest='retrace', default='glretrace',
156         help='retrace command [default: %default]')
157     optparser.add_option(
158         '--ref-driver', metavar='DRIVER',
159         type='string', dest='ref_driver', default=None,
160         help='force reference driver')
161     optparser.add_option(
162         '--src-driver', metavar='DRIVER',
163         type='string', dest='src_driver', default=None,
164         help='force source driver')
165     optparser.add_option(
166         '--ref-arg', metavar='OPTION',
167         type='string', action='append', dest='ref_args', default=[],
168         help='pass argument to reference retrace')
169     optparser.add_option(
170         '--src-arg', metavar='OPTION',
171         type='string', action='append', dest='src_args', default=[],
172         help='pass argument to source retrace')
173     optparser.add_option(
174         '--ref-env', metavar='NAME=VALUE',
175         type='string', action='append', dest='ref_env', default=[],
176         help='add variable to reference environment')
177     optparser.add_option(
178         '--src-env', metavar='NAME=VALUE',
179         type='string', action='append', dest='src_env', default=[],
180         help='add variable to source environment')
181     optparser.add_option(
182         '--diff-prefix', metavar='PATH',
183         type='string', dest='diff_prefix', default='.',
184         help='prefix for the difference images')
185     optparser.add_option(
186         '-t', '--threshold', metavar='BITS',
187         type="float", dest="threshold", default=12.0,
188         help="threshold precision  [default: %default]")
189     optparser.add_option(
190         '-S', '--snapshot-frequency', metavar='CALLSET',
191         type="string", dest="snapshot_frequency", default='draw',
192         help="calls to compare [default: %default]")
193     optparser.add_option(
194         '-o', '--output', metavar='FILE',
195         type="string", dest="output",
196         help="output file [default: stdout]")
197
198     (options, args) = optparser.parse_args(sys.argv[1:])
199     ref_env = parse_env(optparser, options.ref_env)
200     src_env = parse_env(optparser, options.src_env)
201     if not args:
202         optparser.error("incorrect number of arguments")
203     
204     if options.ref_driver:
205         options.ref_args.insert(0, '--driver=' + options.ref_driver)
206     if options.src_driver:
207         options.src_args.insert(0, '--driver=' + options.src_driver)
208
209     ref_setup = Setup(options.ref_args + args, ref_env)
210     src_setup = Setup(options.src_args + args, src_env)
211
212     if options.output:
213         output = open(options.output, 'wt')
214     else:
215         output = sys.stdout
216
217     highligher = AutoHighlighter(output)
218
219     highligher.write('call\tprecision\n')
220
221     last_bad = -1
222     last_good = 0
223     ref_proc = ref_setup.retrace()
224     try:
225         src_proc = src_setup.retrace()
226         try:
227             while True:
228                 # Get the reference image
229                 ref_image, ref_comment = read_pnm(ref_proc.stdout)
230                 if ref_image is None:
231                     break
232
233                 # Get the source image
234                 src_image, src_comment = read_pnm(src_proc.stdout)
235                 if src_image is None:
236                     break
237
238                 assert ref_comment == src_comment
239
240                 call_no = int(ref_comment.strip())
241
242                 # Compare the two images
243                 comparer = Comparer(ref_image, src_image)
244                 precision = comparer.precision()
245
246                 mismatch = precision < options.threshold
247
248                 if mismatch:
249                     highligher.color(highligher.red)
250                     highligher.bold()
251                 highligher.write('%u\t%f\n' % (call_no, precision))
252                 if mismatch:
253                     highligher.normal()
254
255                 if mismatch:
256                     if options.diff_prefix:
257                         prefix = os.path.join(options.diff_prefix, '%010u' % call_no)
258                         prefix_dir = os.path.dirname(prefix)
259                         if not os.path.isdir(prefix_dir):
260                             os.makedirs(prefix_dir)
261                         ref_image.save(prefix + '.ref.png')
262                         src_image.save(prefix + '.src.png')
263                         comparer.write_diff(prefix + '.diff.png')
264                     if last_bad < last_good:
265                         src_setup.diff_state(last_good, call_no, output)
266                     last_bad = call_no
267                 else:
268                     last_good = call_no
269
270                 highligher.flush()
271         finally:
272             src_proc.terminate()
273     finally:
274         ref_proc.terminate()
275
276
277 if __name__ == '__main__':
278     main()