X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=scripts%2Fretracediff.py;h=71e9f06473d38f38dd2e9f8259ad1b1104b10bef;hb=d79c9a22244ebc7aba491ad50ef2edced6c00d88;hp=b4bad08db73588b93237cfc6b494ad0f24dbd8a5;hpb=0b956fd72fee30dccb7c011450066bc8e47c2d66;p=apitrace diff --git a/scripts/retracediff.py b/scripts/retracediff.py index b4bad08..71e9f06 100755 --- a/scripts/retracediff.py +++ b/scripts/retracediff.py @@ -30,64 +30,161 @@ import optparse import os.path -import re -import shutil import subprocess import platform import sys -import tempfile + +from PIL import Image from snapdiff import Comparer +from highlight import AutoHighlighter import jsondiff # Null file, to use when we're not interested in subprocesses output if platform.system() == 'Windows': - NULL = open('NUL:', 'wt') + NULL = open('NUL:', 'wb') else: - NULL = open('/dev/null', 'wt') + NULL = open('/dev/null', 'wb') + + +class RetraceRun: + + def __init__(self, process): + self.process = process + + def nextSnapshot(self): + image, comment = read_pnm(self.process.stdout) + if image is None: + return None, None + + callNo = int(comment.strip()) + + return image, callNo + + def terminate(self): + try: + self.process.terminate() + except OSError: + # Avoid http://bugs.python.org/issue14252 + pass -class Setup: +class Retracer: - def __init__(self, args, env=None): + def __init__(self, retraceExe, args, env=None): + self.retraceExe = retraceExe self.args = args self.env = env - def retrace(self, snapshot_dir): + def _retrace(self, args, stdout=subprocess.PIPE): cmd = [ - options.retrace, - '-s', snapshot_dir + os.path.sep, - '-S', options.snapshot_frequency, - ] + self.args - p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL) - return p + self.retraceExe, + ] + args + self.args + if self.env: + for name, value in self.env.iteritems(): + sys.stderr.write('%s=%s ' % (name, value)) + sys.stderr.write(' '.join(cmd) + '\n') + try: + return subprocess.Popen(cmd, env=self.env, stdout=stdout, stderr=NULL) + except OSError, ex: + sys.stderr.write('error: failed to execute %s: %s\n' % (cmd[0], ex.strerror)) + sys.exit(1) + + def retrace(self, args): + p = self._retrace([]) + p.wait() + return p.returncode + + def snapshot(self, call_nos): + process = self._retrace([ + '-s', '-', + '-S', call_nos, + ]) + return RetraceRun(process) def dump_state(self, call_no): '''Get the state dump at the specified call no.''' - cmd = [ - options.retrace, + p = self._retrace([ '-D', str(call_no), - ] + self.args - p = subprocess.Popen(cmd, env=self.env, stdout=subprocess.PIPE, stderr=NULL) + ]) state = jsondiff.load(p.stdout) p.wait() - return state + return state.get('parameters', {}) + + def diff_state(self, ref_call_no, src_call_no, stream): + '''Compare the state between two calls.''' + + ref_state = self.dump_state(ref_call_no) + src_state = self.dump_state(src_call_no) + + stream.flush() + differ = jsondiff.Differ(stream) + differ.visit(ref_state, src_state) + stream.write('\n') + +def read_pnm(stream): + '''Read a PNM from the stream, and return the image object, and the comment.''' -def diff_state(setup, ref_call_no, src_call_no): - ref_state = setup.dump_state(ref_call_no) - src_state = setup.dump_state(src_call_no) - sys.stdout.flush() - differ = jsondiff.Differ(sys.stdout) - differ.visit(ref_state, src_state) - sys.stdout.write('\n') + magic = stream.readline() + if not magic: + return None, None + magic = magic.rstrip() + if magic == 'P5': + channels = 1 + bytesPerChannel = 1 + mode = 'L' + elif magic == 'P6': + channels = 3 + bytesPerChannel = 1 + mode = 'RGB' + elif magic == 'Pf': + channels = 1 + bytesPerChannel = 4 + mode = 'R' + elif magic == 'PF': + channels = 3 + bytesPerChannel = 4 + mode = 'RGB' + elif magic == 'PX': + channels = 4 + bytesPerChannel = 4 + mode = 'RGB' + else: + raise Exception('Unsupported magic `%s`' % magic) + comment = '' + line = stream.readline() + while line.startswith('#'): + comment += line[1:] + line = stream.readline() + width, height = map(int, line.strip().split()) + maximum = int(stream.readline().strip()) + if bytesPerChannel == 1: + assert maximum == 255 + else: + assert maximum == 1 + data = stream.read(height * width * channels * bytesPerChannel) + if bytesPerChannel == 4: + # Image magic only supports single channel floating point images, so + # represent the image as numpy arrays + + import numpy + pixels = numpy.fromstring(data, dtype=numpy.float32) + pixels.resize((height, width, channels)) + return pixels, comment + + image = Image.frombuffer(mode, (width, height), data, 'raw', mode, 0, 1) + return image, comment def parse_env(optparser, entries): '''Translate a list of NAME=VALUE entries into an environment dictionary.''' + if not entries: + return None + env = os.environ.copy() for entry in entries: try: @@ -112,92 +209,144 @@ def main(): '-r', '--retrace', metavar='PROGRAM', type='string', dest='retrace', default='glretrace', help='retrace command [default: %default]') + optparser.add_option( + '--ref-driver', metavar='DRIVER', + type='string', dest='ref_driver', default=None, + help='force reference driver') + optparser.add_option( + '--src-driver', metavar='DRIVER', + type='string', dest='src_driver', default=None, + help='force source driver') + optparser.add_option( + '--ref-arg', metavar='OPTION', + type='string', action='append', dest='ref_args', default=[], + help='pass argument to reference retrace') + optparser.add_option( + '--src-arg', metavar='OPTION', + type='string', action='append', dest='src_args', default=[], + help='pass argument to source retrace') optparser.add_option( '--ref-env', metavar='NAME=VALUE', type='string', action='append', dest='ref_env', default=[], - help='reference environment variable') + help='add variable to reference environment') optparser.add_option( '--src-env', metavar='NAME=VALUE', type='string', action='append', dest='src_env', default=[], - help='reference environment variable') + help='add variable to source environment') optparser.add_option( '--diff-prefix', metavar='PATH', type='string', dest='diff_prefix', default='.', - help='reference environment variable') + help='prefix for the difference images') optparser.add_option( '-t', '--threshold', metavar='BITS', type="float", dest="threshold", default=12.0, help="threshold precision [default: %default]") optparser.add_option( - '-S', '--snapshot-frequency', metavar='FREQUENCY', + '-S', '--snapshot-frequency', metavar='CALLSET', type="string", dest="snapshot_frequency", default='draw', - help="snapshot frequency [default: %default]") + help="calls to compare [default: %default]") + optparser.add_option( + '--diff-state', + action='store_true', dest='diff_state', default=False, + help='diff state between failing calls') + optparser.add_option( + '-o', '--output', metavar='FILE', + type="string", dest="output", + help="output file [default: stdout]") (options, args) = optparser.parse_args(sys.argv[1:]) ref_env = parse_env(optparser, options.ref_env) src_env = parse_env(optparser, options.src_env) if not args: optparser.error("incorrect number of arguments") + + if options.ref_driver: + options.ref_args.insert(0, '--driver=' + options.ref_driver) + if options.src_driver: + options.src_args.insert(0, '--driver=' + options.src_driver) - ref_setup = Setup(args, ref_env) - src_setup = Setup(args, src_env) + refRetracer = Retracer(options.retrace, options.ref_args + args, ref_env) + srcRetracer = Retracer(options.retrace, options.src_args + args, src_env) - image_re = re.compile('^Wrote (.*\.png)$') + if options.output: + output = open(options.output, 'wt') + else: + output = sys.stdout + + highligher = AutoHighlighter(output) + + highligher.write('call\tprecision\n') - last_good = -1 last_bad = -1 - ref_snapshot_dir = tempfile.mkdtemp() + last_good = 0 + refRun = refRetracer.snapshot(options.snapshot_frequency) try: - src_snapshot_dir = tempfile.mkdtemp() + srcRun = srcRetracer.snapshot(options.snapshot_frequency) try: - ref_proc = ref_setup.retrace(ref_snapshot_dir) - try: - src_proc = src_setup.retrace(src_snapshot_dir) - try: - for ref_line in ref_proc.stdout: - # Get the reference image - ref_line = ref_line.rstrip() - mo = image_re.match(ref_line) - if mo: - ref_image = mo.group(1) - for src_line in src_proc.stdout: - # Get the source image - src_line = src_line.rstrip() - mo = image_re.match(src_line) - if mo: - src_image = mo.group(1) - - root, ext = os.path.splitext(os.path.basename(src_image)) - call_no = int(root) - - # Compare the two images - comparer = Comparer(ref_image, src_image) - precision = comparer.precision() - - sys.stdout.write('%u %f\n' % (call_no, precision)) - - if precision < options.threshold: - if options.diff_prefix: - comparer.write_diff(os.path.join(options.diff_prefix, root + '.diff.png')) - if last_bad < last_good: - diff_state(src_setup, last_good, call_no) - last_bad = call_no - else: - last_good = call_no - - sys.stdout.flush() - - os.unlink(src_image) - break - os.unlink(ref_image) - finally: - src_proc.terminate() - finally: - ref_proc.terminate() + while True: + # Get the reference image + refImage, refCallNo = refRun.nextSnapshot() + if refImage is None: + break + + # Get the source image + srcImage, srcCallNo = srcRun.nextSnapshot() + if srcImage is None: + break + + assert refCallNo == srcCallNo + callNo = refCallNo + + # Compare the two images + if isinstance(refImage, Image.Image) and isinstance(srcImage, Image.Image): + # Using PIL + numpyImages = False + comparer = Comparer(refImage, srcImage) + precision = comparer.precision() + else: + # Using numpy (for floating point images) + # TODO: drop PIL when numpy path becomes general enough + import numpy + assert not isinstance(refImage, Image.Image) + assert not isinstance(srcImage, Image.Image) + numpyImages = True + assert refImage.shape == srcImage.shape + diffImage = numpy.square(srcImage - refImage) + match = numpy.all(diffImage == 0) + if match: + precision = 24 + else: + precision = 0 + + mismatch = precision < options.threshold + + if mismatch: + highligher.color(highligher.red) + highligher.bold() + highligher.write('%u\t%f\n' % (callNo, precision)) + if mismatch: + highligher.normal() + + if mismatch: + if options.diff_prefix and not numpyImages: + prefix = os.path.join(options.diff_prefix, '%010u' % callNo) + prefix_dir = os.path.dirname(prefix) + if not os.path.isdir(prefix_dir): + os.makedirs(prefix_dir) + refImage.save(prefix + '.ref.png') + srcImage.save(prefix + '.src.png') + comparer.write_diff(prefix + '.diff.png') + if last_bad < last_good and options.diff_state: + srcRetracer.diff_state(last_good, callNo, output) + last_bad = callNo + else: + last_good = callNo + + highligher.flush() finally: - shutil.rmtree(ref_snapshot_dir) + srcRun.terminate() finally: - shutil.rmtree(src_snapshot_dir) + refRun.terminate() if __name__ == '__main__':