普通文本  |  236行  |  6.89 KB

# 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)