') print '' def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points, regressions, requested_width, requested_height, title): """Outputs an svg/xhtml view of the data.""" print '' print '' print '' print '%s' % qe(title) print '' print '' output_svg(lines, regressions, requested_width, requested_height) #output the manipulation controls print """ """ all_settings = {} variant_settings = set() for label in lines.keys(): for key, value in label.settings.items(): if key not in all_settings: all_settings[key] = value elif all_settings[key] != value: variant_settings.add(key) print '' % requested_width #output column headers print """ ' print '
""" for k in variant_settings: print '' % qe(k) print '' #output column contents print '' print '' print """
Bench Type Bitmap Config Timer Type (Cpu/Gpu/wall)%s
' create_select(lambda l: l.bench, lines, 'benchSelect') print '' create_select(lambda l: l.config, lines) print '' create_select(lambda l: l.time_type, lines) for k in variant_settings: print '' create_select(lambda l: l.settings.get(k, " "), lines) print '' print '' print '

""" output_ignored_data_points_warning(ignored_revision_data_points) print '
' print '
' print '' % ( qe(title), bench_util.CreateRevisionLink(oldest_revision), bench_util.CreateRevisionLink(newest_revision)) print """
%s

revisions r%s - r%s

Brighter red indicates tests that have gotten worse; brighter green indicates tests that have gotten better.

To highlight individual tests, hold down CONTROL and mouse over graph lines.

To highlight revision numbers, hold down SHIFT and mouse over the graph area.

To only show certain tests on the graph, select any combination of tests in the selectors at left. (To show all, select all.)

Use buttons at left to mark/clear points on the lines for selected benchmarks.

""" def compute_size(requested_width, requested_height, rev_width, time_height): """Converts potentially empty requested size into a concrete size. (Number?, Number?) -> (Number, Number)""" pic_width = 0 pic_height = 0 if (requested_width is not None and requested_height is not None): pic_height = requested_height pic_width = requested_width elif (requested_width is not None): pic_width = requested_width pic_height = pic_width * (float(time_height) / rev_width) elif (requested_height is not None): pic_height = requested_height pic_width = pic_height * (float(rev_width) / time_height) else: pic_height = 800 pic_width = max(rev_width*3 , pic_height * (float(rev_width) / time_height)) return (pic_width, pic_height) def output_svg(lines, regressions, requested_width, requested_height): """Outputs an svg view of the data.""" (global_min_x, _), (global_max_x, global_max_y) = bounds(lines) max_up_slope, min_down_slope = bounds_slope(regressions) #output global_min_y = 0 x = global_min_x y = global_min_y w = global_max_x - global_min_x h = global_max_y - global_min_y font_size = 16 line_width = 2 pic_width, pic_height = compute_size(requested_width, requested_height , w, h) def cw(w1): """Converts a revision difference to display width.""" return (pic_width / float(w)) * w1 def cx(x): """Converts a revision to a horizontal display position.""" return cw(x - global_min_x) def ch(h1): """Converts a time difference to a display height.""" return -(pic_height / float(h)) * h1 def cy(y): """Converts a time to a vertical display position.""" return pic_height + ch(y - global_min_y) print '' % ( pic_height, h) print '' print """ """ #output the revisions print """ """ def print_rect(x, y, w, h, revision): """Outputs a revision rectangle in display space, taking arguments in revision space.""" disp_y = cy(y) disp_h = ch(h) if disp_h < 0: disp_y += disp_h disp_h = -disp_h print '' xes = set() for line in lines.itervalues(): for point in line: xes.add(point[0]) revisions = list(xes) revisions.sort() left = x current_revision = revisions[0] for next_revision in revisions[1:]: width = (((next_revision - current_revision) / 2.0) + (current_revision - left)) print_rect(left, y, width, h, current_revision) left += width current_revision = next_revision print_rect(left, y, x+w - left, h, current_revision) #output the lines print """ """ for label, line in lines.items(): print '' % qa(label) r = 128 g = 128 b = 128 a = .10 if label in regressions: regression = regressions[label] min_slope = regression.find_min_slope() if min_slope < 0: d = max(0, (min_slope / min_down_slope)) g += int(d*128) a += d*0.9 elif min_slope > 0: d = max(0, (min_slope / max_up_slope)) r += int(d*128) a += d*0.9 slope = regression.slope intercept = regression.intercept min_x = regression.min_x max_x = regression.max_x print '' print '' print '' #output the labels print ' ' % qa(font_size) print '' print ' ' print '' if __name__ == "__main__": main()
普通文本  |  955行  |  34.76 KB

'''
Created on May 16, 2011

@author: bungeman
'''
import sys
import getopt
import re
import os
import bench_util
import json
import xml.sax.saxutils

# We throw out any measurement outside this range, and log a warning.
MIN_REASONABLE_TIME = 0
MAX_REASONABLE_TIME = 99999

# Constants for prefixes in output title used in buildbot.
TITLE_PREAMBLE = 'Bench_Performance_for_Skia_'
TITLE_PREAMBLE_LENGTH = len(TITLE_PREAMBLE)

def usage():
    """Prints simple usage information."""
    
    print '-b <bench> the bench to show.'
    print '-c <config> the config to show (GPU, 8888, 565, etc).'
    print '-d <dir> a directory containing bench_r<revision>_<scalar> files.'
    print '-e <file> file containing expected bench values/ranges.'
    print '   Will raise exception if actual bench values are out of range.'
    print '   See bench_expectations.txt for data format and examples.'
    print '-f <revision>[:<revision>] the revisions to use for fitting.'
    print '   Negative <revision> is taken as offset from most recent revision.'
    print '-i <time> the time to ignore (w, c, g, etc).'
    print '   The flag is ignored when -t is set; otherwise we plot all the'
    print '   times except the one specified here.'
    print '-l <title> title to use for the output graph'
    print '-m <representation> representation of bench value.'
    print '   See _ListAlgorithm class in bench_util.py.'
    print '-o <path> path to which to write output; writes to stdout if not specified'
    print '-r <revision>[:<revision>] the revisions to show.'
    print '   Negative <revision> is taken as offset from most recent revision.'
    print '-s <setting>[=<value>] a setting to show (alpha, scalar, etc).'
    print '-t <time> the time to show (w, c, g, etc).'
    print '-x <int> the desired width of the svg.'
    print '-y <int> the desired height of the svg.'
    print '--default-setting <setting>[=<value>] setting for those without.'
    

class Label:
    """The information in a label.
    
    (str, str, str, str, {str:str})"""
    def __init__(self, bench, config, time_type, settings):
        self.bench = bench
        self.config = config
        self.time_type = time_type
        self.settings = settings
    
    def __repr__(self):
        return "Label(%s, %s, %s, %s)" % (
                   str(self.bench),
                   str(self.config),
                   str(self.time_type),
                   str(self.settings),
               )
    
    def __str__(self):
        return "%s_%s_%s_%s" % (
                   str(self.bench),
                   str(self.config),
                   str(self.time_type),
                   str(self.settings),
               )
    
    def __eq__(self, other):
        return (self.bench == other.bench and
                self.config == other.config and
                self.time_type == other.time_type and
                self.settings == other.settings)
    
    def __hash__(self):
        return (hash(self.bench) ^
                hash(self.config) ^
                hash(self.time_type) ^
                hash(frozenset(self.settings.iteritems())))

def get_latest_revision(directory):
    """Returns the latest revision number found within this directory.
    """
    latest_revision_found = -1
    for bench_file in os.listdir(directory):
        file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
        if (file_name_match is None):
            continue
        revision = int(file_name_match.group(1))
        if revision > latest_revision_found:
            latest_revision_found = revision
    if latest_revision_found < 0:
        return None
    else:
        return latest_revision_found

def parse_dir(directory, default_settings, oldest_revision, newest_revision,
              rep):
    """Parses bench data from files like bench_r<revision>_<scalar>.
    
    (str, {str, str}, Number, Number) -> {int:[BenchDataPoints]}"""
    revision_data_points = {} # {revision : [BenchDataPoints]}
    for bench_file in os.listdir(directory):
        file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
        if (file_name_match is None):
            continue

        revision = int(file_name_match.group(1))
        scalar_type = file_name_match.group(2)

        if (revision < oldest_revision or revision > newest_revision):
            continue

        file_handle = open(directory + '/' + bench_file, 'r')

        if (revision not in revision_data_points):
            revision_data_points[revision] = []
        default_settings['scalar'] = scalar_type
        revision_data_points[revision].extend(
                        bench_util.parse(default_settings, file_handle, rep))
        file_handle.close()
    return revision_data_points

def add_to_revision_data_points(new_point, revision, revision_data_points):
    """Add new_point to set of revision_data_points we are building up.
    """
    if (revision not in revision_data_points):
        revision_data_points[revision] = []
    revision_data_points[revision].append(new_point)

def filter_data_points(unfiltered_revision_data_points):
    """Filter out any data points that are utterly bogus.

    Returns (allowed_revision_data_points, ignored_revision_data_points):
        allowed_revision_data_points: points that survived the filter
        ignored_revision_data_points: points that did NOT survive the filter
    """
    allowed_revision_data_points = {} # {revision : [BenchDataPoints]}
    ignored_revision_data_points = {} # {revision : [BenchDataPoints]}
    revisions = unfiltered_revision_data_points.keys()
    revisions.sort()
    for revision in revisions:
        for point in unfiltered_revision_data_points[revision]:
            if point.time < MIN_REASONABLE_TIME or point.time > MAX_REASONABLE_TIME:
                add_to_revision_data_points(point, revision, ignored_revision_data_points)
            else:
                add_to_revision_data_points(point, revision, allowed_revision_data_points)
    return (allowed_revision_data_points, ignored_revision_data_points)

def get_abs_path(relative_path):
    """My own implementation of os.path.abspath() that better handles paths
    which approach Window's 260-character limit.
    See https://code.google.com/p/skia/issues/detail?id=674

    This implementation adds path components one at a time, resolving the
    absolute path each time, to take advantage of any chdirs into outer
    directories that will shorten the total path length.

    TODO: share a single implementation with upload_to_bucket.py, instead
    of pasting this same code into both files."""
    if os.path.isabs(relative_path):
        return relative_path
    path_parts = relative_path.split(os.sep)
    abs_path = os.path.abspath('.')
    for path_part in path_parts:
        abs_path = os.path.abspath(os.path.join(abs_path, path_part))
    return abs_path

def redirect_stdout(output_path):
    """Redirect all following stdout to a file.

    You may be asking yourself, why redirect stdout within Python rather than
    redirecting the script's output in the calling shell?
    The answer lies in https://code.google.com/p/skia/issues/detail?id=674
    ('buildbot: windows GenerateBenchGraphs step fails due to filename length'):
    On Windows, we need to generate the absolute path within Python to avoid
    the operating system's 260-character pathname limit, including chdirs."""
    abs_path = get_abs_path(output_path)
    sys.stdout = open(abs_path, 'w')

def create_lines(revision_data_points, settings
               , bench_of_interest, config_of_interest, time_of_interest
               , time_to_ignore):
    """Convert revision data into sorted line data.
    
    ({int:[BenchDataPoints]}, {str:str}, str?, str?, str?)
        -> {Label:[(x,y)] | [n].x <= [n+1].x}"""
    revisions = revision_data_points.keys()
    revisions.sort()
    lines = {} # {Label:[(x,y)] | x[n] <= x[n+1]}
    for revision in revisions:
        for point in revision_data_points[revision]:
            if (bench_of_interest is not None and
                not bench_of_interest == point.bench):
                continue
            
            if (config_of_interest is not None and
                not config_of_interest == point.config):
                continue
            
            if (time_of_interest is not None and
                not time_of_interest == point.time_type):
                continue
            elif (time_to_ignore is not None and
                  time_to_ignore == point.time_type):
                continue
            
            skip = False
            for key, value in settings.items():
                if key in point.settings and point.settings[key] != value:
                    skip = True
                    break
            if skip:
                continue
            
            line_name = Label(point.bench
                            , point.config
                            , point.time_type
                            , point.settings)
            
            if line_name not in lines:
                lines[line_name] = []
            
            lines[line_name].append((revision, point.time))
            
    return lines

def bounds(lines):
    """Finds the bounding rectangle for the lines.
    
    {Label:[(x,y)]} -> ((min_x, min_y),(max_x,max_y))"""
    min_x = bench_util.Max
    min_y = bench_util.Max
    max_x = bench_util.Min
    max_y = bench_util.Min
    
    for line in lines.itervalues():
        for x, y in line:
            min_x = min(min_x, x)
            min_y = min(min_y, y)
            max_x = max(max_x, x)
            max_y = max(max_y, y)
            
    return ((min_x, min_y), (max_x, max_y))

def create_regressions(lines, start_x, end_x):
    """Creates regression data from line segments.
    
    ({Label:[(x,y)] | [n].x <= [n+1].x}, Number, Number)
        -> {Label:LinearRegression}"""
    regressions = {} # {Label : LinearRegression}
    
    for label, line in lines.iteritems():
        regression_line = [p for p in line if start_x <= p[0] <= end_x]
        
        if (len(regression_line) < 2):
            continue
        regression = bench_util.LinearRegression(regression_line)
        regressions[label] = regression
    
    return regressions

def bounds_slope(regressions):
    """Finds the extreme up and down slopes of a set of linear regressions.
    
    ({Label:LinearRegression}) -> (max_up_slope, min_down_slope)"""
    max_up_slope = 0
    min_down_slope = 0
    for regression in regressions.itervalues():
        min_slope = regression.find_min_slope()
        max_up_slope = max(max_up_slope, min_slope)
        min_down_slope = min(min_down_slope, min_slope)
    
    return (max_up_slope, min_down_slope)

def main():
    """Parses command line and writes output."""
    
    try:
        opts, _ = getopt.getopt(sys.argv[1:]
                                 , "b:c:d:e:f:i:l:m:o:r:s:t:x:y:"
                                 , "default-setting=")
    except getopt.GetoptError, err:
        print str(err) 
        usage()
        sys.exit(2)
    
    directory = None
    config_of_interest = None
    bench_of_interest = None
    time_of_interest = None
    time_to_ignore = None
    bench_expectations = {}
    rep = None  # bench representation algorithm
    revision_range = '0:'
    regression_range = '0:'
    latest_revision = None
    requested_height = None
    requested_width = None
    title = 'Bench graph'
    settings = {}
    default_settings = {}

    def parse_range(range):
        """Takes '<old>[:<new>]' as a string and returns (old, new).
        Any revision numbers that are dependent on the latest revision number
        will be filled in based on latest_revision.
        """
        old, _, new = range.partition(":")
        old = int(old)
        if old < 0:
            old += latest_revision;
        if not new:
            new = latest_revision;
        new = int(new)
        if new < 0:
            new += latest_revision;
        return (old, new)

    def add_setting(settings, setting):
        """Takes <key>[=<value>] adds {key:value} or {key:True} to settings."""
        name, _, value = setting.partition('=')
        if not value:
            settings[name] = True
        else:
            settings[name] = value

    def read_expectations(expectations, filename):
        """Reads expectations data from file and put in expectations dict."""
        for expectation in open(filename).readlines():
            elements = expectation.strip().split(',')
            if not elements[0] or elements[0].startswith('#'):
                continue
            if len(elements) != 5:
                raise Exception("Invalid expectation line format: %s" %
                                expectation)
            bench_entry = elements[0] + ',' + elements[1]
            if bench_entry in expectations:
                raise Exception("Dup entries for bench expectation %s" %
                                bench_entry)
            # [<Bench_BmpConfig_TimeType>,<Platform-Alg>] -> (LB, UB)
            expectations[bench_entry] = (float(elements[-2]),
                                         float(elements[-1]))

    def check_expectations(lines, expectations, newest_revision, key_suffix):
        """Check if there are benches in latest rev outside expected range."""
        exceptions = []
        for line in lines:
            line_str = str(line)
            bench_platform_key = (line_str[ : line_str.find('_{')] + ',' +
                key_suffix)
            this_revision, this_bench_value = lines[line][-1]
            if (this_revision != newest_revision or
                bench_platform_key not in expectations):
                # Skip benches without value for latest revision.
                continue
            this_min, this_max = expectations[bench_platform_key]
            if this_bench_value < this_min or this_bench_value > this_max:
                exceptions.append('Bench %s value %s out of range [%s, %s].' %
                    (bench_platform_key, this_bench_value, this_min, this_max))
        if exceptions:
            raise Exception('Bench values out of range:\n' +
                            '\n'.join(exceptions))

    try:
        for option, value in opts:
            if option == "-b":
                bench_of_interest = value
            elif option == "-c":
                config_of_interest = value
            elif option == "-d":
                directory = value
            elif option == "-e":
                read_expectations(bench_expectations, value)
            elif option == "-f":
                regression_range = value
            elif option == "-i":
                time_to_ignore = value
            elif option == "-l":
                title = value
            elif option == "-m":
                rep = value
            elif option == "-o":
                redirect_stdout(value)
            elif option == "-r":
                revision_range = value
            elif option == "-s":
                add_setting(settings, value)
            elif option == "-t":
                time_of_interest = value
            elif option == "-x":
                requested_width = int(value)
            elif option == "-y":
                requested_height = int(value)
            elif option == "--default-setting":
                add_setting(default_settings, value)
            else:
                usage()
                assert False, "unhandled option"
    except ValueError:
        usage()
        sys.exit(2)

    if directory is None:
        usage()
        sys.exit(2)

    if time_of_interest:
        time_to_ignore = None

    # The title flag (-l) provided in buildbot slave is in the format
    # Bench_Performance_for_Skia_<platform>, and we want to extract <platform>
    # for use in platform_and_alg to track matching benches later. If title flag
    # is not in this format, there may be no matching benches in the file
    # provided by the expectation_file flag (-e).
    platform_and_alg = title
    if platform_and_alg.startswith(TITLE_PREAMBLE):
        platform_and_alg = (
            platform_and_alg[TITLE_PREAMBLE_LENGTH:] + '-' + rep)
    title += ' [representation: %s]' % rep

    latest_revision = get_latest_revision(directory)
    oldest_revision, newest_revision = parse_range(revision_range)
    oldest_regression, newest_regression = parse_range(regression_range)

    unfiltered_revision_data_points = parse_dir(directory
                                   , default_settings
                                   , oldest_revision
                                   , newest_revision
                                   , rep)

    # Filter out any data points that are utterly bogus... make sure to report
    # that we did so later!
    (allowed_revision_data_points, ignored_revision_data_points) = filter_data_points(
        unfiltered_revision_data_points)

    # Update oldest_revision and newest_revision based on the data we could find
    all_revision_numbers = allowed_revision_data_points.keys()
    oldest_revision = min(all_revision_numbers)
    newest_revision = max(all_revision_numbers)

    lines = create_lines(allowed_revision_data_points
                   , settings
                   , bench_of_interest
                   , config_of_interest
                   , time_of_interest
                   , time_to_ignore)

    regressions = create_regressions(lines
                                   , oldest_regression
                                   , newest_regression)

    output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points,
                 regressions, requested_width, requested_height, title)

    check_expectations(lines, bench_expectations, newest_revision,
                       platform_and_alg)

def qa(out):
    """Stringify input and quote as an xml attribute."""
    return xml.sax.saxutils.quoteattr(str(out))
def qe(out):
    """Stringify input and escape as xml data."""
    return xml.sax.saxutils.escape(str(out))

def create_select(qualifier, lines, select_id=None):
    """Output select with options showing lines which qualifier maps to it.
    
    ((Label) -> str, {Label:_}, str?) -> _"""
    options = {} #{ option : [Label]}
    for label in lines.keys():
        option = qualifier(label)
        if (option not in options):
            options[option] = []
        options[option].append(label)
    option_list = list(options.keys())
    option_list.sort()
    print '<select class="lines"',
    if select_id is not None:
        print 'id=%s' % qa(select_id)
    print 'multiple="true" size="10" onchange="updateSvg();">'
    for option in option_list:
        print '<option value=' + qa('[' + 
        reduce(lambda x,y:x+json.dumps(str(y))+',',options[option],"")[0:-1]
        + ']') + '>'+qe(option)+'</option>'
    print '</select>'

def output_ignored_data_points_warning(ignored_revision_data_points):
    """Write description of ignored_revision_data_points to stdout as xhtml.
    """
    num_ignored_points = 0
    description = ''
    revisions = ignored_revision_data_points.keys()
    if revisions:
        revisions.sort()
        revisions.reverse()
        for revision in revisions:
            num_ignored_points += len(ignored_revision_data_points[revision])
            points_at_this_revision = []
            for point in ignored_revision_data_points[revision]:
                points_at_this_revision.append(point.bench)
            points_at_this_revision.sort()
            description += 'r%d: %s\n' % (revision, points_at_this_revision)
    if num_ignored_points == 0:
        print 'Did not discard any data points; all were within the range [%d-%d]' % (
            MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
    else:
        print '<table width="100%" bgcolor="ff0000"><tr><td align="center">'
        print 'Discarded %d data points outside of range [%d-%d]' % (
            num_ignored_points, MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
        print '</td></tr><tr><td width="100%" align="center">'
        print ('<textarea rows="4" style="width:97%" readonly="true" wrap="off">'
            + qe(description) + '</textarea>')
        print '</td></tr></table>'

def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points,
                 regressions, requested_width, requested_height, title):
    """Outputs an svg/xhtml view of the data."""
    print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"',
    print '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
    print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">'
    print '<head>'
    print '<title>%s</title>' % qe(title)
    print '</head>'
    print '<body>'
    
    output_svg(lines, regressions, requested_width, requested_height)

    #output the manipulation controls
    print """
<script type="text/javascript">//<![CDATA[
    function getElementsByClass(node, searchClass, tag) {
        var classElements = new Array();
        var elements = node.getElementsByTagName(tag);
        var pattern = new RegExp("^|\\s"+searchClass+"\\s|$");
        for (var i = 0, elementsFound = 0; i < elements.length; ++i) {
            if (pattern.test(elements[i].className)) {
                classElements[elementsFound] = elements[i];
                ++elementsFound;
            }
        }
        return classElements;
    }
    function getAllLines() {
        var selectElem = document.getElementById('benchSelect');
        var linesObj = {};
        for (var i = 0; i < selectElem.options.length; ++i) {
            var lines = JSON.parse(selectElem.options[i].value);
            for (var j = 0; j < lines.length; ++j) {
                linesObj[lines[j]] = true;
            }
        }
        return linesObj;
    }
    function getOptions(selectElem) {
        var linesSelectedObj = {};
        for (var i = 0; i < selectElem.options.length; ++i) {
            if (!selectElem.options[i].selected) continue;
            
            var linesSelected = JSON.parse(selectElem.options[i].value);
            for (var j = 0; j < linesSelected.length; ++j) {
                linesSelectedObj[linesSelected[j]] = true;
            }
        }
        return linesSelectedObj;
    }
    function objectEmpty(obj) {
        for (var p in obj) {
            return false;
        }
        return true;
    }
    function markSelectedLines(selectElem, allLines) {
        var linesSelected = getOptions(selectElem);
        if (!objectEmpty(linesSelected)) {
            for (var line in allLines) {
                allLines[line] &= (linesSelected[line] == true);
            }
        }
    }
    function updateSvg() {
        var allLines = getAllLines();
        
        var selects = getElementsByClass(document, 'lines', 'select');
        for (var i = 0; i < selects.length; ++i) {
            markSelectedLines(selects[i], allLines);
        }
        
        for (var line in allLines) {
            var svgLine = document.getElementById(line);
            var display = (allLines[line] ? 'inline' : 'none');
            svgLine.setAttributeNS(null,'display', display);
        }
    }
    
    function mark(markerId) {
        for (var line in getAllLines()) {
            var svgLineGroup = document.getElementById(line);
            var display = svgLineGroup.getAttributeNS(null,'display');
            if (display == null || display == "" || display != "none") {
                var svgLine = document.getElementById(line+'_line');
                if (markerId == null) {
                    svgLine.removeAttributeNS(null,'marker-mid');
                } else {
                    svgLine.setAttributeNS(null,'marker-mid', markerId);
                }
            }
        }
    }
//]]></script>"""

    all_settings = {}
    variant_settings = set()
    for label in lines.keys():
        for key, value  in label.settings.items():
            if key not in all_settings:
                all_settings[key] = value
            elif all_settings[key] != value:
                variant_settings.add(key)

    print '<table border="0" width="%s">' % requested_width
    #output column headers
    print """
<tr valign="top"><td width="50%">
<table border="0" width="100%">
<tr><td align="center"><table border="0">
<form>
<tr valign="bottom" align="center">
<td width="1">Bench&nbsp;Type</td>
<td width="1">Bitmap Config</td>
<td width="1">Timer&nbsp;Type (Cpu/Gpu/wall)</td>
"""

    for k in variant_settings:
        print '<td width="1">%s</td>' % qe(k)

    print '<td width="1"><!--buttons--></td></tr>'

    #output column contents
    print '<tr valign="top" align="center">'
    print '<td width="1">'
    create_select(lambda l: l.bench, lines, 'benchSelect')
    print '</td><td width="1">'
    create_select(lambda l: l.config, lines)
    print '</td><td width="1">'
    create_select(lambda l: l.time_type, lines)

    for k in variant_settings:
        print '</td><td width="1">'
        create_select(lambda l: l.settings.get(k, " "), lines)

    print '</td><td width="1"><button type="button"',
    print 'onclick=%s' % qa("mark('url(#circleMark)'); return false;"),
    print '>Mark Points</button>'
    print '<button type="button" onclick="mark(null);">Clear Points</button>'
    print '</td>'
    print """
</tr>
</form>
</table></td></tr>
<tr><td align="center">
<hr />
"""

    output_ignored_data_points_warning(ignored_revision_data_points)
    print '</td></tr></table>'
    print '</td><td width="2%"><!--gutter--></td>'

    print '<td><table border="0">'
    print '<tr><td align="center">%s<br></br>revisions r%s - r%s</td></tr>' % (
        qe(title),
        bench_util.CreateRevisionLink(oldest_revision),
        bench_util.CreateRevisionLink(newest_revision))
    print """
<tr><td align="left">
<p>Brighter red indicates tests that have gotten worse; brighter green
indicates tests that have gotten better.</p>
<p>To highlight individual tests, hold down CONTROL and mouse over
graph lines.</p>
<p>To highlight revision numbers, hold down SHIFT and mouse over
the graph area.</p>
<p>To only show certain tests on the graph, select any combination of
tests in the selectors at left.  (To show all, select all.)</p>
<p>Use buttons at left to mark/clear points on the lines for selected
benchmarks.</p>
</td></tr>
</table>

</td>
</tr>
</table>
</body>
</html>"""
    
def compute_size(requested_width, requested_height, rev_width, time_height):
    """Converts potentially empty requested size into a concrete size.
    
    (Number?,  Number?) -> (Number, Number)"""
    pic_width = 0
    pic_height = 0
    if (requested_width is not None and requested_height is not None):
        pic_height = requested_height
        pic_width = requested_width
    
    elif (requested_width is not None):
        pic_width = requested_width
        pic_height = pic_width * (float(time_height) / rev_width)
        
    elif (requested_height is not None):
        pic_height = requested_height
        pic_width = pic_height * (float(rev_width) / time_height)
        
    else:
        pic_height = 800
        pic_width = max(rev_width*3
                      , pic_height * (float(rev_width) / time_height))
    
    return (pic_width, pic_height)

def output_svg(lines, regressions, requested_width, requested_height):
    """Outputs an svg view of the data."""
    
    (global_min_x, _), (global_max_x, global_max_y) = bounds(lines)
    max_up_slope, min_down_slope = bounds_slope(regressions)
    
    #output
    global_min_y = 0
    x = global_min_x
    y = global_min_y
    w = global_max_x - global_min_x
    h = global_max_y - global_min_y
    font_size = 16
    line_width = 2
    
    pic_width, pic_height = compute_size(requested_width, requested_height
                                       , w, h)
    
    def cw(w1):
        """Converts a revision difference to display width."""
        return (pic_width / float(w)) * w1
    def cx(x):
        """Converts a revision to a horizontal display position."""
        return cw(x - global_min_x)

    def ch(h1):
        """Converts a time difference to a display height."""
        return -(pic_height / float(h)) * h1
    def cy(y):
        """Converts a time to a vertical display position."""
        return pic_height + ch(y - global_min_y)
    
    print '<!--Picture height %.2f corresponds to bench value %.2f.-->' % (
        pic_height, h)
    print '<svg',
    print 'width=%s' % qa(str(pic_width)+'px')
    print 'height=%s' % qa(str(pic_height)+'px')
    print 'viewBox="0 0 %s %s"' % (str(pic_width), str(pic_height))
    print 'onclick=%s' % qa(
            "var event = arguments[0] || window.event;"
            " if (event.shiftKey) { highlightRevision(null); }"
            " if (event.ctrlKey) { highlight(null); }"
            " return false;")
    print 'xmlns="http://www.w3.org/2000/svg"'
    print 'xmlns:xlink="http://www.w3.org/1999/xlink">'
    
    print """
<defs>
    <marker id="circleMark"
      viewBox="0 0 2 2" refX="1" refY="1"
      markerUnits="strokeWidth"
      markerWidth="2" markerHeight="2"
      orient="0">
      <circle cx="1" cy="1" r="1"/>
    </marker>
</defs>"""
    
    #output the revisions
    print """
<script type="text/javascript">//<![CDATA[
    var previousRevision;
    var previousRevisionFill;
    var previousRevisionStroke
    function highlightRevision(id) {
        if (previousRevision == id) return;

        document.getElementById('revision').firstChild.nodeValue = 'r' + id;
        document.getElementById('rev_link').setAttribute('xlink:href',
            'http://code.google.com/p/skia/source/detail?r=' + id);
        
        var preRevision = document.getElementById(previousRevision);
        if (preRevision) {
            preRevision.setAttributeNS(null,'fill', previousRevisionFill);
            preRevision.setAttributeNS(null,'stroke', previousRevisionStroke);
        }
        
        var revision = document.getElementById(id);
        previousRevision = id;
        if (revision) {
            previousRevisionFill = revision.getAttributeNS(null,'fill');
            revision.setAttributeNS(null,'fill','rgb(100%, 95%, 95%)');
            
            previousRevisionStroke = revision.getAttributeNS(null,'stroke');
            revision.setAttributeNS(null,'stroke','rgb(100%, 90%, 90%)');
        }
    }
//]]></script>"""
    
    def print_rect(x, y, w, h, revision):
        """Outputs a revision rectangle in display space,
           taking arguments in revision space."""
        disp_y = cy(y)
        disp_h = ch(h)
        if disp_h < 0:
            disp_y += disp_h
            disp_h = -disp_h
        
        print '<rect id=%s x=%s y=%s' % (qa(revision), qa(cx(x)), qa(disp_y),),
        print 'width=%s height=%s' % (qa(cw(w)), qa(disp_h),),
        print 'fill="white"',
        print 'stroke="rgb(98%%,98%%,88%%)" stroke-width=%s' % qa(line_width),
        print 'onmouseover=%s' % qa(
                "var event = arguments[0] || window.event;"
                " if (event.shiftKey) {"
                    " highlightRevision('"+str(revision)+"');"
                    " return false;"
                " }"),
        print ' />'
    
    xes = set()
    for line in lines.itervalues():
        for point in line:
            xes.add(point[0])
    revisions = list(xes)
    revisions.sort()
    
    left = x
    current_revision = revisions[0]
    for next_revision in revisions[1:]:
        width = (((next_revision - current_revision) / 2.0)
                 + (current_revision - left))
        print_rect(left, y, width, h, current_revision)
        left += width
        current_revision = next_revision
    print_rect(left, y, x+w - left, h, current_revision)

    #output the lines
    print """
<script type="text/javascript">//<![CDATA[
    var previous;
    var previousColor;
    var previousOpacity;
    function highlight(id) {
        if (previous == id) return;

        document.getElementById('label').firstChild.nodeValue = id;

        var preGroup = document.getElementById(previous);
        if (preGroup) {
            var preLine = document.getElementById(previous+'_line');
            preLine.setAttributeNS(null,'stroke', previousColor);
            preLine.setAttributeNS(null,'opacity', previousOpacity);

            var preSlope = document.getElementById(previous+'_linear');
            if (preSlope) {
                preSlope.setAttributeNS(null,'visibility', 'hidden');
            }
        }

        var group = document.getElementById(id);
        previous = id;
        if (group) {
            group.parentNode.appendChild(group);
            
            var line = document.getElementById(id+'_line');
            previousColor = line.getAttributeNS(null,'stroke');
            previousOpacity = line.getAttributeNS(null,'opacity');
            line.setAttributeNS(null,'stroke', 'blue');
            line.setAttributeNS(null,'opacity', '1');
            
            var slope = document.getElementById(id+'_linear');
            if (slope) {
                slope.setAttributeNS(null,'visibility', 'visible');
            }
        }
    }
//]]></script>"""
    for label, line in lines.items():
        print '<g id=%s>' % qa(label)
        r = 128
        g = 128
        b = 128
        a = .10
        if label in regressions:
            regression = regressions[label]
            min_slope = regression.find_min_slope()
            if min_slope < 0:
                d = max(0, (min_slope / min_down_slope))
                g += int(d*128)
                a += d*0.9
            elif min_slope > 0:
                d = max(0, (min_slope / max_up_slope))
                r += int(d*128)
                a += d*0.9
            
            slope = regression.slope
            intercept = regression.intercept
            min_x = regression.min_x
            max_x = regression.max_x
            print '<polyline id=%s' % qa(str(label)+'_linear'),
            print 'fill="none" stroke="yellow"',
            print 'stroke-width=%s' % qa(abs(ch(regression.serror*2))),
            print 'opacity="0.5" pointer-events="none" visibility="hidden"',
            print 'points="',
            print '%s,%s' % (str(cx(min_x)), str(cy(slope*min_x + intercept))),
            print '%s,%s' % (str(cx(max_x)), str(cy(slope*max_x + intercept))),
            print '"/>'
        
        print '<polyline id=%s' % qa(str(label)+'_line'),
        print 'onmouseover=%s' % qa(
                "var event = arguments[0] || window.event;"
                " if (event.ctrlKey) {"
                    " highlight('"+str(label).replace("'", "\\'")+"');"
                    " return false;"
                " }"),
        print 'fill="none" stroke="rgb(%s,%s,%s)"' % (str(r), str(g), str(b)),
        print 'stroke-width=%s' % qa(line_width),
        print 'opacity=%s' % qa(a),
        print 'points="',
        for point in line:
            print '%s,%s' % (str(cx(point[0])), str(cy(point[1]))),
        print '"/>'

        print '</g>'

    #output the labels
    print '<text id="label" x="0" y=%s' % qa(font_size),
    print 'font-size=%s> </text>' % qa(font_size)

    print '<a id="rev_link" xlink:href="" target="_top">'
    print '<text id="revision" x="0" y=%s style="' % qa(font_size*2)
    print 'font-size: %s; ' % qe(font_size)
    print 'stroke: #0000dd; text-decoration: underline; '
    print '"> </text></a>'

    print '</svg>'

if __name__ == "__main__":
    main()