• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
8This script connects via HTTP to a buildbot master in order to scrape and parse
9perf data from Chrome Endure tests that have been run.  The perf data is then
10stored in local text files to be graphed by the Chrome Endure graphing code.
11
12It is assumed that any Chrome Endure tests that show up on the waterfall have
13names that are of the following form:
14
15"endure_<webapp_name>-<test_name>" (non-Web Page Replay tests)
16
17or
18
19"endure_<webapp_name>_wpr-<test_name>" (Web Page Replay tests)
20
21For example: "endure_gmail_wpr-testGmailComposeDiscard"
22
23This script accepts either a URL or a local path as a buildbot location.
24It switches its behavior if a URL is given, or a local path is given.
25
26When a URL is given, it gets buildbot logs from the buildbot builders URL
27e.g. http://build.chromium.org/p/chromium.endure/builders/.
28
29When a local path is given, it gets buildbot logs from buildbot's internal
30files in the directory e.g. /home/chrome-bot/buildbot.
31"""
32
33import cPickle
34import getpass
35import logging
36import optparse
37import os
38import re
39import simplejson
40import socket
41import string
42import sys
43import time
44import urllib
45import urllib2
46
47
48CHROME_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
61BUILDER_URL_BASE = 'http://build.chromium.org/p/chromium.endure/builders/'
62LAST_BUILD_NUM_PROCESSED_FILE = os.path.join(os.path.dirname(__file__),
63                                             '_parser_last_processed.txt')
64LOCAL_GRAPH_DIR = '/home/%s/www/chrome_endure_clean' % getpass.getuser()
65MANGLE_TRANSLATION = string.maketrans(' ()', '___')
66
67def 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
102def 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
131def 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
204def 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
237def 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
358def 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
367def 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
420def 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
456def 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
519def 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
565def 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
610def 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
689def 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
791def 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
840if __name__ == '__main__':
841  main()
842