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