Home | History | Annotate | Download | only in bench
      1 '''
      2 Created on May 16, 2011
      3 
      4 @author: bungeman
      5 '''
      6 import bench_util
      7 import getopt
      8 import httplib
      9 import itertools
     10 import json
     11 import os
     12 import re
     13 import sys
     14 import urllib
     15 import urllib2
     16 import xml.sax.saxutils
     17 
     18 # We throw out any measurement outside this range, and log a warning.
     19 MIN_REASONABLE_TIME = 0
     20 MAX_REASONABLE_TIME = 99999
     21 
     22 # Constants for prefixes in output title used in buildbot.
     23 TITLE_PREAMBLE = 'Bench_Performance_for_'
     24 TITLE_PREAMBLE_LENGTH = len(TITLE_PREAMBLE)
     25 
     26 def usage():
     27     """Prints simple usage information."""
     28 
     29     print '-a <url> the url to use for adding bench values to app engine app.'
     30     print '   Example: "https://skiadash.appspot.com/add_point".'
     31     print '   If not set, will skip this step.'
     32     print '-b <bench> the bench to show.'
     33     print '-c <config> the config to show (GPU, 8888, 565, etc).'
     34     print '-d <dir> a directory containing bench_r<revision>_<scalar> files.'
     35     print '-e <file> file containing expected bench values/ranges.'
     36     print '   Will raise exception if actual bench values are out of range.'
     37     print '   See bench_expectations.txt for data format and examples.'
     38     print '-f <revision>[:<revision>] the revisions to use for fitting.'
     39     print '   Negative <revision> is taken as offset from most recent revision.'
     40     print '-i <time> the time to ignore (w, c, g, etc).'
     41     print '   The flag is ignored when -t is set; otherwise we plot all the'
     42     print '   times except the one specified here.'
     43     print '-l <title> title to use for the output graph'
     44     print '-m <representation> representation of bench value.'
     45     print '   See _ListAlgorithm class in bench_util.py.'
     46     print '-o <path> path to which to write output.'
     47     print '-r <revision>[:<revision>] the revisions to show.'
     48     print '   Negative <revision> is taken as offset from most recent revision.'
     49     print '-s <setting>[=<value>] a setting to show (alpha, scalar, etc).'
     50     print '-t <time> the time to show (w, c, g, etc).'
     51     print '-x <int> the desired width of the svg.'
     52     print '-y <int> the desired height of the svg.'
     53     print '--default-setting <setting>[=<value>] setting for those without.'
     54     
     55 
     56 class Label:
     57     """The information in a label.
     58     
     59     (str, str, str, str, {str:str})"""
     60     def __init__(self, bench, config, time_type, settings):
     61         self.bench = bench
     62         self.config = config
     63         self.time_type = time_type
     64         self.settings = settings
     65     
     66     def __repr__(self):
     67         return "Label(%s, %s, %s, %s)" % (
     68                    str(self.bench),
     69                    str(self.config),
     70                    str(self.time_type),
     71                    str(self.settings),
     72                )
     73     
     74     def __str__(self):
     75         return "%s_%s_%s_%s" % (
     76                    str(self.bench),
     77                    str(self.config),
     78                    str(self.time_type),
     79                    str(self.settings),
     80                )
     81     
     82     def __eq__(self, other):
     83         return (self.bench == other.bench and
     84                 self.config == other.config and
     85                 self.time_type == other.time_type and
     86                 self.settings == other.settings)
     87     
     88     def __hash__(self):
     89         return (hash(self.bench) ^
     90                 hash(self.config) ^
     91                 hash(self.time_type) ^
     92                 hash(frozenset(self.settings.iteritems())))
     93 
     94 def get_latest_revision(directory):
     95     """Returns the latest revision number found within this directory.
     96     """
     97     latest_revision_found = -1
     98     for bench_file in os.listdir(directory):
     99         file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
    100         if (file_name_match is None):
    101             continue
    102         revision = int(file_name_match.group(1))
    103         if revision > latest_revision_found:
    104             latest_revision_found = revision
    105     if latest_revision_found < 0:
    106         return None
    107     else:
    108         return latest_revision_found
    109 
    110 def parse_dir(directory, default_settings, oldest_revision, newest_revision,
    111               rep):
    112     """Parses bench data from files like bench_r<revision>_<scalar>.
    113     
    114     (str, {str, str}, Number, Number) -> {int:[BenchDataPoints]}"""
    115     revision_data_points = {} # {revision : [BenchDataPoints]}
    116     file_list = os.listdir(directory)
    117     file_list.sort()
    118     for bench_file in file_list:
    119         file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file)
    120         if (file_name_match is None):
    121             continue
    122 
    123         revision = int(file_name_match.group(1))
    124         scalar_type = file_name_match.group(2)
    125 
    126         if (revision < oldest_revision or revision > newest_revision):
    127             continue
    128 
    129         file_handle = open(directory + '/' + bench_file, 'r')
    130 
    131         if (revision not in revision_data_points):
    132             revision_data_points[revision] = []
    133         default_settings['scalar'] = scalar_type
    134         revision_data_points[revision].extend(
    135                         bench_util.parse(default_settings, file_handle, rep))
    136         file_handle.close()
    137     return revision_data_points
    138 
    139 def add_to_revision_data_points(new_point, revision, revision_data_points):
    140     """Add new_point to set of revision_data_points we are building up.
    141     """
    142     if (revision not in revision_data_points):
    143         revision_data_points[revision] = []
    144     revision_data_points[revision].append(new_point)
    145 
    146 def filter_data_points(unfiltered_revision_data_points):
    147     """Filter out any data points that are utterly bogus.
    148 
    149     Returns (allowed_revision_data_points, ignored_revision_data_points):
    150         allowed_revision_data_points: points that survived the filter
    151         ignored_revision_data_points: points that did NOT survive the filter
    152     """
    153     allowed_revision_data_points = {} # {revision : [BenchDataPoints]}
    154     ignored_revision_data_points = {} # {revision : [BenchDataPoints]}
    155     revisions = unfiltered_revision_data_points.keys()
    156     revisions.sort()
    157     for revision in revisions:
    158         for point in unfiltered_revision_data_points[revision]:
    159             if point.time < MIN_REASONABLE_TIME or point.time > MAX_REASONABLE_TIME:
    160                 add_to_revision_data_points(point, revision, ignored_revision_data_points)
    161             else:
    162                 add_to_revision_data_points(point, revision, allowed_revision_data_points)
    163     return (allowed_revision_data_points, ignored_revision_data_points)
    164 
    165 def get_abs_path(relative_path):
    166     """My own implementation of os.path.abspath() that better handles paths
    167     which approach Window's 260-character limit.
    168     See https://code.google.com/p/skia/issues/detail?id=674
    169 
    170     This implementation adds path components one at a time, resolving the
    171     absolute path each time, to take advantage of any chdirs into outer
    172     directories that will shorten the total path length.
    173 
    174     TODO: share a single implementation with upload_to_bucket.py, instead
    175     of pasting this same code into both files."""
    176     if os.path.isabs(relative_path):
    177         return relative_path
    178     path_parts = relative_path.split(os.sep)
    179     abs_path = os.path.abspath('.')
    180     for path_part in path_parts:
    181         abs_path = os.path.abspath(os.path.join(abs_path, path_part))
    182     return abs_path
    183 
    184 def redirect_stdout(output_path):
    185     """Redirect all following stdout to a file.
    186 
    187     You may be asking yourself, why redirect stdout within Python rather than
    188     redirecting the script's output in the calling shell?
    189     The answer lies in https://code.google.com/p/skia/issues/detail?id=674
    190     ('buildbot: windows GenerateBenchGraphs step fails due to filename length'):
    191     On Windows, we need to generate the absolute path within Python to avoid
    192     the operating system's 260-character pathname limit, including chdirs."""
    193     abs_path = get_abs_path(output_path)
    194     sys.stdout = open(abs_path, 'w')
    195 
    196 def create_lines(revision_data_points, settings
    197                , bench_of_interest, config_of_interest, time_of_interest
    198                , time_to_ignore):
    199     """Convert revision data into a dictionary of line data.
    200     
    201     Args:
    202       revision_data_points: a dictionary with integer keys (revision #) and a
    203           list of bench data points as values
    204       settings: a dictionary of setting names to value
    205       bench_of_interest: optional filter parameters: which bench type is of
    206           interest. If None, process them all.
    207       config_of_interest: optional filter parameters: which config type is of
    208           interest. If None, process them all.
    209       time_of_interest: optional filter parameters: which timer type is of
    210           interest. If None, process them all.
    211       time_to_ignore: optional timer type to ignore
    212 
    213     Returns:
    214       a dictionary of this form:
    215           keys = Label objects
    216           values = a list of (x, y) tuples sorted such that x values increase
    217               monotonically
    218     """
    219     revisions = revision_data_points.keys()
    220     revisions.sort()
    221     lines = {} # {Label:[(x,y)] | x[n] <= x[n+1]}
    222     for revision in revisions:
    223         for point in revision_data_points[revision]:
    224             if (bench_of_interest is not None and
    225                 not bench_of_interest == point.bench):
    226                 continue
    227             
    228             if (config_of_interest is not None and
    229                 not config_of_interest == point.config):
    230                 continue
    231             
    232             if (time_of_interest is not None and
    233                 not time_of_interest == point.time_type):
    234                 continue
    235             elif (time_to_ignore is not None and
    236                   time_to_ignore == point.time_type):
    237                 continue
    238             
    239             skip = False
    240             for key, value in settings.items():
    241                 if key in point.settings and point.settings[key] != value:
    242                     skip = True
    243                     break
    244             if skip:
    245                 continue
    246             
    247             line_name = Label(point.bench
    248                             , point.config
    249                             , point.time_type
    250                             , point.settings)
    251             
    252             if line_name not in lines:
    253                 lines[line_name] = []
    254             
    255             lines[line_name].append((revision, point.time))
    256             
    257     return lines
    258 
    259 def bounds(lines):
    260     """Finds the bounding rectangle for the lines.
    261     
    262     {Label:[(x,y)]} -> ((min_x, min_y),(max_x,max_y))"""
    263     min_x = bench_util.Max
    264     min_y = bench_util.Max
    265     max_x = bench_util.Min
    266     max_y = bench_util.Min
    267     
    268     for line in lines.itervalues():
    269         for x, y in line:
    270             min_x = min(min_x, x)
    271             min_y = min(min_y, y)
    272             max_x = max(max_x, x)
    273             max_y = max(max_y, y)
    274             
    275     return ((min_x, min_y), (max_x, max_y))
    276 
    277 def create_regressions(lines, start_x, end_x):
    278     """Creates regression data from line segments.
    279     
    280     ({Label:[(x,y)] | [n].x <= [n+1].x}, Number, Number)
    281         -> {Label:LinearRegression}"""
    282     regressions = {} # {Label : LinearRegression}
    283     
    284     for label, line in lines.iteritems():
    285         regression_line = [p for p in line if start_x <= p[0] <= end_x]
    286         
    287         if (len(regression_line) < 2):
    288             continue
    289         regression = bench_util.LinearRegression(regression_line)
    290         regressions[label] = regression
    291     
    292     return regressions
    293 
    294 def bounds_slope(regressions):
    295     """Finds the extreme up and down slopes of a set of linear regressions.
    296     
    297     ({Label:LinearRegression}) -> (max_up_slope, min_down_slope)"""
    298     max_up_slope = 0
    299     min_down_slope = 0
    300     for regression in regressions.itervalues():
    301         min_slope = regression.find_min_slope()
    302         max_up_slope = max(max_up_slope, min_slope)
    303         min_down_slope = min(min_down_slope, min_slope)
    304     
    305     return (max_up_slope, min_down_slope)
    306 
    307 def main():
    308     """Parses command line and writes output."""
    309     
    310     try:
    311         opts, _ = getopt.getopt(sys.argv[1:]
    312                                  , "a:b:c:d:e:f:i:l:m:o:r:s:t:x:y:"
    313                                  , "default-setting=")
    314     except getopt.GetoptError, err:
    315         print str(err) 
    316         usage()
    317         sys.exit(2)
    318     
    319     directory = None
    320     config_of_interest = None
    321     bench_of_interest = None
    322     time_of_interest = None
    323     time_to_ignore = None
    324     output_path = None
    325     bench_expectations = {}
    326     appengine_url = None  # used for adding data to appengine datastore
    327     rep = None  # bench representation algorithm
    328     revision_range = '0:'
    329     regression_range = '0:'
    330     latest_revision = None
    331     requested_height = None
    332     requested_width = None
    333     title = 'Bench graph'
    334     settings = {}
    335     default_settings = {}
    336 
    337     def parse_range(range):
    338         """Takes '<old>[:<new>]' as a string and returns (old, new).
    339         Any revision numbers that are dependent on the latest revision number
    340         will be filled in based on latest_revision.
    341         """
    342         old, _, new = range.partition(":")
    343         old = int(old)
    344         if old < 0:
    345             old += latest_revision;
    346         if not new:
    347             new = latest_revision;
    348         new = int(new)
    349         if new < 0:
    350             new += latest_revision;
    351         return (old, new)
    352 
    353     def add_setting(settings, setting):
    354         """Takes <key>[=<value>] adds {key:value} or {key:True} to settings."""
    355         name, _, value = setting.partition('=')
    356         if not value:
    357             settings[name] = True
    358         else:
    359             settings[name] = value
    360 
    361     def read_expectations(expectations, filename):
    362         """Reads expectations data from file and put in expectations dict."""
    363         for expectation in open(filename).readlines():
    364             elements = expectation.strip().split(',')
    365             if not elements[0] or elements[0].startswith('#'):
    366                 continue
    367             if len(elements) != 5:
    368                 raise Exception("Invalid expectation line format: %s" %
    369                                 expectation)
    370             bench_entry = elements[0] + ',' + elements[1]
    371             if bench_entry in expectations:
    372                 raise Exception("Dup entries for bench expectation %s" %
    373                                 bench_entry)
    374             # [<Bench_BmpConfig_TimeType>,<Platform-Alg>] -> (LB, UB)
    375             expectations[bench_entry] = (float(elements[-2]),
    376                                          float(elements[-1]))
    377 
    378     def check_expectations(lines, expectations, newest_revision, key_suffix):
    379         """Check if there are benches in latest rev outside expected range.
    380         For exceptions, also outputs URL link for the dashboard plot.
    381         The link history token format here only works for single-line plots.
    382         """
    383         # The platform for this bot, to pass to the dashboard plot.
    384         platform = key_suffix[ : key_suffix.rfind('-')]
    385         # Starting revision for the dashboard plot.
    386         start_rev = str(newest_revision - 100)  # Displays about 100 revisions.
    387         exceptions = []
    388         for line in lines:
    389             line_str = str(line)
    390             line_str = line_str[ : line_str.find('_{')]
    391             bench_platform_key = line_str + ',' + key_suffix
    392             this_revision, this_bench_value = lines[line][-1]
    393             if (this_revision != newest_revision or
    394                 bench_platform_key not in expectations):
    395                 # Skip benches without value for latest revision.
    396                 continue
    397             this_min, this_max = expectations[bench_platform_key]
    398             if this_bench_value < this_min or this_bench_value > this_max:
    399                 link = ''
    400                 # For skp benches out of range, create dashboard plot link.
    401                 if line_str.find('.skp_') > 0:
    402                     # Extract bench and config for dashboard plot.
    403                     bench, config = line_str.strip('_').split('.skp_')
    404                     link = ' <a href="'
    405                     link += 'http://go/skpdash/SkpDash.html#%s~%s~%s~%s" ' % (
    406                         start_rev, bench, platform, config)
    407                     link += 'target="_blank">graph</a>'
    408                 exception = 'Bench %s value %s out of range [%s, %s].%s' % (
    409                     bench_platform_key, this_bench_value, this_min, this_max,
    410                     link)
    411                 exceptions.append(exception)
    412         if exceptions:
    413             raise Exception('Bench values out of range:\n' +
    414                             '\n'.join(exceptions))
    415 
    416     def write_to_appengine(line_data_dict, url, newest_revision, bot):
    417         """Writes latest bench values to appengine datastore.
    418           line_data_dict: dictionary from create_lines.
    419           url: the appengine url used to send bench values to write
    420           newest_revision: the latest revision that this script reads
    421           bot: the bot platform the bench is run on
    422         """
    423         config_data_dic = {}
    424         for label in line_data_dict.iterkeys():
    425             if not label.bench.endswith('.skp') or label.time_type:
    426                 # filter out non-picture and non-walltime benches
    427                 continue
    428             config = label.config
    429             rev, val = line_data_dict[label][-1]
    430             # This assumes that newest_revision is >= the revision of the last
    431             # data point we have for each line.
    432             if rev != newest_revision:
    433                 continue
    434             if config not in config_data_dic:
    435                 config_data_dic[config] = []
    436             config_data_dic[config].append(label.bench.replace('.skp', '') +
    437                 ':%.2f' % val)
    438         for config in config_data_dic:
    439             if config_data_dic[config]:
    440                 data = {'master': 'Skia', 'bot': bot, 'test': config,
    441                         'revision': newest_revision,
    442                         'benches': ','.join(config_data_dic[config])}
    443                 req = urllib2.Request(appengine_url,
    444                     urllib.urlencode({'data': json.dumps(data)}))
    445                 try:
    446                     urllib2.urlopen(req)
    447                 except urllib2.HTTPError, e:
    448                     sys.stderr.write("HTTPError for JSON data %s: %s\n" % (
    449                         data, e))
    450                 except urllib2.URLError, e:
    451                     sys.stderr.write("URLError for JSON data %s: %s\n" % (
    452                         data, e))
    453                 except httplib.HTTPException, e:
    454                     sys.stderr.write("HTTPException for JSON data %s: %s\n" % (
    455                         data, e))
    456 
    457     try:
    458         for option, value in opts:
    459             if option == "-a":
    460                 appengine_url = value
    461             elif option == "-b":
    462                 bench_of_interest = value
    463             elif option == "-c":
    464                 config_of_interest = value
    465             elif option == "-d":
    466                 directory = value
    467             elif option == "-e":
    468                 read_expectations(bench_expectations, value)
    469             elif option == "-f":
    470                 regression_range = value
    471             elif option == "-i":
    472                 time_to_ignore = value
    473             elif option == "-l":
    474                 title = value
    475             elif option == "-m":
    476                 rep = value
    477             elif option == "-o":
    478                 output_path = value
    479                 redirect_stdout(output_path)
    480             elif option == "-r":
    481                 revision_range = value
    482             elif option == "-s":
    483                 add_setting(settings, value)
    484             elif option == "-t":
    485                 time_of_interest = value
    486             elif option == "-x":
    487                 requested_width = int(value)
    488             elif option == "-y":
    489                 requested_height = int(value)
    490             elif option == "--default-setting":
    491                 add_setting(default_settings, value)
    492             else:
    493                 usage()
    494                 assert False, "unhandled option"
    495     except ValueError:
    496         usage()
    497         sys.exit(2)
    498 
    499     if directory is None:
    500         usage()
    501         sys.exit(2)
    502 
    503     if not output_path:
    504         print 'Warning: No output path provided. No graphs will be written.'
    505 
    506     if time_of_interest:
    507         time_to_ignore = None
    508 
    509     # The title flag (-l) provided in buildbot slave is in the format
    510     # Bench_Performance_for_<platform>, and we want to extract <platform>
    511     # for use in platform_and_alg to track matching benches later. If title flag
    512     # is not in this format, there may be no matching benches in the file
    513     # provided by the expectation_file flag (-e).
    514     bot = title  # To store the platform as bot name
    515     platform_and_alg = title
    516     if platform_and_alg.startswith(TITLE_PREAMBLE):
    517         bot = platform_and_alg[TITLE_PREAMBLE_LENGTH:]
    518         platform_and_alg = bot + '-' + rep
    519     title += ' [representation: %s]' % rep
    520 
    521     latest_revision = get_latest_revision(directory)
    522     oldest_revision, newest_revision = parse_range(revision_range)
    523     oldest_regression, newest_regression = parse_range(regression_range)
    524 
    525     unfiltered_revision_data_points = parse_dir(directory
    526                                    , default_settings
    527                                    , oldest_revision
    528                                    , newest_revision
    529                                    , rep)
    530 
    531     # Filter out any data points that are utterly bogus... make sure to report
    532     # that we did so later!
    533     (allowed_revision_data_points, ignored_revision_data_points) = filter_data_points(
    534         unfiltered_revision_data_points)
    535 
    536     # Update oldest_revision and newest_revision based on the data we could find
    537     all_revision_numbers = allowed_revision_data_points.keys()
    538     oldest_revision = min(all_revision_numbers)
    539     newest_revision = max(all_revision_numbers)
    540 
    541     lines = create_lines(allowed_revision_data_points
    542                    , settings
    543                    , bench_of_interest
    544                    , config_of_interest
    545                    , time_of_interest
    546                    , time_to_ignore)
    547 
    548     regressions = create_regressions(lines
    549                                    , oldest_regression
    550                                    , newest_regression)
    551 
    552     if output_path:
    553         output_xhtml(lines, oldest_revision, newest_revision,
    554                      ignored_revision_data_points, regressions, requested_width,
    555                      requested_height, title)
    556 
    557     if appengine_url:
    558         write_to_appengine(lines, appengine_url, newest_revision, bot)
    559 
    560     if bench_expectations:
    561         check_expectations(lines, bench_expectations, newest_revision,
    562                            platform_and_alg)
    563 
    564 def qa(out):
    565     """Stringify input and quote as an xml attribute."""
    566     return xml.sax.saxutils.quoteattr(str(out))
    567 def qe(out):
    568     """Stringify input and escape as xml data."""
    569     return xml.sax.saxutils.escape(str(out))
    570 
    571 def create_select(qualifier, lines, select_id=None):
    572     """Output select with options showing lines which qualifier maps to it.
    573     
    574     ((Label) -> str, {Label:_}, str?) -> _"""
    575     options = {} #{ option : [Label]}
    576     for label in lines.keys():
    577         option = qualifier(label)
    578         if (option not in options):
    579             options[option] = []
    580         options[option].append(label)
    581     option_list = list(options.keys())
    582     option_list.sort()
    583     print '<select class="lines"',
    584     if select_id is not None:
    585         print 'id=%s' % qa(select_id)
    586     print 'multiple="true" size="10" onchange="updateSvg();">'
    587     for option in option_list:
    588         print '<option value=' + qa('[' + 
    589         reduce(lambda x,y:x+json.dumps(str(y))+',',options[option],"")[0:-1]
    590         + ']') + '>'+qe(option)+'</option>'
    591     print '</select>'
    592 
    593 def output_ignored_data_points_warning(ignored_revision_data_points):
    594     """Write description of ignored_revision_data_points to stdout as xhtml.
    595     """
    596     num_ignored_points = 0
    597     description = ''
    598     revisions = ignored_revision_data_points.keys()
    599     if revisions:
    600         revisions.sort()
    601         revisions.reverse()
    602         for revision in revisions:
    603             num_ignored_points += len(ignored_revision_data_points[revision])
    604             points_at_this_revision = []
    605             for point in ignored_revision_data_points[revision]:
    606                 points_at_this_revision.append(point.bench)
    607             points_at_this_revision.sort()
    608             description += 'r%d: %s\n' % (revision, points_at_this_revision)
    609     if num_ignored_points == 0:
    610         print 'Did not discard any data points; all were within the range [%d-%d]' % (
    611             MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
    612     else:
    613         print '<table width="100%" bgcolor="ff0000"><tr><td align="center">'
    614         print 'Discarded %d data points outside of range [%d-%d]' % (
    615             num_ignored_points, MIN_REASONABLE_TIME, MAX_REASONABLE_TIME)
    616         print '</td></tr><tr><td width="100%" align="center">'
    617         print ('<textarea rows="4" style="width:97%" readonly="true" wrap="off">'
    618             + qe(description) + '</textarea>')
    619         print '</td></tr></table>'
    620 
    621 def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points,
    622                  regressions, requested_width, requested_height, title):
    623     """Outputs an svg/xhtml view of the data."""
    624     print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"',
    625     print '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
    626     print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">'
    627     print '<head>'
    628     print '<title>%s</title>' % qe(title)
    629     print '</head>'
    630     print '<body>'
    631     
    632     output_svg(lines, regressions, requested_width, requested_height)
    633 
    634     #output the manipulation controls
    635     print """
    636 <script type="text/javascript">//<![CDATA[
    637     function getElementsByClass(node, searchClass, tag) {
    638         var classElements = new Array();
    639         var elements = node.getElementsByTagName(tag);
    640         var pattern = new RegExp("^|\\s"+searchClass+"\\s|$");
    641         for (var i = 0, elementsFound = 0; i < elements.length; ++i) {
    642             if (pattern.test(elements[i].className)) {
    643                 classElements[elementsFound] = elements[i];
    644                 ++elementsFound;
    645             }
    646         }
    647         return classElements;
    648     }
    649     function getAllLines() {
    650         var selectElem = document.getElementById('benchSelect');
    651         var linesObj = {};
    652         for (var i = 0; i < selectElem.options.length; ++i) {
    653             var lines = JSON.parse(selectElem.options[i].value);
    654             for (var j = 0; j < lines.length; ++j) {
    655                 linesObj[lines[j]] = true;
    656             }
    657         }
    658         return linesObj;
    659     }
    660     function getOptions(selectElem) {
    661         var linesSelectedObj = {};
    662         for (var i = 0; i < selectElem.options.length; ++i) {
    663             if (!selectElem.options[i].selected) continue;
    664             
    665             var linesSelected = JSON.parse(selectElem.options[i].value);
    666             for (var j = 0; j < linesSelected.length; ++j) {
    667                 linesSelectedObj[linesSelected[j]] = true;
    668             }
    669         }
    670         return linesSelectedObj;
    671     }
    672     function objectEmpty(obj) {
    673         for (var p in obj) {
    674             return false;
    675         }
    676         return true;
    677     }
    678     function markSelectedLines(selectElem, allLines) {
    679         var linesSelected = getOptions(selectElem);
    680         if (!objectEmpty(linesSelected)) {
    681             for (var line in allLines) {
    682                 allLines[line] &= (linesSelected[line] == true);
    683             }
    684         }
    685     }
    686     function updateSvg() {
    687         var allLines = getAllLines();
    688         
    689         var selects = getElementsByClass(document, 'lines', 'select');
    690         for (var i = 0; i < selects.length; ++i) {
    691             markSelectedLines(selects[i], allLines);
    692         }
    693         
    694         for (var line in allLines) {
    695             var svgLine = document.getElementById(line);
    696             var display = (allLines[line] ? 'inline' : 'none');
    697             svgLine.setAttributeNS(null,'display', display);
    698         }
    699     }
    700     
    701     function mark(markerId) {
    702         for (var line in getAllLines()) {
    703             var svgLineGroup = document.getElementById(line);
    704             var display = svgLineGroup.getAttributeNS(null,'display');
    705             if (display == null || display == "" || display != "none") {
    706                 var svgLine = document.getElementById(line+'_line');
    707                 if (markerId == null) {
    708                     svgLine.removeAttributeNS(null,'marker-mid');
    709                 } else {
    710                     svgLine.setAttributeNS(null,'marker-mid', markerId);
    711                 }
    712             }
    713         }
    714     }
    715 //]]></script>"""
    716 
    717     all_settings = {}
    718     variant_settings = set()
    719     for label in lines.keys():
    720         for key, value  in label.settings.items():
    721             if key not in all_settings:
    722                 all_settings[key] = value
    723             elif all_settings[key] != value:
    724                 variant_settings.add(key)
    725 
    726     print '<table border="0" width="%s">' % requested_width
    727     #output column headers
    728     print """
    729 <tr valign="top"><td width="50%">
    730 <table border="0" width="100%">
    731 <tr><td align="center"><table border="0">
    732 <form>
    733 <tr valign="bottom" align="center">
    734 <td width="1">Bench&nbsp;Type</td>
    735 <td width="1">Bitmap Config</td>
    736 <td width="1">Timer&nbsp;Type (Cpu/Gpu/wall)</td>
    737 """
    738 
    739     for k in variant_settings:
    740         print '<td width="1">%s</td>' % qe(k)
    741 
    742     print '<td width="1"><!--buttons--></td></tr>'
    743 
    744     #output column contents
    745     print '<tr valign="top" align="center">'
    746     print '<td width="1">'
    747     create_select(lambda l: l.bench, lines, 'benchSelect')
    748     print '</td><td width="1">'
    749     create_select(lambda l: l.config, lines)
    750     print '</td><td width="1">'
    751     create_select(lambda l: l.time_type, lines)
    752 
    753     for k in variant_settings:
    754         print '</td><td width="1">'
    755         create_select(lambda l: l.settings.get(k, " "), lines)
    756 
    757     print '</td><td width="1"><button type="button"',
    758     print 'onclick=%s' % qa("mark('url(#circleMark)'); return false;"),
    759     print '>Mark Points</button>'
    760     print '<button type="button" onclick="mark(null);">Clear Points</button>'
    761     print '</td>'
    762     print """
    763 </tr>
    764 </form>
    765 </table></td></tr>
    766 <tr><td align="center">
    767 <hr />
    768 """
    769 
    770     output_ignored_data_points_warning(ignored_revision_data_points)
    771     print '</td></tr></table>'
    772     print '</td><td width="2%"><!--gutter--></td>'
    773 
    774     print '<td><table border="0">'
    775     print '<tr><td align="center">%s<br></br>revisions r%s - r%s</td></tr>' % (
    776         qe(title),
    777         bench_util.CreateRevisionLink(oldest_revision),
    778         bench_util.CreateRevisionLink(newest_revision))
    779     print """
    780 <tr><td align="left">
    781 <p>Brighter red indicates tests that have gotten worse; brighter green
    782 indicates tests that have gotten better.</p>
    783 <p>To highlight individual tests, hold down CONTROL and mouse over
    784 graph lines.</p>
    785 <p>To highlight revision numbers, hold down SHIFT and mouse over
    786 the graph area.</p>
    787 <p>To only show certain tests on the graph, select any combination of
    788 tests in the selectors at left.  (To show all, select all.)</p>
    789 <p>Use buttons at left to mark/clear points on the lines for selected
    790 benchmarks.</p>
    791 </td></tr>
    792 </table>
    793 
    794 </td>
    795 </tr>
    796 </table>
    797 </body>
    798 </html>"""
    799     
    800 def compute_size(requested_width, requested_height, rev_width, time_height):
    801     """Converts potentially empty requested size into a concrete size.
    802     
    803     (Number?,  Number?) -> (Number, Number)"""
    804     pic_width = 0
    805     pic_height = 0
    806     if (requested_width is not None and requested_height is not None):
    807         pic_height = requested_height
    808         pic_width = requested_width
    809     
    810     elif (requested_width is not None):
    811         pic_width = requested_width
    812         pic_height = pic_width * (float(time_height) / rev_width)
    813         
    814     elif (requested_height is not None):
    815         pic_height = requested_height
    816         pic_width = pic_height * (float(rev_width) / time_height)
    817         
    818     else:
    819         pic_height = 800
    820         pic_width = max(rev_width*3
    821                       , pic_height * (float(rev_width) / time_height))
    822     
    823     return (pic_width, pic_height)
    824 
    825 def output_svg(lines, regressions, requested_width, requested_height):
    826     """Outputs an svg view of the data."""
    827     
    828     (global_min_x, _), (global_max_x, global_max_y) = bounds(lines)
    829     max_up_slope, min_down_slope = bounds_slope(regressions)
    830 
    831     #output
    832     global_min_y = 0
    833     x = global_min_x
    834     y = global_min_y
    835     w = global_max_x - global_min_x
    836     h = global_max_y - global_min_y
    837     font_size = 16
    838     line_width = 2
    839 
    840     # If there is nothing to see, don't try to draw anything.
    841     if w == 0 or h == 0:
    842         return
    843 
    844     pic_width, pic_height = compute_size(requested_width, requested_height
    845                                        , w, h)
    846     
    847     def cw(w1):
    848         """Converts a revision difference to display width."""
    849         return (pic_width / float(w)) * w1
    850     def cx(x):
    851         """Converts a revision to a horizontal display position."""
    852         return cw(x - global_min_x)
    853 
    854     def ch(h1):
    855         """Converts a time difference to a display height."""
    856         return -(pic_height / float(h)) * h1
    857     def cy(y):
    858         """Converts a time to a vertical display position."""
    859         return pic_height + ch(y - global_min_y)
    860     
    861     print '<!--Picture height %.2f corresponds to bench value %.2f.-->' % (
    862         pic_height, h)
    863     print '<svg',
    864     print 'width=%s' % qa(str(pic_width)+'px')
    865     print 'height=%s' % qa(str(pic_height)+'px')
    866     print 'viewBox="0 0 %s %s"' % (str(pic_width), str(pic_height))
    867     print 'onclick=%s' % qa(
    868             "var event = arguments[0] || window.event;"
    869             " if (event.shiftKey) { highlightRevision(null); }"
    870             " if (event.ctrlKey) { highlight(null); }"
    871             " return false;")
    872     print 'xmlns="http://www.w3.org/2000/svg"'
    873     print 'xmlns:xlink="http://www.w3.org/1999/xlink">'
    874     
    875     print """
    876 <defs>
    877     <marker id="circleMark"
    878       viewBox="0 0 2 2" refX="1" refY="1"
    879       markerUnits="strokeWidth"
    880       markerWidth="2" markerHeight="2"
    881       orient="0">
    882       <circle cx="1" cy="1" r="1"/>
    883     </marker>
    884 </defs>"""
    885     
    886     #output the revisions
    887     print """
    888 <script type="text/javascript">//<![CDATA[
    889     var previousRevision;
    890     var previousRevisionFill;
    891     var previousRevisionStroke
    892     function highlightRevision(id) {
    893         if (previousRevision == id) return;
    894 
    895         document.getElementById('revision').firstChild.nodeValue = 'r' + id;
    896         document.getElementById('rev_link').setAttribute('xlink:href',
    897             'http://code.google.com/p/skia/source/detail?r=' + id);
    898         
    899         var preRevision = document.getElementById(previousRevision);
    900         if (preRevision) {
    901             preRevision.setAttributeNS(null,'fill', previousRevisionFill);
    902             preRevision.setAttributeNS(null,'stroke', previousRevisionStroke);
    903         }
    904         
    905         var revision = document.getElementById(id);
    906         previousRevision = id;
    907         if (revision) {
    908             previousRevisionFill = revision.getAttributeNS(null,'fill');
    909             revision.setAttributeNS(null,'fill','rgb(100%, 95%, 95%)');
    910             
    911             previousRevisionStroke = revision.getAttributeNS(null,'stroke');
    912             revision.setAttributeNS(null,'stroke','rgb(100%, 90%, 90%)');
    913         }
    914     }
    915 //]]></script>"""
    916     
    917     def print_rect(x, y, w, h, revision):
    918         """Outputs a revision rectangle in display space,
    919            taking arguments in revision space."""
    920         disp_y = cy(y)
    921         disp_h = ch(h)
    922         if disp_h < 0:
    923             disp_y += disp_h
    924             disp_h = -disp_h
    925         
    926         print '<rect id=%s x=%s y=%s' % (qa(revision), qa(cx(x)), qa(disp_y),),
    927         print 'width=%s height=%s' % (qa(cw(w)), qa(disp_h),),
    928         print 'fill="white"',
    929         print 'stroke="rgb(98%%,98%%,88%%)" stroke-width=%s' % qa(line_width),
    930         print 'onmouseover=%s' % qa(
    931                 "var event = arguments[0] || window.event;"
    932                 " if (event.shiftKey) {"
    933                     " highlightRevision('"+str(revision)+"');"
    934                     " return false;"
    935                 " }"),
    936         print ' />'
    937     
    938     xes = set()
    939     for line in lines.itervalues():
    940         for point in line:
    941             xes.add(point[0])
    942     revisions = list(xes)
    943     revisions.sort()
    944     
    945     left = x
    946     current_revision = revisions[0]
    947     for next_revision in revisions[1:]:
    948         width = (((next_revision - current_revision) / 2.0)
    949                  + (current_revision - left))
    950         print_rect(left, y, width, h, current_revision)
    951         left += width
    952         current_revision = next_revision
    953     print_rect(left, y, x+w - left, h, current_revision)
    954 
    955     #output the lines
    956     print """
    957 <script type="text/javascript">//<![CDATA[
    958     var previous;
    959     var previousColor;
    960     var previousOpacity;
    961     function highlight(id) {
    962         if (previous == id) return;
    963 
    964         document.getElementById('label').firstChild.nodeValue = id;
    965 
    966         var preGroup = document.getElementById(previous);
    967         if (preGroup) {
    968             var preLine = document.getElementById(previous+'_line');
    969             preLine.setAttributeNS(null,'stroke', previousColor);
    970             preLine.setAttributeNS(null,'opacity', previousOpacity);
    971 
    972             var preSlope = document.getElementById(previous+'_linear');
    973             if (preSlope) {
    974                 preSlope.setAttributeNS(null,'visibility', 'hidden');
    975             }
    976         }
    977 
    978         var group = document.getElementById(id);
    979         previous = id;
    980         if (group) {
    981             group.parentNode.appendChild(group);
    982             
    983             var line = document.getElementById(id+'_line');
    984             previousColor = line.getAttributeNS(null,'stroke');
    985             previousOpacity = line.getAttributeNS(null,'opacity');
    986             line.setAttributeNS(null,'stroke', 'blue');
    987             line.setAttributeNS(null,'opacity', '1');
    988             
    989             var slope = document.getElementById(id+'_linear');
    990             if (slope) {
    991                 slope.setAttributeNS(null,'visibility', 'visible');
    992             }
    993         }
    994     }
    995 //]]></script>"""
    996 
    997     # Add a new element to each item in the 'lines' list: the label in string
    998     # form.  Then use that element to sort the list.
    999     sorted_lines = []
   1000     for label, line in lines.items():
   1001         sorted_lines.append([str(label), label, line])
   1002     sorted_lines.sort()
   1003 
   1004     for label_as_string, label, line in sorted_lines:
   1005         print '<g id=%s>' % qa(label_as_string)
   1006         r = 128
   1007         g = 128
   1008         b = 128
   1009         a = .10
   1010         if label in regressions:
   1011             regression = regressions[label]
   1012             min_slope = regression.find_min_slope()
   1013             if min_slope < 0:
   1014                 d = max(0, (min_slope / min_down_slope))
   1015                 g += int(d*128)
   1016                 a += d*0.9
   1017             elif min_slope > 0:
   1018                 d = max(0, (min_slope / max_up_slope))
   1019                 r += int(d*128)
   1020                 a += d*0.9
   1021             
   1022             slope = regression.slope
   1023             intercept = regression.intercept
   1024             min_x = regression.min_x
   1025             max_x = regression.max_x
   1026             print '<polyline id=%s' % qa(str(label)+'_linear'),
   1027             print 'fill="none" stroke="yellow"',
   1028             print 'stroke-width=%s' % qa(abs(ch(regression.serror*2))),
   1029             print 'opacity="0.5" pointer-events="none" visibility="hidden"',
   1030             print 'points="',
   1031             print '%s,%s' % (str(cx(min_x)), str(cy(slope*min_x + intercept))),
   1032             print '%s,%s' % (str(cx(max_x)), str(cy(slope*max_x + intercept))),
   1033             print '"/>'
   1034         
   1035         print '<polyline id=%s' % qa(str(label)+'_line'),
   1036         print 'onmouseover=%s' % qa(
   1037                 "var event = arguments[0] || window.event;"
   1038                 " if (event.ctrlKey) {"
   1039                     " highlight('"+str(label).replace("'", "\\'")+"');"
   1040                     " return false;"
   1041                 " }"),
   1042         print 'fill="none" stroke="rgb(%s,%s,%s)"' % (str(r), str(g), str(b)),
   1043         print 'stroke-width=%s' % qa(line_width),
   1044         print 'opacity=%s' % qa(a),
   1045         print 'points="',
   1046         for point in line:
   1047             print '%s,%s' % (str(cx(point[0])), str(cy(point[1]))),
   1048         print '"/>'
   1049 
   1050         print '</g>'
   1051 
   1052     #output the labels
   1053     print '<text id="label" x="0" y=%s' % qa(font_size),
   1054     print 'font-size=%s> </text>' % qa(font_size)
   1055 
   1056     print '<a id="rev_link" xlink:href="" target="_top">'
   1057     print '<text id="revision" x="0" y=%s style="' % qa(font_size*2)
   1058     print 'font-size: %s; ' % qe(font_size)
   1059     print 'stroke: #0000dd; text-decoration: underline; '
   1060     print '"> </text></a>'
   1061 
   1062     print '</svg>'
   1063 
   1064 if __name__ == "__main__":
   1065     main()
   1066