| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1 | ''' | 
 | 2 | Created on May 16, 2011 | 
 | 3 |  | 
 | 4 | @author: bungeman | 
 | 5 | ''' | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 6 | import bench_util | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 7 | import getopt | 
 | 8 | import httplib | 
 | 9 | import itertools | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 10 | import json | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 11 | import os | 
 | 12 | import re | 
 | 13 | import sys | 
 | 14 | import urllib | 
 | 15 | import urllib2 | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 16 | import xml.sax.saxutils | 
 | 17 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 18 | # We throw out any measurement outside this range, and log a warning. | 
 | 19 | MIN_REASONABLE_TIME = 0 | 
 | 20 | MAX_REASONABLE_TIME = 99999 | 
 | 21 |  | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 22 | # Constants for prefixes in output title used in buildbot. | 
| borenet@google.com | e6598a0 | 2013-04-30 12:02:32 +0000 | [diff] [blame] | 23 | TITLE_PREAMBLE = 'Bench_Performance_for_' | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 24 | TITLE_PREAMBLE_LENGTH = len(TITLE_PREAMBLE) | 
 | 25 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 26 | def usage(): | 
 | 27 |     """Prints simple usage information.""" | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 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.' | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 32 |     print '-b <bench> the bench to show.' | 
 | 33 |     print '-c <config> the config to show (GPU, 8888, 565, etc).' | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 34 |     print '-d <dir> a directory containing bench_r<revision>_<scalar> files.' | 
| bensong@google.com | 7d9a21b | 2013-09-25 20:51:16 +0000 | [diff] [blame] | 35 |     print '-e <file> file containing expected bench builder values/ranges.' | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 36 |     print '   Will raise exception if actual bench values are out of range.' | 
| bensong@google.com | 7d9a21b | 2013-09-25 20:51:16 +0000 | [diff] [blame] | 37 |     print '   See bench_expectations_<builder>.txt for data format / examples.' | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 38 |     print '-f <revision>[:<revision>] the revisions to use for fitting.' | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 39 |     print '   Negative <revision> is taken as offset from most recent revision.' | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 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.' | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 43 |     print '-l <title> title to use for the output graph' | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 44 |     print '-m <representation> representation of bench value.' | 
 | 45 |     print '   See _ListAlgorithm class in bench_util.py.' | 
| borenet@google.com | 5dc0678 | 2013-03-12 17:16:07 +0000 | [diff] [blame] | 46 |     print '-o <path> path to which to write output.' | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 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).' | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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 |  | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 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 |  | 
| bensong@google.com | 8734816 | 2012-08-15 17:31:46 +0000 | [diff] [blame] | 110 | def parse_dir(directory, default_settings, oldest_revision, newest_revision, | 
 | 111 |               rep): | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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]} | 
| epoger@google.com | e9b31fa | 2013-02-14 20:13:32 +0000 | [diff] [blame] | 116 |     file_list = os.listdir(directory) | 
 | 117 |     file_list.sort() | 
 | 118 |     for bench_file in file_list: | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 119 |         file_name_match = re.match('bench_r(\d+)_(\S+)', bench_file) | 
 | 120 |         if (file_name_match is None): | 
 | 121 |             continue | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 122 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 123 |         revision = int(file_name_match.group(1)) | 
 | 124 |         scalar_type = file_name_match.group(2) | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 125 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 126 |         if (revision < oldest_revision or revision > newest_revision): | 
 | 127 |             continue | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 128 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 129 |         file_handle = open(directory + '/' + bench_file, 'r') | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 130 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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( | 
| bensong@google.com | 8734816 | 2012-08-15 17:31:46 +0000 | [diff] [blame] | 135 |                         bench_util.parse(default_settings, file_handle, rep)) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 136 |         file_handle.close() | 
 | 137 |     return revision_data_points | 
 | 138 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 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 |  | 
| epoger@google.com | 1513f6e | 2012-06-27 13:38:37 +0000 | [diff] [blame] | 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 |  | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 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.""" | 
| epoger@google.com | 1513f6e | 2012-06-27 13:38:37 +0000 | [diff] [blame] | 193 |     abs_path = get_abs_path(output_path) | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 194 |     sys.stdout = open(abs_path, 'w') | 
 | 195 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 196 | def create_lines(revision_data_points, settings | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 197 |                , bench_of_interest, config_of_interest, time_of_interest | 
 | 198 |                , time_to_ignore): | 
| epoger@google.com | 2459a39 | 2013-02-14 18:58:05 +0000 | [diff] [blame] | 199 |     """Convert revision data into a dictionary of line data. | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 200 |      | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 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 |     """ | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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 | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 235 |             elif (time_to_ignore is not None and | 
 | 236 |                   time_to_ignore == point.time_type): | 
 | 237 |                 continue | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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:] | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 312 |                                  , "a:b:c:d:e:f:i:l:m:o:r:s:t:x:y:" | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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 | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 323 |     time_to_ignore = None | 
| borenet@google.com | 5dc0678 | 2013-03-12 17:16:07 +0000 | [diff] [blame] | 324 |     output_path = None | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 325 |     bench_expectations = {} | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 326 |     appengine_url = None  # used for adding data to appengine datastore | 
| bensong@google.com | b6204b1 | 2012-08-16 20:49:28 +0000 | [diff] [blame] | 327 |     rep = None  # bench representation algorithm | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 328 |     revision_range = '0:' | 
 | 329 |     regression_range = '0:' | 
 | 330 |     latest_revision = None | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 331 |     requested_height = None | 
 | 332 |     requested_width = None | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 333 |     title = 'Bench graph' | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 334 |     settings = {} | 
 | 335 |     default_settings = {} | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 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; | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 346 |         if not new: | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 347 |             new = latest_revision; | 
 | 348 |         new = int(new) | 
 | 349 |         if new < 0: | 
 | 350 |             new += latest_revision; | 
 | 351 |         return (old, new) | 
 | 352 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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 | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 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): | 
| bensong@google.com | 43e4f6e | 2013-07-30 14:47:04 +0000 | [diff] [blame] | 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. | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 387 |         exceptions = [] | 
 | 388 |         for line in lines: | 
 | 389 |             line_str = str(line) | 
| bensong@google.com | 43e4f6e | 2013-07-30 14:47:04 +0000 | [diff] [blame] | 390 |             line_str = line_str[ : line_str.find('_{')] | 
 | 391 |             bench_platform_key = line_str + ',' + key_suffix | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 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: | 
| bensong@google.com | 43e4f6e | 2013-07-30 14:47:04 +0000 | [diff] [blame] | 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) | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 412 |         if exceptions: | 
 | 413 |             raise Exception('Bench values out of range:\n' + | 
 | 414 |                             '\n'.join(exceptions)) | 
 | 415 |  | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 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 |         """ | 
| bensong@google.com | ae6f47e | 2013-04-08 14:57:40 +0000 | [diff] [blame] | 423 |         config_data_dic = {} | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 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 | 
| bensong@google.com | ae6f47e | 2013-04-08 14:57:40 +0000 | [diff] [blame] | 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)) | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 456 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 457 |     try: | 
 | 458 |         for option, value in opts: | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 459 |             if option == "-a": | 
 | 460 |                 appengine_url = value | 
 | 461 |             elif option == "-b": | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 462 |                 bench_of_interest = value | 
 | 463 |             elif option == "-c": | 
 | 464 |                 config_of_interest = value | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 465 |             elif option == "-d": | 
 | 466 |                 directory = value | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 467 |             elif option == "-e": | 
 | 468 |                 read_expectations(bench_expectations, value) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 469 |             elif option == "-f": | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 470 |                 regression_range = value | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 471 |             elif option == "-i": | 
 | 472 |                 time_to_ignore = value | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 473 |             elif option == "-l": | 
 | 474 |                 title = value | 
| bensong@google.com | 8734816 | 2012-08-15 17:31:46 +0000 | [diff] [blame] | 475 |             elif option == "-m": | 
 | 476 |                 rep = value | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 477 |             elif option == "-o": | 
| borenet@google.com | 5dc0678 | 2013-03-12 17:16:07 +0000 | [diff] [blame] | 478 |                 output_path = value | 
 | 479 |                 redirect_stdout(output_path) | 
| epoger@google.com | 5b2e01c | 2012-06-25 20:29:04 +0000 | [diff] [blame] | 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 | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 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) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 498 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 499 |     if directory is None: | 
 | 500 |         usage() | 
 | 501 |         sys.exit(2) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 502 |  | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 503 |     if time_of_interest: | 
 | 504 |         time_to_ignore = None | 
 | 505 |  | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 506 |     # The title flag (-l) provided in buildbot slave is in the format | 
| borenet@google.com | e6598a0 | 2013-04-30 12:02:32 +0000 | [diff] [blame] | 507 |     # Bench_Performance_for_<platform>, and we want to extract <platform> | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 508 |     # for use in platform_and_alg to track matching benches later. If title flag | 
 | 509 |     # is not in this format, there may be no matching benches in the file | 
 | 510 |     # provided by the expectation_file flag (-e). | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 511 |     bot = title  # To store the platform as bot name | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 512 |     platform_and_alg = title | 
 | 513 |     if platform_and_alg.startswith(TITLE_PREAMBLE): | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 514 |         bot = platform_and_alg[TITLE_PREAMBLE_LENGTH:] | 
 | 515 |         platform_and_alg = bot + '-' + rep | 
| bensong@google.com | 8c1de76 | 2012-08-15 18:27:38 +0000 | [diff] [blame] | 516 |     title += ' [representation: %s]' % rep | 
 | 517 |  | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 518 |     latest_revision = get_latest_revision(directory) | 
 | 519 |     oldest_revision, newest_revision = parse_range(revision_range) | 
 | 520 |     oldest_regression, newest_regression = parse_range(regression_range) | 
 | 521 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 522 |     unfiltered_revision_data_points = parse_dir(directory | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 523 |                                    , default_settings | 
 | 524 |                                    , oldest_revision | 
| bensong@google.com | 8734816 | 2012-08-15 17:31:46 +0000 | [diff] [blame] | 525 |                                    , newest_revision | 
 | 526 |                                    , rep) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 527 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 528 |     # Filter out any data points that are utterly bogus... make sure to report | 
 | 529 |     # that we did so later! | 
 | 530 |     (allowed_revision_data_points, ignored_revision_data_points) = filter_data_points( | 
 | 531 |         unfiltered_revision_data_points) | 
 | 532 |  | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 533 |     # Update oldest_revision and newest_revision based on the data we could find | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 534 |     all_revision_numbers = allowed_revision_data_points.keys() | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 535 |     oldest_revision = min(all_revision_numbers) | 
 | 536 |     newest_revision = max(all_revision_numbers) | 
 | 537 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 538 |     lines = create_lines(allowed_revision_data_points | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 539 |                    , settings | 
 | 540 |                    , bench_of_interest | 
 | 541 |                    , config_of_interest | 
| bensong@google.com | 8ccfa55 | 2012-08-17 21:42:14 +0000 | [diff] [blame] | 542 |                    , time_of_interest | 
 | 543 |                    , time_to_ignore) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 544 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 545 |     regressions = create_regressions(lines | 
 | 546 |                                    , oldest_regression | 
 | 547 |                                    , newest_regression) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 548 |  | 
| borenet@google.com | 5dc0678 | 2013-03-12 17:16:07 +0000 | [diff] [blame] | 549 |     if output_path: | 
 | 550 |         output_xhtml(lines, oldest_revision, newest_revision, | 
 | 551 |                      ignored_revision_data_points, regressions, requested_width, | 
 | 552 |                      requested_height, title) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 553 |  | 
| bensong@google.com | 848fa2b | 2013-03-07 17:12:43 +0000 | [diff] [blame] | 554 |     if appengine_url: | 
 | 555 |         write_to_appengine(lines, appengine_url, newest_revision, bot) | 
 | 556 |  | 
| borenet@google.com | 5dc0678 | 2013-03-12 17:16:07 +0000 | [diff] [blame] | 557 |     if bench_expectations: | 
 | 558 |         check_expectations(lines, bench_expectations, newest_revision, | 
 | 559 |                            platform_and_alg) | 
| bensong@google.com | ad0c5d2 | 2012-09-13 21:08:52 +0000 | [diff] [blame] | 560 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 561 | def qa(out): | 
 | 562 |     """Stringify input and quote as an xml attribute.""" | 
 | 563 |     return xml.sax.saxutils.quoteattr(str(out)) | 
 | 564 | def qe(out): | 
 | 565 |     """Stringify input and escape as xml data.""" | 
 | 566 |     return xml.sax.saxutils.escape(str(out)) | 
 | 567 |  | 
 | 568 | def create_select(qualifier, lines, select_id=None): | 
 | 569 |     """Output select with options showing lines which qualifier maps to it. | 
 | 570 |      | 
 | 571 |     ((Label) -> str, {Label:_}, str?) -> _""" | 
 | 572 |     options = {} #{ option : [Label]} | 
 | 573 |     for label in lines.keys(): | 
 | 574 |         option = qualifier(label) | 
 | 575 |         if (option not in options): | 
 | 576 |             options[option] = [] | 
 | 577 |         options[option].append(label) | 
 | 578 |     option_list = list(options.keys()) | 
 | 579 |     option_list.sort() | 
 | 580 |     print '<select class="lines"', | 
 | 581 |     if select_id is not None: | 
 | 582 |         print 'id=%s' % qa(select_id) | 
 | 583 |     print 'multiple="true" size="10" onchange="updateSvg();">' | 
 | 584 |     for option in option_list: | 
 | 585 |         print '<option value=' + qa('[' +  | 
 | 586 |         reduce(lambda x,y:x+json.dumps(str(y))+',',options[option],"")[0:-1] | 
 | 587 |         + ']') + '>'+qe(option)+'</option>' | 
 | 588 |     print '</select>' | 
 | 589 |  | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 590 | def output_ignored_data_points_warning(ignored_revision_data_points): | 
 | 591 |     """Write description of ignored_revision_data_points to stdout as xhtml. | 
 | 592 |     """ | 
 | 593 |     num_ignored_points = 0 | 
 | 594 |     description = '' | 
 | 595 |     revisions = ignored_revision_data_points.keys() | 
 | 596 |     if revisions: | 
 | 597 |         revisions.sort() | 
 | 598 |         revisions.reverse() | 
 | 599 |         for revision in revisions: | 
 | 600 |             num_ignored_points += len(ignored_revision_data_points[revision]) | 
 | 601 |             points_at_this_revision = [] | 
 | 602 |             for point in ignored_revision_data_points[revision]: | 
 | 603 |                 points_at_this_revision.append(point.bench) | 
 | 604 |             points_at_this_revision.sort() | 
 | 605 |             description += 'r%d: %s\n' % (revision, points_at_this_revision) | 
 | 606 |     if num_ignored_points == 0: | 
 | 607 |         print 'Did not discard any data points; all were within the range [%d-%d]' % ( | 
 | 608 |             MIN_REASONABLE_TIME, MAX_REASONABLE_TIME) | 
 | 609 |     else: | 
 | 610 |         print '<table width="100%" bgcolor="ff0000"><tr><td align="center">' | 
 | 611 |         print 'Discarded %d data points outside of range [%d-%d]' % ( | 
 | 612 |             num_ignored_points, MIN_REASONABLE_TIME, MAX_REASONABLE_TIME) | 
 | 613 |         print '</td></tr><tr><td width="100%" align="center">' | 
 | 614 |         print ('<textarea rows="4" style="width:97%" readonly="true" wrap="off">' | 
 | 615 |             + qe(description) + '</textarea>') | 
 | 616 |         print '</td></tr></table>' | 
 | 617 |  | 
 | 618 | def output_xhtml(lines, oldest_revision, newest_revision, ignored_revision_data_points, | 
| epoger@google.com | 3726000 | 2011-08-08 20:27:04 +0000 | [diff] [blame] | 619 |                  regressions, requested_width, requested_height, title): | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 620 |     """Outputs an svg/xhtml view of the data.""" | 
 | 621 |     print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"', | 
 | 622 |     print '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' | 
 | 623 |     print '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">' | 
 | 624 |     print '<head>' | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 625 |     print '<title>%s</title>' % qe(title) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 626 |     print '</head>' | 
 | 627 |     print '<body>' | 
 | 628 |      | 
 | 629 |     output_svg(lines, regressions, requested_width, requested_height) | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 630 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 631 |     #output the manipulation controls | 
 | 632 |     print """ | 
 | 633 | <script type="text/javascript">//<![CDATA[ | 
 | 634 |     function getElementsByClass(node, searchClass, tag) { | 
 | 635 |         var classElements = new Array(); | 
 | 636 |         var elements = node.getElementsByTagName(tag); | 
 | 637 |         var pattern = new RegExp("^|\\s"+searchClass+"\\s|$"); | 
 | 638 |         for (var i = 0, elementsFound = 0; i < elements.length; ++i) { | 
 | 639 |             if (pattern.test(elements[i].className)) { | 
 | 640 |                 classElements[elementsFound] = elements[i]; | 
 | 641 |                 ++elementsFound; | 
 | 642 |             } | 
 | 643 |         } | 
 | 644 |         return classElements; | 
 | 645 |     } | 
 | 646 |     function getAllLines() { | 
 | 647 |         var selectElem = document.getElementById('benchSelect'); | 
 | 648 |         var linesObj = {}; | 
 | 649 |         for (var i = 0; i < selectElem.options.length; ++i) { | 
 | 650 |             var lines = JSON.parse(selectElem.options[i].value); | 
 | 651 |             for (var j = 0; j < lines.length; ++j) { | 
 | 652 |                 linesObj[lines[j]] = true; | 
 | 653 |             } | 
 | 654 |         } | 
 | 655 |         return linesObj; | 
 | 656 |     } | 
 | 657 |     function getOptions(selectElem) { | 
 | 658 |         var linesSelectedObj = {}; | 
 | 659 |         for (var i = 0; i < selectElem.options.length; ++i) { | 
 | 660 |             if (!selectElem.options[i].selected) continue; | 
 | 661 |              | 
 | 662 |             var linesSelected = JSON.parse(selectElem.options[i].value); | 
 | 663 |             for (var j = 0; j < linesSelected.length; ++j) { | 
 | 664 |                 linesSelectedObj[linesSelected[j]] = true; | 
 | 665 |             } | 
 | 666 |         } | 
 | 667 |         return linesSelectedObj; | 
 | 668 |     } | 
 | 669 |     function objectEmpty(obj) { | 
 | 670 |         for (var p in obj) { | 
 | 671 |             return false; | 
 | 672 |         } | 
 | 673 |         return true; | 
 | 674 |     } | 
 | 675 |     function markSelectedLines(selectElem, allLines) { | 
 | 676 |         var linesSelected = getOptions(selectElem); | 
 | 677 |         if (!objectEmpty(linesSelected)) { | 
 | 678 |             for (var line in allLines) { | 
 | 679 |                 allLines[line] &= (linesSelected[line] == true); | 
 | 680 |             } | 
 | 681 |         } | 
 | 682 |     } | 
 | 683 |     function updateSvg() { | 
 | 684 |         var allLines = getAllLines(); | 
 | 685 |          | 
 | 686 |         var selects = getElementsByClass(document, 'lines', 'select'); | 
 | 687 |         for (var i = 0; i < selects.length; ++i) { | 
 | 688 |             markSelectedLines(selects[i], allLines); | 
 | 689 |         } | 
 | 690 |          | 
 | 691 |         for (var line in allLines) { | 
 | 692 |             var svgLine = document.getElementById(line); | 
 | 693 |             var display = (allLines[line] ? 'inline' : 'none'); | 
 | 694 |             svgLine.setAttributeNS(null,'display', display); | 
 | 695 |         } | 
 | 696 |     } | 
 | 697 |      | 
 | 698 |     function mark(markerId) { | 
 | 699 |         for (var line in getAllLines()) { | 
 | 700 |             var svgLineGroup = document.getElementById(line); | 
 | 701 |             var display = svgLineGroup.getAttributeNS(null,'display'); | 
 | 702 |             if (display == null || display == "" || display != "none") { | 
 | 703 |                 var svgLine = document.getElementById(line+'_line'); | 
 | 704 |                 if (markerId == null) { | 
 | 705 |                     svgLine.removeAttributeNS(null,'marker-mid'); | 
 | 706 |                 } else { | 
 | 707 |                     svgLine.setAttributeNS(null,'marker-mid', markerId); | 
 | 708 |                 } | 
 | 709 |             } | 
 | 710 |         } | 
 | 711 |     } | 
 | 712 | //]]></script>""" | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 713 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 714 |     all_settings = {} | 
 | 715 |     variant_settings = set() | 
 | 716 |     for label in lines.keys(): | 
 | 717 |         for key, value  in label.settings.items(): | 
 | 718 |             if key not in all_settings: | 
 | 719 |                 all_settings[key] = value | 
 | 720 |             elif all_settings[key] != value: | 
 | 721 |                 variant_settings.add(key) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 722 |  | 
| bungeman@google.com | 752acc7 | 2012-09-12 19:34:17 +0000 | [diff] [blame] | 723 |     print '<table border="0" width="%s">' % requested_width | 
 | 724 |     #output column headers | 
 | 725 |     print """ | 
 | 726 | <tr valign="top"><td width="50%"> | 
 | 727 | <table border="0" width="100%"> | 
 | 728 | <tr><td align="center"><table border="0"> | 
 | 729 | <form> | 
 | 730 | <tr valign="bottom" align="center"> | 
 | 731 | <td width="1">Bench Type</td> | 
 | 732 | <td width="1">Bitmap Config</td> | 
 | 733 | <td width="1">Timer Type (Cpu/Gpu/wall)</td> | 
 | 734 | """ | 
 | 735 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 736 |     for k in variant_settings: | 
| bungeman@google.com | 752acc7 | 2012-09-12 19:34:17 +0000 | [diff] [blame] | 737 |         print '<td width="1">%s</td>' % qe(k) | 
 | 738 |  | 
 | 739 |     print '<td width="1"><!--buttons--></td></tr>' | 
 | 740 |  | 
 | 741 |     #output column contents | 
 | 742 |     print '<tr valign="top" align="center">' | 
 | 743 |     print '<td width="1">' | 
 | 744 |     create_select(lambda l: l.bench, lines, 'benchSelect') | 
 | 745 |     print '</td><td width="1">' | 
 | 746 |     create_select(lambda l: l.config, lines) | 
 | 747 |     print '</td><td width="1">' | 
 | 748 |     create_select(lambda l: l.time_type, lines) | 
 | 749 |  | 
 | 750 |     for k in variant_settings: | 
 | 751 |         print '</td><td width="1">' | 
 | 752 |         create_select(lambda l: l.settings.get(k, " "), lines) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 753 |  | 
 | 754 |     print '</td><td width="1"><button type="button"', | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 755 |     print 'onclick=%s' % qa("mark('url(#circleMark)'); return false;"), | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 756 |     print '>Mark Points</button>' | 
 | 757 |     print '<button type="button" onclick="mark(null);">Clear Points</button>' | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 758 |     print '</td>' | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 759 |     print """ | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 760 | </tr> | 
 | 761 | </form> | 
 | 762 | </table></td></tr> | 
 | 763 | <tr><td align="center"> | 
 | 764 | <hr /> | 
 | 765 | """ | 
 | 766 |  | 
 | 767 |     output_ignored_data_points_warning(ignored_revision_data_points) | 
 | 768 |     print '</td></tr></table>' | 
 | 769 |     print '</td><td width="2%"><!--gutter--></td>' | 
 | 770 |  | 
 | 771 |     print '<td><table border="0">' | 
 | 772 |     print '<tr><td align="center">%s<br></br>revisions r%s - r%s</td></tr>' % ( | 
 | 773 |         qe(title), | 
 | 774 |         bench_util.CreateRevisionLink(oldest_revision), | 
 | 775 |         bench_util.CreateRevisionLink(newest_revision)) | 
 | 776 |     print """ | 
 | 777 | <tr><td align="left"> | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 778 | <p>Brighter red indicates tests that have gotten worse; brighter green | 
 | 779 | indicates tests that have gotten better.</p> | 
 | 780 | <p>To highlight individual tests, hold down CONTROL and mouse over | 
 | 781 | graph lines.</p> | 
 | 782 | <p>To highlight revision numbers, hold down SHIFT and mouse over | 
 | 783 | the graph area.</p> | 
 | 784 | <p>To only show certain tests on the graph, select any combination of | 
 | 785 | tests in the selectors at left.  (To show all, select all.)</p> | 
 | 786 | <p>Use buttons at left to mark/clear points on the lines for selected | 
 | 787 | benchmarks.</p> | 
| epoger@google.com | 3d8cd17 | 2012-05-11 18:26:16 +0000 | [diff] [blame] | 788 | </td></tr> | 
 | 789 | </table> | 
 | 790 |  | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 791 | </td> | 
 | 792 | </tr> | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 793 | </table> | 
 | 794 | </body> | 
 | 795 | </html>""" | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 796 |      | 
 | 797 | def compute_size(requested_width, requested_height, rev_width, time_height): | 
 | 798 |     """Converts potentially empty requested size into a concrete size. | 
 | 799 |      | 
 | 800 |     (Number?,  Number?) -> (Number, Number)""" | 
 | 801 |     pic_width = 0 | 
 | 802 |     pic_height = 0 | 
 | 803 |     if (requested_width is not None and requested_height is not None): | 
 | 804 |         pic_height = requested_height | 
 | 805 |         pic_width = requested_width | 
 | 806 |      | 
 | 807 |     elif (requested_width is not None): | 
 | 808 |         pic_width = requested_width | 
 | 809 |         pic_height = pic_width * (float(time_height) / rev_width) | 
 | 810 |          | 
 | 811 |     elif (requested_height is not None): | 
 | 812 |         pic_height = requested_height | 
 | 813 |         pic_width = pic_height * (float(rev_width) / time_height) | 
 | 814 |          | 
 | 815 |     else: | 
 | 816 |         pic_height = 800 | 
 | 817 |         pic_width = max(rev_width*3 | 
 | 818 |                       , pic_height * (float(rev_width) / time_height)) | 
 | 819 |      | 
 | 820 |     return (pic_width, pic_height) | 
 | 821 |  | 
 | 822 | def output_svg(lines, regressions, requested_width, requested_height): | 
 | 823 |     """Outputs an svg view of the data.""" | 
 | 824 |      | 
 | 825 |     (global_min_x, _), (global_max_x, global_max_y) = bounds(lines) | 
 | 826 |     max_up_slope, min_down_slope = bounds_slope(regressions) | 
| bensong@google.com | 741b2e1 | 2013-06-13 13:53:16 +0000 | [diff] [blame] | 827 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 828 |     #output | 
 | 829 |     global_min_y = 0 | 
 | 830 |     x = global_min_x | 
 | 831 |     y = global_min_y | 
 | 832 |     w = global_max_x - global_min_x | 
 | 833 |     h = global_max_y - global_min_y | 
 | 834 |     font_size = 16 | 
 | 835 |     line_width = 2 | 
| bensong@google.com | 741b2e1 | 2013-06-13 13:53:16 +0000 | [diff] [blame] | 836 |  | 
 | 837 |     # If there is nothing to see, don't try to draw anything. | 
 | 838 |     if w == 0 or h == 0: | 
 | 839 |         return | 
 | 840 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 841 |     pic_width, pic_height = compute_size(requested_width, requested_height | 
 | 842 |                                        , w, h) | 
 | 843 |      | 
 | 844 |     def cw(w1): | 
 | 845 |         """Converts a revision difference to display width.""" | 
 | 846 |         return (pic_width / float(w)) * w1 | 
 | 847 |     def cx(x): | 
 | 848 |         """Converts a revision to a horizontal display position.""" | 
 | 849 |         return cw(x - global_min_x) | 
 | 850 |  | 
 | 851 |     def ch(h1): | 
 | 852 |         """Converts a time difference to a display height.""" | 
 | 853 |         return -(pic_height / float(h)) * h1 | 
 | 854 |     def cy(y): | 
 | 855 |         """Converts a time to a vertical display position.""" | 
 | 856 |         return pic_height + ch(y - global_min_y) | 
 | 857 |      | 
| bensong@google.com | 7426743 | 2012-08-30 18:19:02 +0000 | [diff] [blame] | 858 |     print '<!--Picture height %.2f corresponds to bench value %.2f.-->' % ( | 
 | 859 |         pic_height, h) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 860 |     print '<svg', | 
 | 861 |     print 'width=%s' % qa(str(pic_width)+'px') | 
 | 862 |     print 'height=%s' % qa(str(pic_height)+'px') | 
 | 863 |     print 'viewBox="0 0 %s %s"' % (str(pic_width), str(pic_height)) | 
 | 864 |     print 'onclick=%s' % qa( | 
 | 865 |             "var event = arguments[0] || window.event;" | 
 | 866 |             " if (event.shiftKey) { highlightRevision(null); }" | 
 | 867 |             " if (event.ctrlKey) { highlight(null); }" | 
 | 868 |             " return false;") | 
 | 869 |     print 'xmlns="http://www.w3.org/2000/svg"' | 
 | 870 |     print 'xmlns:xlink="http://www.w3.org/1999/xlink">' | 
 | 871 |      | 
 | 872 |     print """ | 
 | 873 | <defs> | 
 | 874 |     <marker id="circleMark" | 
 | 875 |       viewBox="0 0 2 2" refX="1" refY="1" | 
 | 876 |       markerUnits="strokeWidth" | 
 | 877 |       markerWidth="2" markerHeight="2" | 
 | 878 |       orient="0"> | 
 | 879 |       <circle cx="1" cy="1" r="1"/> | 
 | 880 |     </marker> | 
 | 881 | </defs>""" | 
 | 882 |      | 
 | 883 |     #output the revisions | 
 | 884 |     print """ | 
 | 885 | <script type="text/javascript">//<![CDATA[ | 
 | 886 |     var previousRevision; | 
 | 887 |     var previousRevisionFill; | 
 | 888 |     var previousRevisionStroke | 
 | 889 |     function highlightRevision(id) { | 
 | 890 |         if (previousRevision == id) return; | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 891 |  | 
 | 892 |         document.getElementById('revision').firstChild.nodeValue = 'r' + id; | 
 | 893 |         document.getElementById('rev_link').setAttribute('xlink:href', | 
 | 894 |             'http://code.google.com/p/skia/source/detail?r=' + id); | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 895 |          | 
 | 896 |         var preRevision = document.getElementById(previousRevision); | 
 | 897 |         if (preRevision) { | 
 | 898 |             preRevision.setAttributeNS(null,'fill', previousRevisionFill); | 
 | 899 |             preRevision.setAttributeNS(null,'stroke', previousRevisionStroke); | 
 | 900 |         } | 
 | 901 |          | 
 | 902 |         var revision = document.getElementById(id); | 
 | 903 |         previousRevision = id; | 
 | 904 |         if (revision) { | 
 | 905 |             previousRevisionFill = revision.getAttributeNS(null,'fill'); | 
 | 906 |             revision.setAttributeNS(null,'fill','rgb(100%, 95%, 95%)'); | 
 | 907 |              | 
 | 908 |             previousRevisionStroke = revision.getAttributeNS(null,'stroke'); | 
 | 909 |             revision.setAttributeNS(null,'stroke','rgb(100%, 90%, 90%)'); | 
 | 910 |         } | 
 | 911 |     } | 
 | 912 | //]]></script>""" | 
 | 913 |      | 
 | 914 |     def print_rect(x, y, w, h, revision): | 
 | 915 |         """Outputs a revision rectangle in display space, | 
 | 916 |            taking arguments in revision space.""" | 
 | 917 |         disp_y = cy(y) | 
 | 918 |         disp_h = ch(h) | 
 | 919 |         if disp_h < 0: | 
 | 920 |             disp_y += disp_h | 
 | 921 |             disp_h = -disp_h | 
 | 922 |          | 
 | 923 |         print '<rect id=%s x=%s y=%s' % (qa(revision), qa(cx(x)), qa(disp_y),), | 
 | 924 |         print 'width=%s height=%s' % (qa(cw(w)), qa(disp_h),), | 
 | 925 |         print 'fill="white"', | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 926 |         print 'stroke="rgb(98%%,98%%,88%%)" stroke-width=%s' % qa(line_width), | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 927 |         print 'onmouseover=%s' % qa( | 
 | 928 |                 "var event = arguments[0] || window.event;" | 
 | 929 |                 " if (event.shiftKey) {" | 
 | 930 |                     " highlightRevision('"+str(revision)+"');" | 
 | 931 |                     " return false;" | 
 | 932 |                 " }"), | 
 | 933 |         print ' />' | 
 | 934 |      | 
 | 935 |     xes = set() | 
 | 936 |     for line in lines.itervalues(): | 
 | 937 |         for point in line: | 
 | 938 |             xes.add(point[0]) | 
 | 939 |     revisions = list(xes) | 
 | 940 |     revisions.sort() | 
 | 941 |      | 
 | 942 |     left = x | 
 | 943 |     current_revision = revisions[0] | 
 | 944 |     for next_revision in revisions[1:]: | 
 | 945 |         width = (((next_revision - current_revision) / 2.0) | 
 | 946 |                  + (current_revision - left)) | 
 | 947 |         print_rect(left, y, width, h, current_revision) | 
 | 948 |         left += width | 
 | 949 |         current_revision = next_revision | 
 | 950 |     print_rect(left, y, x+w - left, h, current_revision) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 951 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 952 |     #output the lines | 
 | 953 |     print """ | 
 | 954 | <script type="text/javascript">//<![CDATA[ | 
 | 955 |     var previous; | 
 | 956 |     var previousColor; | 
 | 957 |     var previousOpacity; | 
 | 958 |     function highlight(id) { | 
 | 959 |         if (previous == id) return; | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 960 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 961 |         document.getElementById('label').firstChild.nodeValue = id; | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 962 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 963 |         var preGroup = document.getElementById(previous); | 
 | 964 |         if (preGroup) { | 
 | 965 |             var preLine = document.getElementById(previous+'_line'); | 
 | 966 |             preLine.setAttributeNS(null,'stroke', previousColor); | 
 | 967 |             preLine.setAttributeNS(null,'opacity', previousOpacity); | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 968 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 969 |             var preSlope = document.getElementById(previous+'_linear'); | 
 | 970 |             if (preSlope) { | 
 | 971 |                 preSlope.setAttributeNS(null,'visibility', 'hidden'); | 
 | 972 |             } | 
 | 973 |         } | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 974 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 975 |         var group = document.getElementById(id); | 
 | 976 |         previous = id; | 
 | 977 |         if (group) { | 
 | 978 |             group.parentNode.appendChild(group); | 
 | 979 |              | 
 | 980 |             var line = document.getElementById(id+'_line'); | 
 | 981 |             previousColor = line.getAttributeNS(null,'stroke'); | 
 | 982 |             previousOpacity = line.getAttributeNS(null,'opacity'); | 
 | 983 |             line.setAttributeNS(null,'stroke', 'blue'); | 
 | 984 |             line.setAttributeNS(null,'opacity', '1'); | 
 | 985 |              | 
 | 986 |             var slope = document.getElementById(id+'_linear'); | 
 | 987 |             if (slope) { | 
 | 988 |                 slope.setAttributeNS(null,'visibility', 'visible'); | 
 | 989 |             } | 
 | 990 |         } | 
 | 991 |     } | 
 | 992 | //]]></script>""" | 
| epoger@google.com | 2459a39 | 2013-02-14 18:58:05 +0000 | [diff] [blame] | 993 |  | 
 | 994 |     # Add a new element to each item in the 'lines' list: the label in string | 
 | 995 |     # form.  Then use that element to sort the list. | 
 | 996 |     sorted_lines = [] | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 997 |     for label, line in lines.items(): | 
| epoger@google.com | 2459a39 | 2013-02-14 18:58:05 +0000 | [diff] [blame] | 998 |         sorted_lines.append([str(label), label, line]) | 
 | 999 |     sorted_lines.sort() | 
 | 1000 |  | 
 | 1001 |     for label_as_string, label, line in sorted_lines: | 
 | 1002 |         print '<g id=%s>' % qa(label_as_string) | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1003 |         r = 128 | 
 | 1004 |         g = 128 | 
 | 1005 |         b = 128 | 
 | 1006 |         a = .10 | 
 | 1007 |         if label in regressions: | 
 | 1008 |             regression = regressions[label] | 
 | 1009 |             min_slope = regression.find_min_slope() | 
 | 1010 |             if min_slope < 0: | 
 | 1011 |                 d = max(0, (min_slope / min_down_slope)) | 
 | 1012 |                 g += int(d*128) | 
 | 1013 |                 a += d*0.9 | 
 | 1014 |             elif min_slope > 0: | 
 | 1015 |                 d = max(0, (min_slope / max_up_slope)) | 
 | 1016 |                 r += int(d*128) | 
 | 1017 |                 a += d*0.9 | 
 | 1018 |              | 
 | 1019 |             slope = regression.slope | 
 | 1020 |             intercept = regression.intercept | 
 | 1021 |             min_x = regression.min_x | 
 | 1022 |             max_x = regression.max_x | 
 | 1023 |             print '<polyline id=%s' % qa(str(label)+'_linear'), | 
 | 1024 |             print 'fill="none" stroke="yellow"', | 
 | 1025 |             print 'stroke-width=%s' % qa(abs(ch(regression.serror*2))), | 
 | 1026 |             print 'opacity="0.5" pointer-events="none" visibility="hidden"', | 
 | 1027 |             print 'points="', | 
 | 1028 |             print '%s,%s' % (str(cx(min_x)), str(cy(slope*min_x + intercept))), | 
 | 1029 |             print '%s,%s' % (str(cx(max_x)), str(cy(slope*max_x + intercept))), | 
 | 1030 |             print '"/>' | 
 | 1031 |          | 
 | 1032 |         print '<polyline id=%s' % qa(str(label)+'_line'), | 
 | 1033 |         print 'onmouseover=%s' % qa( | 
 | 1034 |                 "var event = arguments[0] || window.event;" | 
 | 1035 |                 " if (event.ctrlKey) {" | 
 | 1036 |                     " highlight('"+str(label).replace("'", "\\'")+"');" | 
 | 1037 |                     " return false;" | 
 | 1038 |                 " }"), | 
 | 1039 |         print 'fill="none" stroke="rgb(%s,%s,%s)"' % (str(r), str(g), str(b)), | 
 | 1040 |         print 'stroke-width=%s' % qa(line_width), | 
 | 1041 |         print 'opacity=%s' % qa(a), | 
 | 1042 |         print 'points="', | 
 | 1043 |         for point in line: | 
 | 1044 |             print '%s,%s' % (str(cx(point[0])), str(cy(point[1]))), | 
 | 1045 |         print '"/>' | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 1046 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1047 |         print '</g>' | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 1048 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1049 |     #output the labels | 
 | 1050 |     print '<text id="label" x="0" y=%s' % qa(font_size), | 
 | 1051 |     print 'font-size=%s> </text>' % qa(font_size) | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 1052 |  | 
 | 1053 |     print '<a id="rev_link" xlink:href="" target="_top">' | 
 | 1054 |     print '<text id="revision" x="0" y=%s style="' % qa(font_size*2) | 
 | 1055 |     print 'font-size: %s; ' % qe(font_size) | 
 | 1056 |     print 'stroke: #0000dd; text-decoration: underline; ' | 
 | 1057 |     print '"> </text></a>' | 
 | 1058 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1059 |     print '</svg>' | 
| epoger@google.com | c71174d | 2011-08-08 17:19:23 +0000 | [diff] [blame] | 1060 |  | 
| bungeman@google.com | 85669f9 | 2011-06-17 13:58:14 +0000 | [diff] [blame] | 1061 | if __name__ == "__main__": | 
 | 1062 |     main() |