1 #!/usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Script to parse perf data from Chrome Endure test executions, to be graphed. 7 8 This script connects via HTTP to a buildbot master in order to scrape and parse 9 perf data from Chrome Endure tests that have been run. The perf data is then 10 stored in local text files to be graphed by the Chrome Endure graphing code. 11 12 It is assumed that any Chrome Endure tests that show up on the waterfall have 13 names that are of the following form: 14 15 "endure_<webapp_name>-<test_name>" (non-Web Page Replay tests) 16 17 or 18 19 "endure_<webapp_name>_wpr-<test_name>" (Web Page Replay tests) 20 21 For example: "endure_gmail_wpr-testGmailComposeDiscard" 22 23 This script accepts either a URL or a local path as a buildbot location. 24 It switches its behavior if a URL is given, or a local path is given. 25 26 When a URL is given, it gets buildbot logs from the buildbot builders URL 27 e.g. http://build.chromium.org/p/chromium.endure/builders/. 28 29 When a local path is given, it gets buildbot logs from buildbot's internal 30 files in the directory e.g. /home/chrome-bot/buildbot. 31 """ 32 33 import cPickle 34 import getpass 35 import logging 36 import optparse 37 import os 38 import re 39 import simplejson 40 import socket 41 import string 42 import sys 43 import time 44 import urllib 45 import urllib2 46 47 48 CHROME_ENDURE_SLAVE_NAMES = [ 49 'Linux QA Perf (0)', 50 'Linux QA Perf (1)', 51 'Linux QA Perf (2)', 52 'Linux QA Perf (3)', 53 'Linux QA Perf (4)', 54 'Linux QA Perf (dbg)(0)', 55 'Linux QA Perf (dbg)(1)', 56 'Linux QA Perf (dbg)(2)', 57 'Linux QA Perf (dbg)(3)', 58 'Linux QA Perf (dbg)(4)', 59 ] 60 61 BUILDER_URL_BASE = 'http://build.chromium.org/p/chromium.endure/builders/' 62 LAST_BUILD_NUM_PROCESSED_FILE = os.path.join(os.path.dirname(__file__), 63 '_parser_last_processed.txt') 64 LOCAL_GRAPH_DIR = '/home/%s/www/chrome_endure_clean' % getpass.getuser() 65 MANGLE_TRANSLATION = string.maketrans(' ()', '___') 66 67 def SetupBaseGraphDirIfNeeded(webapp_name, test_name, dest_dir): 68 """Sets up the directory containing results for a particular test, if needed. 69 70 Args: 71 webapp_name: The string name of the webapp associated with the given test. 72 test_name: The string name of the test. 73 dest_dir: The name of the destination directory that needs to be set up. 74 """ 75 if not os.path.exists(dest_dir): 76 os.mkdir(dest_dir) # Test name directory. 77 os.chmod(dest_dir, 0755) 78 79 # Create config file. 80 config_file = os.path.join(dest_dir, 'config.js') 81 if not os.path.exists(config_file): 82 with open(config_file, 'w') as f: 83 f.write('var Config = {\n') 84 f.write('buildslave: "Chrome Endure Bots",\n') 85 f.write('title: "Chrome Endure %s Test: %s",\n' % (webapp_name.upper(), 86 test_name)) 87 f.write('};\n') 88 os.chmod(config_file, 0755) 89 90 # Set up symbolic links to the real graphing files. 91 link_file = os.path.join(dest_dir, 'index.html') 92 if not os.path.exists(link_file): 93 os.symlink('../../endure_plotter.html', link_file) 94 link_file = os.path.join(dest_dir, 'endure_plotter.js') 95 if not os.path.exists(link_file): 96 os.symlink('../../endure_plotter.js', link_file) 97 link_file = os.path.join(dest_dir, 'js') 98 if not os.path.exists(link_file): 99 os.symlink('../../js', link_file) 100 101 102 def WriteToDataFile(new_line, existing_lines, revision, data_file): 103 """Writes a new entry to an existing perf data file to be graphed. 104 105 If there's an existing line with the same revision number, overwrite its data 106 with the new line. Else, prepend the info for the new revision. 107 108 Args: 109 new_line: A dictionary representing perf information for the new entry. 110 existing_lines: A list of string lines from the existing perf data file. 111 revision: The string revision number associated with the new perf entry. 112 data_file: The string name of the perf data file to which to write. 113 """ 114 overwritten = False 115 for i, line in enumerate(existing_lines): 116 line_dict = simplejson.loads(line) 117 if line_dict['rev'] == revision: 118 existing_lines[i] = simplejson.dumps(new_line) 119 overwritten = True 120 break 121 elif int(line_dict['rev']) < int(revision): 122 break 123 if not overwritten: 124 existing_lines.insert(0, simplejson.dumps(new_line)) 125 126 with open(data_file, 'w') as f: 127 f.write('\n'.join(existing_lines)) 128 os.chmod(data_file, 0755) 129 130 131 def OutputPerfData(revision, graph_name, values, units, units_x, dest_dir, 132 is_stacked=False, stack_order=[]): 133 """Outputs perf data to a local text file to be graphed. 134 135 Args: 136 revision: The string revision number associated with the perf data. 137 graph_name: The string name of the graph on which to plot the data. 138 values: A dict which maps a description to a value. A value is either a 139 single data value to be graphed, or a list of 2-tuples 140 representing (x, y) points to be graphed for long-running tests. 141 units: The string description for the y-axis units on the graph. 142 units_x: The string description for the x-axis units on the graph. Should 143 be set to None if the results are not for long-running graphs. 144 dest_dir: The name of the destination directory to which to write. 145 is_stacked: True to draw a "stacked" graph. First-come values are 146 stacked at bottom by default. 147 stack_order: A list that contains key strings in the order to stack values 148 in the graph. 149 """ 150 # Update graphs.dat, which contains metadata associated with each graph. 151 existing_graphs = [] 152 graphs_file = os.path.join(dest_dir, 'graphs.dat') 153 if os.path.exists(graphs_file): 154 with open(graphs_file, 'r') as f: 155 existing_graphs = simplejson.loads(f.read()) 156 is_new_graph = True 157 for graph in existing_graphs: 158 if graph['name'] == graph_name: 159 is_new_graph = False 160 break 161 if is_new_graph: 162 new_graph = { 163 'name': graph_name, 164 'units': units, 165 'important': False, 166 } 167 if units_x: 168 new_graph['units_x'] = units_x 169 existing_graphs.append(new_graph) 170 existing_graphs = sorted(existing_graphs, key=lambda x: x['name']) 171 with open(graphs_file, 'w') as f: 172 f.write(simplejson.dumps(existing_graphs, indent=2)) 173 os.chmod(graphs_file, 0755) 174 175 # Update summary data file, containing the actual data to be graphed. 176 data_file_name = graph_name + '-summary.dat' 177 existing_lines = [] 178 data_file = os.path.join(dest_dir, data_file_name) 179 if os.path.exists(data_file): 180 with open(data_file, 'r') as f: 181 existing_lines = f.readlines() 182 existing_lines = map(lambda x: x.strip(), existing_lines) 183 new_traces = {} 184 for description in values: 185 value = values[description] 186 if units_x: 187 points = [] 188 for point in value: 189 points.append([str(point[0]), str(point[1])]) 190 new_traces[description] = points 191 else: 192 new_traces[description] = [str(value), str(0.0)] 193 new_line = { 194 'traces': new_traces, 195 'rev': revision 196 } 197 if is_stacked: 198 new_line['stack'] = True 199 new_line['stack_order'] = stack_order 200 201 WriteToDataFile(new_line, existing_lines, revision, data_file) 202 203 204 def OutputEventData(revision, event_dict, dest_dir): 205 """Outputs event data to a local text file to be graphed. 206 207 Args: 208 revision: The string revision number associated with the event data. 209 event_dict: A dict which maps a description to an array of tuples 210 representing event data to be graphed. 211 dest_dir: The name of the destination directory to which to write. 212 """ 213 data_file_name = '_EVENT_-summary.dat' 214 existing_lines = [] 215 data_file = os.path.join(dest_dir, data_file_name) 216 if os.path.exists(data_file): 217 with open(data_file, 'r') as f: 218 existing_lines = f.readlines() 219 existing_lines = map(lambda x: x.strip(), existing_lines) 220 221 new_events = {} 222 for description in event_dict: 223 event_list = event_dict[description] 224 value_list = [] 225 for event_time, event_data in event_list: 226 value_list.append([str(event_time), event_data]) 227 new_events[description] = value_list 228 229 new_line = { 230 'rev': revision, 231 'events': new_events 232 } 233 234 WriteToDataFile(new_line, existing_lines, revision, data_file) 235 236 237 def UpdatePerfDataFromFetchedContent( 238 revision, content, webapp_name, test_name, graph_dir, only_dmp=False): 239 """Update perf data from fetched stdio data. 240 241 Args: 242 revision: The string revision number associated with the new perf entry. 243 content: Fetched stdio data. 244 webapp_name: A name of the webapp. 245 test_name: A name of the test. 246 graph_dir: A path to the graph directory. 247 only_dmp: True if only Deep Memory Profiler results should be used. 248 """ 249 perf_data_raw = [] 250 251 def AppendRawPerfData(graph_name, description, value, units, units_x, 252 webapp_name, test_name, is_stacked=False): 253 perf_data_raw.append({ 254 'graph_name': graph_name, 255 'description': description, 256 'value': value, 257 'units': units, 258 'units_x': units_x, 259 'webapp_name': webapp_name, 260 'test_name': test_name, 261 'stack': is_stacked, 262 }) 263 264 # First scan for short-running perf test results. 265 for match in re.findall( 266 r'RESULT ([^:]+): ([^=]+)= ([-\d\.]+) (\S+)', content): 267 if (not only_dmp) or match[0].endswith('-DMP'): 268 try: 269 match2 = eval(match[2]) 270 except SyntaxError: 271 match2 = None 272 if match2: 273 AppendRawPerfData(match[0], match[1], match2, match[3], None, 274 webapp_name, webapp_name) 275 276 # Next scan for long-running perf test results. 277 for match in re.findall( 278 r'RESULT ([^:]+): ([^=]+)= (\[[^\]]+\]) (\S+) (\S+)', content): 279 if (not only_dmp) or match[0].endswith('-DMP'): 280 try: 281 match2 = eval(match[2]) 282 except SyntaxError: 283 match2 = None 284 # TODO(dmikurube): Change the condition to use stacked graph when we 285 # determine how to specify it. 286 if match2: 287 AppendRawPerfData(match[0], match[1], match2, match[3], match[4], 288 webapp_name, test_name, match[0].endswith('-DMP')) 289 290 # Next scan for events in the test results. 291 for match in re.findall( 292 r'RESULT _EVENT_: ([^=]+)= (\[[^\]]+\])', content): 293 try: 294 match1 = eval(match[1]) 295 except SyntaxError: 296 match1 = None 297 if match1: 298 AppendRawPerfData('_EVENT_', match[0], match1, None, None, 299 webapp_name, test_name) 300 301 # For each graph_name/description pair that refers to a long-running test 302 # result or an event, concatenate all the results together (assume results 303 # in the input file are in the correct order). For short-running test 304 # results, keep just one if more than one is specified. 305 perf_data = {} # Maps a graph-line key to a perf data dictionary. 306 for data in perf_data_raw: 307 key_graph = data['graph_name'] 308 key_description = data['description'] 309 if not key_graph in perf_data: 310 perf_data[key_graph] = { 311 'graph_name': data['graph_name'], 312 'value': {}, 313 'units': data['units'], 314 'units_x': data['units_x'], 315 'webapp_name': data['webapp_name'], 316 'test_name': data['test_name'], 317 } 318 perf_data[key_graph]['stack'] = data['stack'] 319 if 'stack_order' not in perf_data[key_graph]: 320 perf_data[key_graph]['stack_order'] = [] 321 if (data['stack'] and 322 data['description'] not in perf_data[key_graph]['stack_order']): 323 perf_data[key_graph]['stack_order'].append(data['description']) 324 325 if data['graph_name'] != '_EVENT_' and not data['units_x']: 326 # Short-running test result. 327 perf_data[key_graph]['value'][key_description] = data['value'] 328 else: 329 # Long-running test result or event. 330 if key_description in perf_data[key_graph]['value']: 331 perf_data[key_graph]['value'][key_description] += data['value'] 332 else: 333 perf_data[key_graph]['value'][key_description] = data['value'] 334 335 # Finally, for each graph-line in |perf_data|, update the associated local 336 # graph data files if necessary. 337 for perf_data_key in perf_data: 338 perf_data_dict = perf_data[perf_data_key] 339 340 dest_dir = os.path.join(graph_dir, perf_data_dict['webapp_name']) 341 if not os.path.exists(dest_dir): 342 os.mkdir(dest_dir) # Webapp name directory. 343 os.chmod(dest_dir, 0755) 344 dest_dir = os.path.join(dest_dir, perf_data_dict['test_name']) 345 346 SetupBaseGraphDirIfNeeded(perf_data_dict['webapp_name'], 347 perf_data_dict['test_name'], dest_dir) 348 if perf_data_dict['graph_name'] == '_EVENT_': 349 OutputEventData(revision, perf_data_dict['value'], dest_dir) 350 else: 351 OutputPerfData(revision, perf_data_dict['graph_name'], 352 perf_data_dict['value'], 353 perf_data_dict['units'], perf_data_dict['units_x'], 354 dest_dir, 355 perf_data_dict['stack'], perf_data_dict['stack_order']) 356 357 358 def SlaveLocation(master_location, slave_info): 359 """Returns slave location for |master_location| and |slave_info|.""" 360 if master_location.startswith('http://'): 361 return master_location + urllib.quote(slave_info['slave_name']) 362 else: 363 return os.path.join(master_location, 364 slave_info['slave_name'].translate(MANGLE_TRANSLATION)) 365 366 367 def GetRevisionAndLogs(slave_location, build_num): 368 """Get a revision number and log locations. 369 370 Args: 371 slave_location: A URL or a path to the build slave data. 372 build_num: A build number. 373 374 Returns: 375 A pair of the revision number and a list of strings that contain locations 376 of logs. (False, []) in case of error. 377 """ 378 if slave_location.startswith('http://'): 379 location = slave_location + '/builds/' + str(build_num) 380 else: 381 location = os.path.join(slave_location, str(build_num)) 382 383 revision = False 384 logs = [] 385 fp = None 386 try: 387 if location.startswith('http://'): 388 fp = urllib2.urlopen(location) 389 contents = fp.read() 390 revisions = re.findall(r'<td class="left">got_revision</td>\s+' 391 '<td>(\d+)</td>\s+<td>Source</td>', contents) 392 if revisions: 393 revision = revisions[0] 394 logs = [location + link + '/text' for link 395 in re.findall(r'(/steps/endure[^/]+/logs/stdio)', contents)] 396 else: 397 fp = open(location, 'rb') 398 build = cPickle.load(fp) 399 properties = build.getProperties() 400 if properties.has_key('got_revision'): 401 revision = build.getProperty('got_revision') 402 candidates = os.listdir(slave_location) 403 logs = [os.path.join(slave_location, filename) 404 for filename in candidates 405 if re.match(r'%d-log-endure[^/]+-stdio' % build_num, filename)] 406 407 except urllib2.URLError, e: 408 logging.exception('Error reading build URL "%s": %s', location, str(e)) 409 return False, [] 410 except (IOError, OSError), e: 411 logging.exception('Error reading build file "%s": %s', location, str(e)) 412 return False, [] 413 finally: 414 if fp: 415 fp.close() 416 417 return revision, logs 418 419 420 def ExtractTestNames(log_location, is_dbg): 421 """Extract test names from |log_location|. 422 423 Returns: 424 A dict of a log location, webapp's name and test's name. False if error. 425 """ 426 if log_location.startswith('http://'): 427 location = urllib.unquote(log_location) 428 test_pattern = r'endure_([^_]+)(_test |-)([^/]+)/' 429 wpr_test_pattern = r'endure_([^_]+)_wpr(_test |-)([^/]+)/' 430 else: 431 location = log_location 432 test_pattern = r'endure_([^_]+)(_test_|-)([^/]+)-stdio' 433 wpr_test_pattern = 'endure_([^_]+)_wpr(_test_|-)([^/]+)-stdio' 434 435 found_wpr_result = False 436 match = re.findall(test_pattern, location) 437 if not match: 438 match = re.findall(wpr_test_pattern, location) 439 if match: 440 found_wpr_result = True 441 else: 442 logging.error('Test name not in expected format: ' + location) 443 return False 444 match = match[0] 445 webapp_name = match[0] + '_wpr' if found_wpr_result else match[0] 446 webapp_name = webapp_name + '_dbg' if is_dbg else webapp_name 447 test_name = match[2] 448 449 return { 450 'location': log_location, 451 'webapp_name': webapp_name, 452 'test_name': test_name, 453 } 454 455 456 def GetStdioContents(stdio_location): 457 """Gets appropriate stdio contents. 458 459 Returns: 460 A content string of the stdio log. None in case of error. 461 """ 462 fp = None 463 contents = '' 464 try: 465 if stdio_location.startswith('http://'): 466 fp = urllib2.urlopen(stdio_location, timeout=60) 467 # Since in-progress test output is sent chunked, there's no EOF. We need 468 # to specially handle this case so we don't hang here waiting for the 469 # test to complete. 470 start_time = time.time() 471 while True: 472 data = fp.read(1024) 473 if not data: 474 break 475 contents += data 476 if time.time() - start_time >= 30: # Read for at most 30 seconds. 477 break 478 else: 479 fp = open(stdio_location) 480 data = fp.read() 481 contents = '' 482 index = 0 483 484 # Buildbot log files are stored in the netstring format. 485 # http://en.wikipedia.org/wiki/Netstring 486 while index < len(data): 487 index2 = index 488 while data[index2].isdigit(): 489 index2 += 1 490 if data[index2] != ':': 491 logging.error('Log file is not in expected format: %s' % 492 stdio_location) 493 contents = None 494 break 495 length = int(data[index:index2]) 496 index = index2 + 1 497 channel = int(data[index]) 498 index += 1 499 if data[index+length-1] != ',': 500 logging.error('Log file is not in expected format: %s' % 501 stdio_location) 502 contents = None 503 break 504 if channel == 0: 505 contents += data[index:(index+length-1)] 506 index += length 507 508 except (urllib2.URLError, socket.error, IOError, OSError), e: 509 # Issue warning but continue to the next stdio link. 510 logging.warning('Error reading test stdio data "%s": %s', 511 stdio_location, str(e)) 512 finally: 513 if fp: 514 fp.close() 515 516 return contents 517 518 519 def UpdatePerfDataForSlaveAndBuild( 520 slave_info, build_num, graph_dir, master_location): 521 """Process updated perf data for a particular slave and build number. 522 523 Args: 524 slave_info: A dictionary containing information about the slave to process. 525 build_num: The particular build number on the slave to process. 526 graph_dir: A path to the graph directory. 527 master_location: A URL or a path to the build master data. 528 529 Returns: 530 True if the perf data for the given slave/build is updated properly, or 531 False if any critical error occurred. 532 """ 533 if not master_location.startswith('http://'): 534 # Source is a file. 535 from buildbot.status import builder 536 537 slave_location = SlaveLocation(master_location, slave_info) 538 logging.debug(' %s, build %d.', slave_info['slave_name'], build_num) 539 is_dbg = '(dbg)' in slave_info['slave_name'] 540 541 revision, logs = GetRevisionAndLogs(slave_location, build_num) 542 if not revision: 543 return False 544 545 stdios = [] 546 for log_location in logs: 547 stdio = ExtractTestNames(log_location, is_dbg) 548 if not stdio: 549 return False 550 stdios.append(stdio) 551 552 for stdio in stdios: 553 stdio_location = stdio['location'] 554 contents = GetStdioContents(stdio_location) 555 556 if contents: 557 UpdatePerfDataFromFetchedContent(revision, contents, 558 stdio['webapp_name'], 559 stdio['test_name'], 560 graph_dir, is_dbg) 561 562 return True 563 564 565 def GetMostRecentBuildNum(master_location, slave_name): 566 """Gets the most recent buld number for |slave_name| in |master_location|.""" 567 most_recent_build_num = None 568 569 if master_location.startswith('http://'): 570 slave_url = master_location + urllib.quote(slave_name) 571 572 url_contents = '' 573 fp = None 574 try: 575 fp = urllib2.urlopen(slave_url, timeout=60) 576 url_contents = fp.read() 577 except urllib2.URLError, e: 578 logging.exception('Error reading builder URL: %s', str(e)) 579 return None 580 finally: 581 if fp: 582 fp.close() 583 584 matches = re.findall(r'/(\d+)/stop', url_contents) 585 if matches: 586 most_recent_build_num = int(matches[0]) 587 else: 588 matches = re.findall(r'#(\d+)</a></td>', url_contents) 589 if matches: 590 most_recent_build_num = sorted(map(int, matches), reverse=True)[0] 591 592 else: 593 slave_path = os.path.join(master_location, 594 slave_name.translate(MANGLE_TRANSLATION)) 595 files = os.listdir(slave_path) 596 number_files = [int(filename) for filename in files if filename.isdigit()] 597 if number_files: 598 most_recent_build_num = sorted(number_files, reverse=True)[0] 599 600 if most_recent_build_num: 601 logging.debug('%s most recent build number: %s', 602 slave_name, most_recent_build_num) 603 else: 604 logging.error('Could not identify latest build number for slave %s.', 605 slave_name) 606 607 return most_recent_build_num 608 609 610 def UpdatePerfDataFiles(graph_dir, master_location): 611 """Updates the Chrome Endure graph data files with the latest test results. 612 613 For each known Chrome Endure slave, we scan its latest test results looking 614 for any new test data. Any new data that is found is then appended to the 615 data files used to display the Chrome Endure graphs. 616 617 Args: 618 graph_dir: A path to the graph directory. 619 master_location: A URL or a path to the build master data. 620 621 Returns: 622 True if all graph data files are updated properly, or 623 False if any error occurred. 624 """ 625 slave_list = [] 626 for slave_name in CHROME_ENDURE_SLAVE_NAMES: 627 slave_info = {} 628 slave_info['slave_name'] = slave_name 629 slave_info['most_recent_build_num'] = None 630 slave_info['last_processed_build_num'] = None 631 slave_list.append(slave_info) 632 633 # Identify the most recent build number for each slave. 634 logging.debug('Searching for latest build numbers for each slave...') 635 for slave in slave_list: 636 slave_name = slave['slave_name'] 637 slave['most_recent_build_num'] = GetMostRecentBuildNum( 638 master_location, slave_name) 639 640 # Identify the last-processed build number for each slave. 641 logging.debug('Identifying last processed build numbers...') 642 if not os.path.exists(LAST_BUILD_NUM_PROCESSED_FILE): 643 for slave_info in slave_list: 644 slave_info['last_processed_build_num'] = 0 645 else: 646 with open(LAST_BUILD_NUM_PROCESSED_FILE, 'r') as fp: 647 file_contents = fp.read() 648 for match in re.findall(r'([^:]+):(\d+)', file_contents): 649 slave_name = match[0].strip() 650 last_processed_build_num = match[1].strip() 651 for slave_info in slave_list: 652 if slave_info['slave_name'] == slave_name: 653 slave_info['last_processed_build_num'] = int( 654 last_processed_build_num) 655 for slave_info in slave_list: 656 if not slave_info['last_processed_build_num']: 657 slave_info['last_processed_build_num'] = 0 658 logging.debug('Done identifying last processed build numbers.') 659 660 # For each Chrome Endure slave, process each build in-between the last 661 # processed build num and the most recent build num, inclusive. To process 662 # each one, first get the revision number for that build, then scan the test 663 # result stdio for any performance data, and add any new performance data to 664 # local files to be graphed. 665 for slave_info in slave_list: 666 logging.debug('Processing %s, builds %d-%d...', 667 slave_info['slave_name'], 668 slave_info['last_processed_build_num'], 669 slave_info['most_recent_build_num']) 670 curr_build_num = slave_info['last_processed_build_num'] 671 while curr_build_num <= slave_info['most_recent_build_num']: 672 if not UpdatePerfDataForSlaveAndBuild(slave_info, curr_build_num, 673 graph_dir, master_location): 674 # Do not give up. The first files might be removed by buildbot. 675 logging.warning('Logs do not exist in buildbot for #%d of %s.' % 676 (curr_build_num, slave_info['slave_name'])) 677 curr_build_num += 1 678 679 # Log the newly-processed build numbers. 680 logging.debug('Logging the newly-processed build numbers...') 681 with open(LAST_BUILD_NUM_PROCESSED_FILE, 'w') as f: 682 for slave_info in slave_list: 683 f.write('%s:%s\n' % (slave_info['slave_name'], 684 slave_info['most_recent_build_num'])) 685 686 return True 687 688 689 def GenerateIndexPage(graph_dir): 690 """Generates a summary (landing) page for the Chrome Endure graphs. 691 692 Args: 693 graph_dir: A path to the graph directory. 694 """ 695 logging.debug('Generating new index.html page...') 696 697 # Page header. 698 page = """ 699 <html> 700 701 <head> 702 <title>Chrome Endure Overview</title> 703 <script language="javascript"> 704 function DisplayGraph(name, graph) { 705 document.write( 706 '<td><iframe scrolling="no" height="438" width="700" src="'); 707 document.write(name); 708 document.write('"></iframe></td>'); 709 } 710 </script> 711 </head> 712 713 <body> 714 <center> 715 716 <h1> 717 Chrome Endure 718 </h1> 719 """ 720 # Print current time. 721 page += '<p>Updated: %s</p>\n' % ( 722 time.strftime('%A, %B %d, %Y at %I:%M:%S %p %Z')) 723 724 # Links for each webapp. 725 webapp_names = [x for x in os.listdir(graph_dir) if 726 x not in ['js', 'old_data', '.svn', '.git'] and 727 os.path.isdir(os.path.join(graph_dir, x))] 728 webapp_names = sorted(webapp_names) 729 730 page += '<p> [' 731 for i, name in enumerate(webapp_names): 732 page += '<a href="#%s">%s</a>' % (name.upper(), name.upper()) 733 if i < len(webapp_names) - 1: 734 page += ' | ' 735 page += '] </p>\n' 736 737 # Print out the data for each webapp. 738 for webapp_name in webapp_names: 739 page += '\n<h1 id="%s">%s</h1>\n' % (webapp_name.upper(), 740 webapp_name.upper()) 741 742 # Links for each test for this webapp. 743 test_names = [x for x in 744 os.listdir(os.path.join(graph_dir, webapp_name))] 745 test_names = sorted(test_names) 746 747 page += '<p> [' 748 for i, name in enumerate(test_names): 749 page += '<a href="#%s">%s</a>' % (name, name) 750 if i < len(test_names) - 1: 751 page += ' | ' 752 page += '] </p>\n' 753 754 # Print out the data for each test for this webapp. 755 for test_name in test_names: 756 # Get the set of graph names for this test. 757 graph_names = [x[:x.find('-summary.dat')] for x in 758 os.listdir(os.path.join(graph_dir, 759 webapp_name, test_name)) 760 if '-summary.dat' in x and '_EVENT_' not in x] 761 graph_names = sorted(graph_names) 762 763 page += '<h2 id="%s">%s</h2>\n' % (test_name, test_name) 764 page += '<table>\n' 765 766 for i, graph_name in enumerate(graph_names): 767 if i % 2 == 0: 768 page += ' <tr>\n' 769 page += (' <script>DisplayGraph("%s/%s?graph=%s&lookout=1");' 770 '</script>\n' % (webapp_name, test_name, graph_name)) 771 if i % 2 == 1: 772 page += ' </tr>\n' 773 if len(graph_names) % 2 == 1: 774 page += ' </tr>\n' 775 page += '</table>\n' 776 777 # Page footer. 778 page += """ 779 </center> 780 </body> 781 782 </html> 783 """ 784 785 index_file = os.path.join(graph_dir, 'index.html') 786 with open(index_file, 'w') as f: 787 f.write(page) 788 os.chmod(index_file, 0755) 789 790 791 def main(): 792 parser = optparse.OptionParser() 793 parser.add_option( 794 '-v', '--verbose', action='store_true', default=False, 795 help='Use verbose logging.') 796 parser.add_option( 797 '-s', '--stdin', action='store_true', default=False, 798 help='Input from stdin instead of slaves for testing this script.') 799 parser.add_option( 800 '-b', '--buildbot', dest='buildbot', metavar="BUILDBOT", 801 default=BUILDER_URL_BASE, 802 help='Use log files in a buildbot at BUILDBOT. BUILDBOT can be a ' 803 'buildbot\'s builder URL or a local path to a buildbot directory. ' 804 'Both an absolute path and a relative path are available, e.g. ' 805 '"/home/chrome-bot/buildbot" or "../buildbot". ' 806 '[default: %default]') 807 parser.add_option( 808 '-g', '--graph', dest='graph_dir', metavar="DIR", default=LOCAL_GRAPH_DIR, 809 help='Output graph data files to DIR. [default: %default]') 810 options, _ = parser.parse_args(sys.argv) 811 812 logging_level = logging.DEBUG if options.verbose else logging.INFO 813 logging.basicConfig(level=logging_level, 814 format='[%(asctime)s] %(levelname)s: %(message)s') 815 816 if options.stdin: 817 content = sys.stdin.read() 818 UpdatePerfDataFromFetchedContent( 819 '12345', content, 'webapp', 'test', options.graph_dir) 820 else: 821 if options.buildbot.startswith('http://'): 822 master_location = options.buildbot 823 else: 824 build_dir = os.path.join(options.buildbot, 'build') 825 third_party_dir = os.path.join(build_dir, 'third_party') 826 sys.path.append(third_party_dir) 827 sys.path.append(os.path.join(third_party_dir, 'buildbot_8_4p1')) 828 sys.path.append(os.path.join(third_party_dir, 'twisted_10_2')) 829 master_location = os.path.join(build_dir, 'masters', 830 'master.chromium.endure') 831 success = UpdatePerfDataFiles(options.graph_dir, master_location) 832 if not success: 833 logging.error('Failed to update perf data files.') 834 sys.exit(0) 835 836 GenerateIndexPage(options.graph_dir) 837 logging.debug('All done!') 838 839 840 if __name__ == '__main__': 841 main() 842