blob: 2bf912d44b9ed426d948f34ebbe73edc38f00c0b [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import logging
import re
import time
from google.appengine.api import urlfetch
import webapp2
from base import bigquery
from base import constants
from common import buildbot
class Builds(webapp2.RequestHandler):
def get(self):
urlfetch.set_default_fetch_deadline(300)
bq = bigquery.BigQuery()
current_events = []
events = []
for master_name in constants.MASTER_NAMES:
builders = buildbot.Builders(master_name)
available_builds = _AvailableBuilds(builders)
recorded_builds = _RecordedBuilds(bq, builders, available_builds)
for builder in builders:
# Filter out recorded builds from available builds.
build_numbers = (available_builds[builder.name] -
recorded_builds[builder.name])
builder_current_events, builder_events = _TraceEventsForBuilder(
builder, build_numbers)
current_events += builder_current_events
events += builder_events
jobs = []
if current_events:
jobs += bq.InsertRowsAsync(
constants.DATASET, constants.CURRENT_BUILDS_TABLE,
current_events, truncate=True)
if events:
jobs += bq.InsertRowsAsync(constants.DATASET, constants.BUILDS_TABLE,
events)
for job in jobs:
bq.PollJob(job, 60 * 20) # 20 minutes.
def _AvailableBuilds(builders):
available_builds = {}
for builder in builders:
if not builder.cached_builds:
available_builds[builder.name] = frozenset()
continue
max_build = max(builder.cached_builds)
# Buildbot on tryserver.chromium.perf is occasionally including build 0 in
# its list of cached builds. That results in more builds than we want.
# Limit the list to the last 100 builds, because the urlfetch URL limit is
# 2048 bytes, and "&select=100000" * 100 is 1400 bytes.
builds = frozenset(build for build in builder.cached_builds
if build >= max_build - 100)
available_builds[builder.name] = builds
return available_builds
def _RecordedBuilds(bq, builders, available_builds):
# 105 days / 15 weeks. Must be some number greater than 100 days, because
# we request up to 100 builds (see above comment), and the slowest cron bots
# run one job every day.
start_time_ms = -1000 * 60 * 60 * 24 * 105
table = '%s.%s@%d-' % (constants.DATASET, constants.BUILDS_TABLE,
start_time_ms)
conditions = []
for builder in builders:
if not available_builds[builder.name]:
continue
max_build = max(available_builds[builder.name])
min_build = min(available_builds[builder.name])
conditions.append('WHEN builder = "%s" THEN build >= %d AND build <= %d' %
(builder.name, min_build, max_build))
query = (
'SELECT builder, build '
'FROM [%s] ' % table +
'WHERE CASE %s END ' % ' '.join(conditions) +
'GROUP BY builder, build'
)
query_result = bq.QuerySync(query, 600)
builds = collections.defaultdict(set)
for row in query_result:
builds[row['f'][0]['v']].add(int(row['f'][1]['v']))
return builds
def _TraceEventsForBuilder(builder, build_numbers):
if not build_numbers:
return (), ()
build_numbers_string = ', '.join(map(str, sorted(build_numbers)))
logging.info('Getting %s: %s', builder.name, build_numbers_string)
# Fetch build information and generate trace events.
current_events = []
events = []
builder_builds = builder.builds.Fetch(build_numbers)
query_time = time.time()
for build in builder_builds:
if build.complete:
events += _TraceEventsFromBuild(builder, build, query_time)
else:
current_events += _TraceEventsFromBuild(builder, build, query_time)
return current_events, events
def _TraceEventsFromBuild(builder, build, query_time):
match = re.match(r'(.+) \(([0-9]+)\)', builder.name)
if match:
configuration, host_shard = match.groups()
host_shard = int(host_shard)
else:
configuration = builder.name
host_shard = 0
# Build trace event.
if build.end_time:
build_end_time = build.end_time
else:
build_end_time = query_time
os, os_version, role = _ParseBuilderName(builder.master_name, builder.name)
yield {
'name': 'Build %d' % build.number,
'start_time': build.start_time,
'end_time': build_end_time,
'build': build.number,
'builder': builder.name,
'configuration': configuration,
'host_shard': host_shard,
'hostname': build.slave_name,
'master': builder.master_name,
'os': os,
'os_version': os_version,
'role': role,
'status': build.status,
'url': build.url,
}
# Step trace events.
for step in build.steps:
if not step.start_time:
continue
if step.name == 'steps':
continue
if step.end_time:
step_end_time = step.end_time
else:
step_end_time = query_time
yield {
'name': step.name,
'start_time': step.start_time,
'end_time': step_end_time,
'benchmark': step.name, # TODO(dtu): This isn't always right.
'build': build.number,
'builder': builder.name,
'configuration': configuration,
'host_shard': host_shard,
'hostname': build.slave_name,
'master': builder.master_name,
'os': os,
'os_version': os_version,
'role': role,
'status': step.status,
'url': step.url,
}
def _ParseBuilderName(master_name, builder_name):
if master_name == 'chromium.perf':
match = re.match(r'^([A-Za-z]+)(?: ([0-9\.]+|XP))?([A-Za-z0-9-\. ]+)? '
r'(Builder|Perf)(?: \([0-9]+\))?$', builder_name).groups()
os = match[0]
if match[1]:
os_version = match[1]
else:
os_version = None
if match[3] == 'Builder':
role = 'builder'
elif match[3] == 'Perf':
role = 'tester'
else:
raise NotImplementedError()
elif master_name == 'client.catapult':
match = re.match(r'^Catapult(?: ([A-Za-z])+)? ([A-Za-z]+)$',
builder_name).groups()
os = match[1]
os_version = None
role = match[0]
if not role:
role = 'tester'
elif master_name == 'tryserver.chromium.perf':
match = re.match(r'^(android|linux|mac|win).*_([a-z]+)$',
builder_name).groups()
os = match[0]
os_version = None
role = match[1]
elif master_name == 'tryserver.client.catapult':
match = re.match(r'^Catapult(?: (Android|Linux|Mac|Windows))? ([A-Za-z]+)$',
builder_name).groups()
os = match[0]
os_version = None
role = match[1]
else:
raise NotImplementedError()
if os:
os = os.lower()
if os == 'windows':
os = 'win'
if os_version:
os_version = os_version.lower()
role = role.lower()
return (os, os_version, role)