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 Type</td> 735 <td width="1">Bitmap Config</td> 736 <td width="1">Timer 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