| Index: tools/perf/dashboard/buildbot.py
|
| diff --git a/tools/perf/dashboard/buildbot.py b/tools/perf/dashboard/buildbot.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..db33cd83f7c677440471730fc607535ab92c8fad
|
| --- /dev/null
|
| +++ b/tools/perf/dashboard/buildbot.py
|
| @@ -0,0 +1,438 @@
|
| +import collections
|
| +import cPickle
|
| +import json
|
| +import logging
|
| +import os
|
| +import re
|
| +import socket
|
| +import time
|
| +import urllib
|
| +import urllib2
|
| +
|
| +
|
| +PENDING = None
|
| +SUCCESS = 0
|
| +WARNING = 1
|
| +FAILURE = 2
|
| +EXCEPTION = 4
|
| +SLAVE_LOST = 5
|
| +
|
| +
|
| +BASE_URL = 'http://build.chromium.org/p'
|
| +CACHE_FILE_NAME = 'cache.dat'
|
| +
|
| +
|
| +StackTraceLine = collections.namedtuple(
|
| + 'StackTraceLine', ('file', 'function', 'line', 'source'))
|
| +
|
| +
|
| +def _FetchData(master, url):
|
| + url = '%s/%s/json/%s' % (BASE_URL, master, url)
|
| + try:
|
| + logging.info('Retrieving ' + url)
|
| + return json.load(urllib2.urlopen(url))
|
| + except (urllib2.HTTPError, socket.error):
|
| + # Could be intermittent; try again.
|
| + try:
|
| + return json.load(urllib2.urlopen(url))
|
| + except (urllib2.HTTPError, socket.error):
|
| + logging.error('Error retrieving URL ' + url)
|
| + raise
|
| + except:
|
| + logging.error('Error retrieving URL ' + url)
|
| + raise
|
| +
|
| +
|
| +def Builders(master):
|
| + builders = {}
|
| +
|
| + # Load builders from cache file.
|
| + if os.path.exists(master):
|
| + start_time = time.time()
|
| + for builder_name in os.listdir(master):
|
| + cache_file_path = os.path.join(master, builder_name, CACHE_FILE_NAME)
|
| + if os.path.exists(cache_file_path):
|
| + with open(cache_file_path, 'rb') as cache_file:
|
| + try:
|
| + builders[builder_name] = cPickle.load(cache_file)
|
| + except EOFError:
|
| + logging.error('File is corrupted: %s', cache_file_path)
|
| + raise
|
| + logging.info('Loaded builder caches in %0.2f seconds.',
|
| + time.time() - start_time)
|
| +
|
| + return builders
|
| +
|
| +
|
| +def Update(master, builders):
|
| + # Update builders with latest information.
|
| + builder_data = _FetchData(master, 'builders')
|
| + for builder_name, builder_info in builder_data.iteritems():
|
| + if builder_name in builders:
|
| + builders[builder_name].Update(builder_info)
|
| + else:
|
| + builders[builder_name] = Builder(master, builder_name, builder_info)
|
| +
|
| + return builders
|
| +
|
| +
|
| +class Builder(object):
|
| + def __init__(self, master, name, data):
|
| + self._master = master
|
| + self._name = name
|
| +
|
| + self.Update(data)
|
| +
|
| + self._builds = {}
|
| +
|
| + def __setstate__(self, state):
|
| + self.__dict__ = state # pylint: disable=attribute-defined-outside-init
|
| + if not hasattr(self, '_builds'):
|
| + self._builds = {}
|
| +
|
| + def __lt__(self, other):
|
| + return self.name < other.name
|
| +
|
| + def __str__(self):
|
| + return self.name
|
| +
|
| + def __getitem__(self, key):
|
| + if not isinstance(key, int):
|
| + raise TypeError('build numbers must be integers, not %s' %
|
| + type(key).__name__)
|
| +
|
| + self._FetchBuilds(key)
|
| + return self._builds[key]
|
| +
|
| + def _FetchBuilds(self, *build_numbers):
|
| + """Download build details, if not already cached.
|
| +
|
| + Returns:
|
| + A tuple of downloaded build numbers.
|
| + """
|
| + build_numbers = tuple(build_number for build_number in build_numbers
|
| + if not (build_number in self._builds and
|
| + self._builds[build_number].complete))
|
| + if not build_numbers:
|
| + return ()
|
| +
|
| + for build_number in build_numbers:
|
| + if build_number < 0:
|
| + raise ValueError('Invalid build number: %d' % build_number)
|
| +
|
| + build_query = urllib.urlencode(
|
| + [('select', build) for build in build_numbers])
|
| + url = 'builders/%s/builds/?%s' % (urllib.quote(self.name), build_query)
|
| + builds = _FetchData(self.master, url)
|
| + for build_info in builds.itervalues():
|
| + self._builds[build_info['number']] = Build(self.master, build_info)
|
| +
|
| + self._Cache()
|
| +
|
| + return build_numbers
|
| +
|
| + def FetchRecentBuilds(self, number_of_builds):
|
| + min_build = max(self.last_build - number_of_builds, -1)
|
| + return self._FetchBuilds(*xrange(self.last_build, min_build, -1))
|
| +
|
| + def Update(self, data=None):
|
| + if not data:
|
| + data = _FetchData(self.master, 'builders/%s' % urllib.quote(self.name))
|
| + self._state = data['state']
|
| + self._pending_build_count = data['pendingBuilds']
|
| + self._current_builds = tuple(data['currentBuilds'])
|
| + self._cached_builds = tuple(data['cachedBuilds'])
|
| + self._slaves = tuple(data['slaves'])
|
| +
|
| + self._Cache()
|
| +
|
| + def _Cache(self):
|
| + cache_dir_path = os.path.join(self.master, self.name)
|
| + if not os.path.exists(cache_dir_path):
|
| + os.makedirs(cache_dir_path)
|
| + cache_file_path = os.path.join(cache_dir_path, CACHE_FILE_NAME)
|
| + with open(cache_file_path, 'wb') as cache_file:
|
| + cPickle.dump(self, cache_file, -1)
|
| +
|
| + def LastBuilds(self, count):
|
| + min_build = max(self.last_build - count, -1)
|
| + for build_number in xrange(self.last_build, min_build, -1):
|
| + yield self._builds[build_number]
|
| +
|
| + @property
|
| + def master(self):
|
| + return self._master
|
| +
|
| + @property
|
| + def name(self):
|
| + return self._name
|
| +
|
| + @property
|
| + def state(self):
|
| + return self._state
|
| +
|
| + @property
|
| + def pending_build_count(self):
|
| + return self._pending_build_count
|
| +
|
| + @property
|
| + def current_builds(self):
|
| + """List of build numbers currently building.
|
| +
|
| + There may be multiple entries if there are multiple build slaves."""
|
| + return self._current_builds
|
| +
|
| + @property
|
| + def cached_builds(self):
|
| + """Builds whose data are visible on the master in increasing order.
|
| +
|
| + More builds may be available than this."""
|
| + return self._cached_builds
|
| +
|
| + @property
|
| + def last_build(self):
|
| + """Last completed build."""
|
| + for build_number in reversed(self.cached_builds):
|
| + if build_number not in self.current_builds:
|
| + return build_number
|
| + return None
|
| +
|
| + @property
|
| + def slaves(self):
|
| + return self._slaves
|
| +
|
| +
|
| +class Build(object):
|
| + def __init__(self, master, data):
|
| + self._master = master
|
| + self._builder_name = data['builderName']
|
| + self._number = data['number']
|
| + self._complete = not ('currentStep' in data and data['currentStep'])
|
| + self._start_time, self._end_time = data['times']
|
| +
|
| + self._steps = {step_info['name']:
|
| + Step(self._master, self._builder_name, self._number, step_info)
|
| + for step_info in data['steps']}
|
| +
|
| + def __str__(self):
|
| + return str(self.number)
|
| +
|
| + def __lt__(self, other):
|
| + return self.number < other.number
|
| +
|
| + @property
|
| + def builder_name(self):
|
| + return self._builder_name
|
| +
|
| + @property
|
| + def number(self):
|
| + return self._number
|
| +
|
| + @property
|
| + def complete(self):
|
| + return self._complete
|
| +
|
| + @property
|
| + def start_time(self):
|
| + return self._start_time
|
| +
|
| + @property
|
| + def end_time(self):
|
| + return self._end_time
|
| +
|
| + @property
|
| + def steps(self):
|
| + return self._steps
|
| +
|
| +
|
| +def _ParseTraceFromLog(log):
|
| + """Search the log for a stack trace and return a structured representation.
|
| +
|
| + This function supports both default Python-style stacks and Telemetry-style
|
| + stacks. It returns the first stack trace found in the log - sometimes a bug
|
| + leads to a cascade of failures, so the first one is usually the root cause.
|
| + """
|
| + log_iterator = iter(log.splitlines())
|
| + for line in log_iterator:
|
| + if line == 'Traceback (most recent call last):':
|
| + break
|
| + else:
|
| + return (None, None)
|
| +
|
| + stack_trace = []
|
| + while True:
|
| + line = log_iterator.next()
|
| + match1 = re.match(r'\s*File "(?P<file>.+)", line (?P<line>[0-9]+), '
|
| + 'in (?P<function>.+)', line)
|
| + match2 = re.match(r'\s*(?P<function>.+) at '
|
| + '(?P<file>.+):(?P<line>[0-9]+)', line)
|
| + match = match1 or match2
|
| + if not match:
|
| + exception = line
|
| + break
|
| + trace_line = match.groupdict()
|
| + # Use the base name, because the path will be different
|
| + # across platforms and configurations.
|
| + file_base_name = trace_line['file'].split('/')[-1].split('\\')[-1]
|
| + source = log_iterator.next().strip()
|
| + stack_trace.append(StackTraceLine(
|
| + file_base_name, trace_line['function'], trace_line['line'], source))
|
| +
|
| + return tuple(stack_trace), exception
|
| +
|
| +
|
| +class Step(object):
|
| + def __init__(self, master, builder_name, build_number, data):
|
| + self._master = master
|
| + self._builder_name = builder_name
|
| + self._build_number = build_number
|
| + self._name = data['name']
|
| + self._result = data['results'][0]
|
| + self._start_time, self._end_time = data['times']
|
| +
|
| + self._log_link = None
|
| + self._results_link = None
|
| + for link_name, link_url in data['logs']:
|
| + if link_name == 'stdio':
|
| + self._log_link = link_url + '/text'
|
| + elif link_name == 'json.output':
|
| + self._results_link = link_url + '/text'
|
| +
|
| + self._log = None
|
| + self._results = None
|
| + self._stack_trace = None
|
| +
|
| + def __getstate__(self):
|
| + return {
|
| + '_master': self._master,
|
| + '_builder_name': self._builder_name,
|
| + '_build_number': self._build_number,
|
| + '_name': self._name,
|
| + '_result': self._result,
|
| + '_start_time': self._start_time,
|
| + '_end_time': self._end_time,
|
| + '_log_link': self._log_link,
|
| + '_results_link': self._results_link,
|
| + }
|
| +
|
| + def __setstate__(self, state):
|
| + self.__dict__ = state # pylint: disable=attribute-defined-outside-init
|
| + self._log = None
|
| + self._results = None
|
| + self._stack_trace = None
|
| +
|
| + def __str__(self):
|
| + return self.name
|
| +
|
| + @property
|
| + def name(self):
|
| + return self._name
|
| +
|
| + @property
|
| + def result(self):
|
| + return self._result
|
| +
|
| + @property
|
| + def start_time(self):
|
| + return self._start_time
|
| +
|
| + @property
|
| + def end_time(self):
|
| + return self._end_time
|
| +
|
| + @property
|
| + def log_link(self):
|
| + return self._log_link
|
| +
|
| + @property
|
| + def results_link(self):
|
| + return self._results_link
|
| +
|
| + @property
|
| + def log(self):
|
| + if self._log is None:
|
| + if not self.log_link:
|
| + return None
|
| + cache_file_path = os.path.join(
|
| + self._master, self._builder_name,
|
| + str(self._build_number), self._name, 'log')
|
| + if os.path.exists(cache_file_path):
|
| + # Load cache file, if it exists.
|
| + with open(cache_file_path, 'rb') as cache_file:
|
| + self._log = cache_file.read()
|
| + else:
|
| + # Otherwise, download it.
|
| + logging.info('Retrieving ' + self.log_link)
|
| + try:
|
| + data = urllib2.urlopen(self.log_link).read()
|
| + except (urllib2.HTTPError, socket.error):
|
| + # Could be intermittent; try again.
|
| + try:
|
| + data = urllib2.urlopen(self.log_link).read()
|
| + except (urllib2.HTTPError, socket.error):
|
| + logging.error('Error retrieving URL ' + self.log_link)
|
| + raise
|
| + except:
|
| + logging.error('Error retrieving URL ' + self.log_link)
|
| + raise
|
| + # And cache the newly downloaded data.
|
| + cache_dir_path = os.path.dirname(cache_file_path)
|
| + if not os.path.exists(cache_dir_path):
|
| + os.makedirs(cache_dir_path)
|
| + with open(cache_file_path, 'wb') as cache_file:
|
| + cache_file.write(data)
|
| + self._log = data
|
| + return self._log
|
| +
|
| + @property
|
| + def results(self):
|
| + if self._results is None:
|
| + if not self.results_link:
|
| + return None
|
| + cache_file_path = os.path.join(
|
| + self._master, self._builder_name,
|
| + str(self._build_number), self._name, 'results')
|
| + if os.path.exists(cache_file_path):
|
| + # Load cache file, if it exists.
|
| + try:
|
| + with open(cache_file_path, 'rb') as cache_file:
|
| + self._results = cPickle.load(cache_file)
|
| + except EOFError:
|
| + os.remove(cache_file_path)
|
| + return self.results
|
| + else:
|
| + # Otherwise, download it.
|
| + logging.info('Retrieving ' + self.results_link)
|
| + try:
|
| + data = json.load(urllib2.urlopen(self.results_link))
|
| + except (urllib2.HTTPError, socket.error):
|
| + # Could be intermittent; try again.
|
| + try:
|
| + data = json.load(urllib2.urlopen(self.results_link))
|
| + except (urllib2.HTTPError, socket.error):
|
| + logging.error('Error retrieving URL ' + self.results_link)
|
| + raise
|
| + except ValueError:
|
| + # If the build had an exception, the results might not be valid.
|
| + data = None
|
| + except:
|
| + logging.error('Error retrieving URL ' + self.results_link)
|
| + raise
|
| + # And cache the newly downloaded data.
|
| + cache_dir_path = os.path.dirname(cache_file_path)
|
| + if not os.path.exists(cache_dir_path):
|
| + os.makedirs(cache_dir_path)
|
| + with open(cache_file_path, 'wb') as cache_file:
|
| + cPickle.dump(data, cache_file, -1)
|
| + self._results = data
|
| + return self._results
|
| +
|
| + @property
|
| + def stack_trace(self):
|
| + if self._stack_trace is None:
|
| + self._stack_trace = _ParseTraceFromLog(self.log)
|
| + return self._stack_trace
|
| +
|
| + @property
|
| + def chrome_stack_trace(self):
|
| + raise NotImplementedError()
|
|
|