blob: a73e712e2ffa4d6a9e83f34864c90bc911970162 [file]
# Copyright 2021 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.
"""Buildbot recipe definition for the various Crashpad continuous builders."""
import datetime
from recipe_engine import post_process
from PB.recipe_engine import result as result_pb2
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb
from dataclasses import dataclass
from recipe_engine.recipe_api import RecipeScriptApi
from recipe_engine.recipe_test_api import RecipeTestApi
from RECIPE_MODULES.build import chromium, chromium_checkout
from RECIPE_MODULES.depot_tools import gclient, gsutil
from RECIPE_MODULES.infra import zip as zip_module
from RECIPE_MODULES.recipe_engine import (
buildbucket,
cipd,
context,
file,
futures,
json,
path,
platform,
runtime,
step,
time,
)
@dataclass
class DEPS(RecipeScriptApi):
buildbucket: buildbucket.API
chromium: chromium.API
chromium_checkout: chromium_checkout.API
cipd: cipd.API
context: context.API
file: file.API
futures: futures.API
gclient: gclient.API
gsutil: gsutil.API
json: json.API
path: path.API
platform: platform.API
runtime: runtime.API
step: step.API
time: time.API
zip: zip_module.API
@dataclass
class TEST_DEPS(RecipeTestApi):
buildbucket: buildbucket.TEST_API
chromium_checkout: chromium_checkout.TEST_API
cipd: cipd.TEST_API
file: file.TEST_API
gclient: gclient.TEST_API
json: json.TEST_API
path: path.TEST_API
platform: platform.TEST_API
runtime: runtime.TEST_API
step: step.TEST_API
time: time.TEST_API
MODEL_CIPD_PREFIX = 'chromium/rts/model/'
RTS_EXEC_CIPD_PREFIX = 'chromium/rts/rts-chromium/'
PLATFORMS = [
'linux-amd64',
'linux-arm64',
'mac-amd64',
'mac-arm64',
'windows-amd64',
'windows-arm64',
]
REJECTION_DATA_WINDOW = datetime.timedelta(weeks=12)
TEST_DURATION_DATA_WINDOW = datetime.timedelta(weeks=1)
# Processing 10% of 1w-worth test durations takes 7h on a 32-core bot.
TEST_DURATION_DATA_PERCENTAGE = 1
def RunSteps(api: DEPS):
# Start bot_update in background.
checkout_dir_fut = api.futures.spawn_immediate(checkout_chromium, api)
# Install rts-chromium executables.
exec_pkg_paths = install_rts_executables(api)
assert api.platform.is_linux
assert api.platform.arch == 'intel'
assert api.platform.bits == 64
exec_path = exec_pkg_paths['linux-amd64'].joinpath('rts-chromium')
# Fetch the dataset.
# Ignore today because we might fetch incomplete data.
yesterday = api.time.utcnow().date() - datetime.timedelta(days=1)
rejections_dir, durations_dir = _fetch_model_data(
api,
exec_path,
rejection_date_range=(
yesterday - REJECTION_DATA_WINDOW,
yesterday,
),
duration_date_range=(
yesterday - TEST_DURATION_DATA_WINDOW,
yesterday,
),
)
# Create the model.
checkout_dir = checkout_dir_fut.result()
model_dir = api.path.cleanup_dir / 'rts-chromium-model'
api.step(
'create-model',
[
exec_path,
'create-model',
'-checkout',
str(checkout_dir),
'-rejections',
str(rejections_dir),
'-durations',
str(durations_dir),
'-model-dir',
str(model_dir),
'-log-furthest',
'30',
],
)
# Upload the training data for inspection if we need to diagnose the model
archive_training_data(api)
step_result = run_integration_tests(api, exec_path, model_dir, checkout_dir)
# Still create the cipd package so we can inspect it if the build failed
# but don't move the ref to latest so broken models don't end up in builds
should_update_ref = step_result.presentation.status == api.step.SUCCESS
# TODO(crbug.com/1172372): ensure the new model is not significantly worse
# than the current one.
# Create CIPD packages.
futures = []
with api.step.nest('Create CIPD packages'):
for platform, pkg_dir in exec_pkg_paths.items():
futures.append(
api.futures.spawn_immediate(
create_cipd_package,
api,
platform,
pkg_dir,
model_dir,
should_update_ref,
)
)
# Check success.
for f in futures:
f.result()
return result_pb2.RawResult(
status=step_result.presentation.status,
summary_markdown=compose_build_summary(api, model_dir),
)
def archive_training_data(api: DEPS):
zip_out = api.path.cleanup_dir.joinpath(
'rts-chromium-model-data', 'rts-chromium-model-data.zip'
)
api.zip.directory(
'zip model dir', api.path.cleanup_dir / 'rts-chromium-model-data', zip_out
)
api.gsutil.upload(
zip_out,
'chrome-rts',
api.path.join('rts-chromium-model-data', api.buildbucket.build.number),
raise_on_failure=False,
use_retry_wrapper=False,
name='archive model data',
)
def create_cipd_package(
api: DEPS, platform, exec_pkg_dir, model_dir, update_ref
):
with api.step.nest('Upload CIPD package - %s' % platform):
# Copy the model files, such that create_cipd_package can be called for
# different platforms concurrently.
# Note that we add a platform-specific executable below.
pkg_dir = api.path.cleanup_dir.joinpath('model-pkg-dir', platform)
api.file.copytree('Copy the model files', model_dir, pkg_dir)
# Add the executable to the model.
exe_ext = '.exe' if platform.startswith('windows-') else ''
exe_base_name = 'rts-chromium' + exe_ext
api.file.copy(
'Include the executable',
exec_pkg_dir / exe_base_name,
pkg_dir / exe_base_name,
)
# Upload to CIPD.
pkg = api.cipd.PackageDefinition(
package_name=MODEL_CIPD_PREFIX + platform,
package_root=pkg_dir,
)
pkg.add_dir(pkg_dir)
pin = api.cipd.create_from_pkg(
pkg,
tags={
'build': 'https://ci.chromium.org/b/%d' % api.buildbucket.build.id,
},
)
# Skip the ref update if it is an experimental build.
if not api.runtime.is_experimental and update_ref: # pragma: no branch
api.cipd.set_ref(pin.package, version=pin.instance_id, refs=['latest'])
def install_rts_executables(api: DEPS):
"""Installs rts-chromium executables for all platforms.
Returns:
Mapping {platform: package_path}, where
- platform is a CIPD platform, e.g. "linux-amd64".
- package_path is a api.path.Path object.
"""
ver = pick_executable_version(api)
install_dir = api.path.cleanup_dir / 'rts-chromium'
ret = {}
ensure_file = api.cipd.EnsureFile()
for plat in PLATFORMS:
ensure_file.add_package(
name=RTS_EXEC_CIPD_PREFIX + plat,
version=ver,
subdir=plat,
)
ret[plat] = install_dir / plat
api.cipd.ensure(install_dir, ensure_file, name='install RTS executables')
return ret
def pick_executable_version(api: DEPS) -> str:
"""Returns the CIPD version of rts-executable CIPD packages to use.
Waits until the latest Windows revision is available on all platforms.
"""
# Find the git_revision of the latest windows-amd64 package.
# Using the git revision guarantees that packages for all platforms are built
# from the same source code.
# Use Windows because it seems to be slower to update than Mac now,
# so its latest version is more likely to exist on all platforms.
descr = api.cipd.describe(
RTS_EXEC_CIPD_PREFIX + 'windows-amd64',
'latest',
test_data_tags=['git_revision:c1ee0e03d15281730ebedf1f7151474f7a523001'],
)
# Pick any git_revision.
# It doesn't matter which one if they produced the same instance hash.
ver = None
for t in descr.tags:
if t.tag.startswith('git_revision:'):
ver = t.tag
break
if not ver:
raise api.step.StepFailure(
'git_revision tag not found in windows-amd64 latest'
)
max_attempts = 10
sleep_seconds = 60
for attempt in range(max_attempts):
# Check if this specific version exists on all other platforms.
all_present = True
for plat in PLATFORMS:
if plat == 'windows-amd64':
continue
try:
api.cipd.describe(
RTS_EXEC_CIPD_PREFIX + plat,
ver,
test_data_tags=[
'git_revision:c1ee0e03d15281730ebedf1f7151474f7a523001'
],
)
except api.step.StepFailure:
all_present = False
break
if all_present:
return ver
if attempt < max_attempts - 1:
api.time.sleep(sleep_seconds)
raise api.step.StepFailure(
'Timed out waiting for version %s to appear on all platforms' % ver
)
def checkout_chromium(api: DEPS):
"""Checks out chromium/src and returns its path."""
api.gclient.set_config('chromium_empty')
api.chromium_checkout.ensure_checkout()
return api.chromium_checkout.source_dir
def _fetch_model_data(
api: DEPS, exec_path, rejection_date_range, duration_date_range
):
"""Fetches the data for model creation.
Returns:
A tuple (rejections_dir, durations_dir) with path to the directories with
rejections and durations respectively.
"""
data_dir = api.path.cleanup_dir / 'rts-chromium-model-data'
rejections_dir = data_dir / 'rejections'
durations_dir = data_dir / 'durations'
futures = api.futures.wait(
[
api.futures.spawn_immediate(
api.step,
'fetch rejections (fast)',
[
str(exec_path),
'fetch-rejections-fast',
'-out',
str(rejections_dir),
]
+ _date_range_flags(rejection_date_range),
),
api.futures.spawn_immediate(
api.step,
'fetch durations',
[
str(exec_path),
'fetch-durations',
'-frac',
'%.3f' % (TEST_DURATION_DATA_PERCENTAGE / 100.0),
'-out',
str(durations_dir),
]
+ _date_range_flags(duration_date_range),
),
]
)
# Check future's exception.
for f in futures:
f.result()
return rejections_dir, durations_dir
def _date_range_flags(date_range):
from_date, to_date = date_range
return [
'-from',
from_date.strftime('%Y-%m-%d'),
'-to',
to_date.strftime('%Y-%m-%d'),
]
def compose_build_summary(api: DEPS, model_dir):
cfg = api.file.read_json(
'read thresholds',
model_dir.joinpath('git-file-graph', 'config.json'),
test_data={
'thresholds': [
{
'changeRecall': '0.97',
'savings': '0.5',
'maxDistance': '50',
},
{
'changeRecall': '0.98',
'savings': '0.4',
'maxDistance': '60',
},
{
'changeRecall': '0.99',
'savings': '0.3',
'maxDistance': '70',
},
{
'changeRecall': '1.00',
'savings': '0.2',
'maxDistance': '80',
},
]
},
)
# Format of the file is GitBasedStrategyConfig in
# https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/rts/cmd/rts-chromium/rts-chromium.proto
# Note: this is essentially HTML, not plaintext, so whitespace is collapsed.
lines = ['ChangeRecall | Savings | Max distance']
lines += [
'%.4f | %.2f | %.2f'
% (
float(th.get('changeRecall', '0')),
float(th.get('savings', '0')),
float(th.get('maxDistance', '0')),
)
for th in cfg['thresholds'] # note: thresholds are already sorted
if float(th.get('changeRecall', '0')) >= 0.9
]
return '<br>'.join(lines)
def run_integration_tests(api: DEPS, exec_path, model_dir, checkout_dir):
return api.step(
'integration_tests',
[
'python3',
api.resource('integration_tests.py'),
'--rts-exec',
exec_path,
'--model-dir',
model_dir,
'--chromium-checkout',
checkout_dir,
],
raise_on_failure=False,
)
def GenTests(api: TEST_DEPS):
linux_amd64 = (
api.platform.name('linux')
+ api.platform.arch('intel')
+ api.platform.bits(64)
)
yield api.test('basic') + linux_amd64
yield (
api.test('missing_git_revision', status='FAILURE')
+ linux_amd64
+ api.step_data(
'cipd describe chromium/rts/rts-chromium/windows-amd64',
api.json.output(
{
'result': {
'pin': {
'instance_id': 'resolved-instance_id-of-latest----------',
'package': 'chromium/rts/rts-chromium/windows-amd64',
},
'refs': [
{
'instance_id': 'resolved-instance_id-of-latest----------',
'modified_by': 'user:44-blablbla@developer.gserviceaccount.com',
'modified_ts': 1446574210,
'ref': 'latest',
}
],
'registered_by': 'user:44-blablbla@developer.gserviceaccount.com',
'registered_ts': 1446574210,
'tags': [
{
'registered_by': 'user:44-blablbla@developer.gserviceaccount.com',
'registered_ts': 1446574210,
'tag': 'some_other_tag',
}
],
}
}
),
)
+ api.post_process(post_process.DropExpectation)
)
timeout_test = api.test('timeout', status='FAILURE') + linux_amd64
for i in range(10):
suffix = f' ({i + 1})' if i > 0 else ''
timeout_test += api.step_data(
f'cipd describe chromium/rts/rts-chromium/linux-amd64{suffix}', retcode=1
)
yield timeout_test + api.post_process(post_process.DropExpectation)