blob: b80173a6ac00478f91957dde89ce2aa51fe71c9f [file]
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import dataclasses
import datetime
import math
import textwrap
from typing import Any, Self
from recipe_engine import post_process, config_types
from recipe_engine.recipe_api import RecipeApi
from PB.recipes.build.chromium.autosharder import InputProperties
from PB.go.chromium.org.luci.buildbucket.proto import common as common_pb
from PB.recipe_engine import result as result_pb2
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 (
depot_tools,
gclient,
gerrit,
git,
git_cl,
)
from RECIPE_MODULES.recipe_engine import (
buildbucket,
context,
file,
json,
led,
path,
properties,
raw_io,
step,
time,
)
@dataclass
class DEPS(RecipeScriptApi):
buildbucket: buildbucket.API
chromium: chromium.API
chromium_checkout: chromium_checkout.API
context: context.API
depot_tools: depot_tools.API
file: file.API
gclient: gclient.API
gerrit: gerrit.API
git: git.API
git_cl: git_cl.API
json: json.API
led: led.API
path: path.API
properties: properties.API
raw_io: raw_io.API
step: step.API
time: time.API
@dataclass
class TEST_DEPS(RecipeTestApi):
chromium_checkout: chromium_checkout.TEST_API
context: context.TEST_API
file: file.TEST_API
gerrit: gerrit.TEST_API
git: git.TEST_API
json: json.TEST_API
led: led.TEST_API
path: path.TEST_API
properties: properties.TEST_API
raw_io: raw_io.TEST_API
step: step.TEST_API
time: time.TEST_API
COMMIT_MESSAGE = """
Autoshard chromium/src test suites
This is an automated CL generated by the autosharder.
Optimal shard counts for test suites are calculated by querying recent
runtimes.
The build that created this CL was https://ci.chromium.org/b/{}
Ignore-Freeze:True
"""
_CLOUD_PROJECT_ID = 'chrome-trooper-analytics'
# Minimum number of non-experimental samples to allow a suite to be
# resharded
MIN_SAMPLE_SIZE = 1500
# The percentile that must meet the target runtime
DEFAULT_PERCENTILE = 80
# TODO(crbug.com/40281184): Replace with queried, per-suite overheads, once
# infra is set up to support automated overhead measurements.
# See go/nplus1shardsproposal
DEFAULT_OVERHEAD_MIN = 1.0
ANDROID_OVERHEAD_MIN = 2.0
# Clamp values for test overheads.
MIN_OVERHEAD_MIN = 0.2
MAX_OVERHEAD_MIN = 4.0
PROPERTIES = InputProperties
@dataclasses.dataclass(kw_only=True)
class Sharding:
test_suite: str
try_builder: str
waterfall_builder_group: str
waterfall_builder_name: str
shard_count: int
runtime: float | None
builds_per_hour: int | None = None
optimal_shard_count: int | None = None
_overhead: float | None = None
@property
def id(self) -> str:
return f'{self.try_builder}:{self.test_suite}'
@property
def overhead(self) -> float:
if self._overhead:
return self._overhead
if 'android' in self.try_builder:
return ANDROID_OVERHEAD_MIN
return DEFAULT_OVERHEAD_MIN
@overhead.setter
def overhead(self, value: float):
# Suites can be in a bad sharding. Since we only use one set of shards
# (n and n+1) this can create a bad value for the overhead so clamp it
# to reasonable values. At the time of writing this the min and max are
# around 0.21 and 4.01. Ideally we could use more than one set of
# shardings to determine this overhead.
self._overhead = max(min(value, MAX_OVERHEAD_MIN), MIN_OVERHEAD_MIN)
def calculate_optimal_shard_count(
self,
api: DEPS,
target_runtime: float,
):
optimal_shard_count = (
self.shard_count * (self.runtime - self.overhead)
) / (target_runtime - self.overhead)
# When the overhead is poorly measured it can lead to the shard
# count being < 1 (e.g. the default android overhead is used
# on a suite taking < 2 min). In these cases force count to 1.
integer_optimal_shard_count = max(math.ceil(optimal_shard_count), 1)
api.step.empty(
'optimal shard calculation',
step_text=textwrap.dedent(f"""Calculating optimal shards:
(current_shard_count * (current_runtime - overhead)) / (target_runtime - overhead))
current_runtime = {self.runtime}
current_shard_count = {self.shard_count}
target_runtime = {target_runtime}
overhead = {self.overhead}
optimal_shard_count = {optimal_shard_count} ~= {integer_optimal_shard_count}"""),
)
self.optimal_shard_count = integer_optimal_shard_count
self._check_runtime(target_runtime)
self._emit_cost_step(api)
return self.optimal_shard_count
def _check_runtime(self, target_runtime: float) -> None:
# Double check we have successfully sharded below the desired runtime.
# This should be impossible as adding shards should only reduce runtimes
overhead_change = (
self.optimal_shard_count - self.shard_count
) * self.overhead
simulated_max_shard_duration = (
self.runtime * self.shard_count + overhead_change
) / self.optimal_shard_count
assert simulated_max_shard_duration <= target_runtime or math.isclose(
simulated_max_shard_duration, target_runtime
), (
f'Simulated runtime {simulated_max_shard_duration} is greater '
f'than the desired runtime {target_runtime}'
)
def _emit_cost_step(self, api: DEPS):
# Display estimated_bot_hour_cost and avg_num_builds_per_peak_hour
estimated_bot_hour_cost = round(
(self.optimal_shard_count - self.shard_count)
* (self.overhead / 60.0)
* self.builds_per_hour,
2,
)
api.step.empty(
'estimated_cost',
step_text=f'Estimated change in bot hours: {estimated_bot_hour_cost}',
)
def check_valid_sharding(self, api: DEPS, old_sharding: Self) -> bool:
if old_sharding and self.shard_count != int(old_sharding.shard_count):
# This row is for an old shard count that is no longer being used.
api.step.empty(
f'Skipping because it is not using the configured sharding ({old_sharding})'
)
return False
if not old_sharding and self.optimal_shard_count < self.shard_count:
# Do not reduce shard counts for tests that have never been autosharded.
# Only increase these.
api.step.empty('Skipping non-autosharded suite with a higher shard count')
return False
if self.optimal_shard_count == self.shard_count:
api.step.empty('Skipping already optimized sharding')
return False
if not old_sharding and self.shard_count == 1:
api.step.empty('Skipping unverified shardable suite')
return False
return True
def RunSteps(api: RecipeApi, properties: InputProperties):
"""Creates a CL to update test suite shards in chromium/src"""
api.gclient.set_config('chromium')
api.chromium.set_config('chromium')
api.chromium_checkout.ensure_checkout()
maybe_result = _check_run_conditions(api)
if maybe_result:
return maybe_result
_setup_git(api)
percentile = properties.percentile or DEFAULT_PERCENTILE
_calculate_optimal_shards(
api,
properties.target_runtime,
percentile,
properties.exclude_test_suites,
properties.exclude_builders,
properties.exclude_builder_test_suites,
)
def step_test_data():
return api.raw_io.test_api.stream_output_text(
'diff --git a/infra/config/autoshard_exceptions.json'
' b/infra/config/autoshard_exceptions.json'
)
diff_step = api.git(
'diff', stdout=api.raw_io.output_text(), step_test_data=step_test_data
)
diff_step.presentation.logs['stdout'] = diff_step.stdout
if not diff_step.stdout:
return
with api.step.nest('regenerate targets specs'):
src_dir = api.chromium_checkout.source_dir
api.step(
'//testing/buildbot',
['vpython3', src_dir / 'testing/buildbot/generate_buildbot_json.py'],
)
api.step('//infra/config prod', [src_dir / 'infra/config/main.star'])
if not api.led.led_build:
commit_message = COMMIT_MESSAGE.format(api.buildbucket.build.id)
api.git('commit', '-a', '-m', 'Autoshard test suites')
upload_args = [
'--cq-dry-run',
'--bypass-hooks',
'--enable-auto-submit',
'-r',
'rubber-stamper@appspot.gserviceaccount.com',
'--cc',
','.join(
[
'gatong@google.com',
'sshrimp@google.com',
'chrome-dev-infra-auto+reviews@google.com',
]
),
'--send-email',
]
with api.context(cwd=api.chromium_checkout.source_dir):
api.git_cl.upload(commit_message, upload_args, name='git cl upload')
api.git_cl(
'issue',
['--json', api.json.output()],
name='git cl issue',
)
def _calculate_optimal_shards(
api: DEPS,
target_runtime: float,
percentile: int,
exclude_suites: list[str],
exclude_builders: list[str],
exclude_builder_suites: dict[str, list[str]],
) -> None:
api.step('which bq', ['which', 'bq'])
today = api.time.utcnow()
start = today - datetime.timedelta(days=14)
lookback_start_date = start.strftime('%Y-%m-%d')
lookback_end_date = today.strftime('%Y-%m-%d')
shardings = _query_durations(
api,
target_runtime,
percentile,
lookback_start_date,
lookback_end_date,
exclude_suites,
exclude_builders,
exclude_builder_suites,
)
cq_builders = _query_cq_builders(api)
_query_overheads(
api,
shardings,
lookback_start_date,
lookback_end_date,
)
_query_avg_builds_per_hour(
api,
shardings,
lookback_start_date,
lookback_end_date,
)
old_shardings, exceptions_file = _load_shardings_file(api)
shardings = _calculate_optimal_shard_counts(
api,
target_runtime,
shardings,
old_shardings,
)
shardings = [
sharding for sharding in shardings if sharding.try_builder in cq_builders
]
_save_shardings_file(
api,
shardings,
exceptions_file,
)
def _load_shardings_file(api: DEPS):
exceptions_file = api.chromium_checkout.source_dir.joinpath(
'infra', 'config', 'autoshard_exceptions.json'
)
exceptions_json = api.file.read_json(
'read current exceptions file', exceptions_file, test_data={}
)
return [
Sharding(
test_suite=test_suite,
try_builder=shard_info['try_builder'],
waterfall_builder_group=group,
waterfall_builder_name=ci_builder,
optimal_shard_count=shard_info['shards'],
shard_count=shard_info['shards'],
runtime=None,
)
for group, ci_builders in exceptions_json.items()
for ci_builder, test_suites in ci_builders.items()
for test_suite, shard_info in test_suites.items()
], exceptions_file
def _save_shardings_file(
api: DEPS, shardings: list[Sharding], exceptions_file: config_types.Path
):
# Update the file contents
shard_exceptions = {}
for sharding in shardings:
suite_exception = {
sharding.test_suite: {
'shards': sharding.optimal_shard_count,
'try_builder': sharding.try_builder,
},
}
shard_exceptions.setdefault(
sharding.waterfall_builder_group, {}
).setdefault(sharding.waterfall_builder_name, {}).update(suite_exception)
api.file.write_json(
'write exceptions file',
exceptions_file,
shard_exceptions,
indent=4,
)
def _calculate_optimal_shard_counts(
api: DEPS,
target_runtime: float,
shardings: list[Sharding],
old_shardings: list[Sharding],
) -> list[Sharding]:
builder_suite_shardings = {s.id: s for s in old_shardings}
with api.step.nest('calculate optimal shards'):
for sharding in shardings:
with api.step.nest(sharding.id):
sharding.calculate_optimal_shard_count(api, target_runtime)
# Get the number of shards we need to hit the target_runtime
old_sharding = builder_suite_shardings.get(sharding.id, None)
if sharding.check_valid_sharding(api, old_sharding):
builder_suite_shardings[sharding.id] = sharding
return list(builder_suite_shardings.values())
def _run_query(api: DEPS, query: str) -> list[dict[str, Any]]:
result = api.step(
'query',
[
'bq',
'query',
f'--project_id={_CLOUD_PROJECT_ID}',
'--format=json',
'--max_rows=100000',
'--nouse_legacy_sql',
str(query),
],
step_test_data=lambda: api.raw_io.test_api.stream_output_text('[]'),
stdout=api.json.output(),
)
result.presentation.logs['query'] = str(query)
return result.stdout
def _query_avg_builds_per_hour(
api: DEPS,
shardings: list[Sharding],
lookback_start_date: datetime.datetime,
lookback_end_date: datetime.datetime,
) -> None:
with api.step.nest('query average builds per hour'):
query_tmpl = api.file.read_text(
'read_query',
api.resource('query_average_number_builds_per_hour.sql.tmpl'),
)
query = query_tmpl.format(
builds_project='cr-buildbucket',
builds_dataset='chromium',
lookback_start_date=lookback_start_date,
lookback_end_date=lookback_end_date,
)
rows = _run_query(api, query)
for row in rows:
for sharding in shardings:
if row['try_builder'] == sharding.try_builder:
sharding.builds_per_hour = int(row['avg_count'])
def _query_overheads(
api: DEPS,
shardings: list[Sharding],
lookback_start_date: datetime.datetime,
lookback_end_date: datetime.datetime,
) -> None:
with api.step.nest('query overheads'):
query_tmpl = api.file.read_text(
'read_query', api.resource('query_test_overheads.sql.tmpl')
)
query = query_tmpl.format(
builds_project='cr-buildbucket',
builds_dataset='chromium',
tasks_project='chromium-swarm',
tasks_dataset='swarming',
lookback_start_date=lookback_start_date,
lookback_end_date=lookback_end_date,
)
rows = _run_query(api, query)
for row in rows:
shard_count = int(row['normally_assigned_shard_count'])
overhead = (
float(row['p50_task_setup_duration_sec'])
+ float(row['p50_test_harness_overhead_sec'])
) / 60
for sharding in shardings:
if (
sharding.try_builder == row['try_builder']
and sharding.test_suite == row['test_suite']
and sharding.shard_count == shard_count
):
sharding.overhead = overhead
def _query_durations(
api: DEPS,
target_runtime: float,
percentile: int,
lookback_start_date: datetime.datetime,
lookback_end_date: datetime.datetime,
exclude_suites: list[str],
exclude_builders: list[str],
exclude_builder_suites: dict[str, list[str]],
) -> list[Sharding]:
def _join_sql_collection(collection):
return ','.join([f'"{u}"' for u in collection] if collection else '""')
with api.step.nest('query durations'):
query_tmpl = api.file.read_text(
'read_query', api.resource('query_suite_durations.sql.tmpl')
)
query = query_tmpl.format(
builds_project='cr-buildbucket',
builds_dataset='chromium',
tasks_project='chromium-swarm',
tasks_dataset='swarming',
lookback_start_date=lookback_start_date,
lookback_end_date=lookback_end_date,
percentile=percentile,
min_sample_size=MIN_SAMPLE_SIZE,
target_runtime=target_runtime,
# TODO(crbug.com/1275620): Replace with
# service_account.default().get_email()
ignore_cl_owner=_join_sql_collection(
['chromium-autosharder@chops-service-accounts.iam.gserviceaccount.com']
),
exclude_test_suites=_join_sql_collection(exclude_suites),
exclude_builders=_join_sql_collection(exclude_builders),
exclude_builder_suites=_join_sql_collection(
[
f'{builder}:{suite}'
for builder, suites in exclude_builder_suites.items()
for suite in suites.suites
]
),
)
rows = _run_query(api, query)
return [
Sharding(
test_suite=row['test_suite'],
try_builder=row['try_builder'],
waterfall_builder_group=row['waterfall_builder_group'],
waterfall_builder_name=row['waterfall_builder_name'],
shard_count=int(row['shard_count']),
runtime=float(row['percentile_duration_minutes']),
)
for row in rows
]
def _query_cq_builders(api: DEPS) -> list[str]:
with api.step.nest('query cq builders'):
query_tmpl = api.file.read_text(
'read_query', api.resource('query_cq_builders.sql.tmpl')
)
query = query_tmpl.format(
builders_project='chrome-trooper-analytics',
builders_dataset='metrics',
)
rows = _run_query(api, query)
return [row['builder'] for row in rows]
def _check_run_conditions(api: DEPS) -> result_pb2.RawResult:
# If it's a led job, we almost certainly want to run
if api.led.led_build:
return None
# Check if it's been at least 7 days since the last merged reshard CL
last_merged_change_list = api.gerrit.get_changes(
name='get last merged change',
host='https://chromium-review.googlesource.com',
query_params=[('owner', 'chromium-autosharder'), ('status', 'merged')],
limit=1,
)
# TODO(crbug.com/407846444): We need to verify that comments on CLs don't
# change the updated timestamp
if last_merged_change_list:
last_merged_date = datetime.datetime.strptime(
last_merged_change_list[0]['updated'], '%Y-%m-%d %H:%M:%S.%f000'
).date()
current_date = datetime.date.fromtimestamp(api.time.time())
# Only look up to Monday in order to maintain the same cadence when a CL
# isn't merged the first time
start_of_week = current_date - datetime.timedelta(
days=current_date.weekday()
)
if last_merged_date >= start_of_week:
return result_pb2.RawResult(
status=common_pb.SUCCESS,
summary_markdown=(
'Skipping autosharder CL creation because it has been merged '
'this week.'
),
)
# Check to see if there's already an active autosharder CL
changes = api.gerrit.get_changes(
name='get active changes',
host='https://chromium-review.googlesource.com',
query_params=[('uploader', 'chromium-autosharder'), ('status', 'open')],
)
if changes:
if len(changes) > 1:
return result_pb2.RawResult(
status=common_pb.INFRA_FAILURE,
summary_markdown=(
'More than one autosharder CL exists. There should only be a max '
'of one CL at any time.'
),
)
issue_num = changes[0]['_number']
with api.context(cwd=api.chromium_checkout.source_dir):
status_step = api.git_cl(
'status',
['--issue', issue_num, '--field', 'status'],
name='git cl status',
stdout=api.raw_io.output_text(),
step_test_data=lambda: api.raw_io.test_api.stream_output_text(
'commit', stream='stdout'
),
)
cl_status = status_step.stdout.strip()
status_step.presentation.step_text = cl_status
# Exit if currently open CL is running a CQ attempt
if cl_status in ['dry-run', 'commit']:
return result_pb2.RawResult(
status=common_pb.SUCCESS,
summary_markdown=(
'There is already a CL running a CQ attempt or ready to submit.'
),
)
# Close CL so we can upload a new one
with api.context(cwd=api.chromium_checkout.source_dir):
api.git_cl('set-close', ['--issue', issue_num], name='git cl set-close')
return None
def _setup_git(api: DEPS) -> None:
api.git('config', 'user.name', 'autosharder')
api.git('branch', '-D', 'autoshard', ok_ret='any')
api.git('restore', '--staged', '.')
with api.depot_tools.on_path():
api.git('new-branch', 'autoshard', '--upstream', 'origin/main')
def GenTests(api: RecipeApi):
# Simulate running on a Monday
current_timestamp = int(datetime.datetime(2023, 7, 3).timestamp())
autoshard_exceptions_json_path = api.path.cache_dir.joinpath(
'builder', 'src', 'testing', 'buildbot', 'autoshard_exceptions.json'
)
def create_durations_entry(
waterfall_builder_group,
waterfall_builder_name,
try_builder,
test_suite,
**kwargs,
):
return {
'waterfall_builder_group': waterfall_builder_group,
'waterfall_builder_name': waterfall_builder_name,
'try_builder': try_builder,
'test_suite': test_suite,
'shard_count': 10,
'p50_pending_time_sec': 1,
'p90_pending_time_sec': 231,
'avg_pending_time_sec': 65,
'avg_task_setup_overhead_sec': 27,
'percentile_duration_minutes': 20,
'sample_size': 9508,
**kwargs,
}
def create_overhead_entry(
waterfall_builder_group,
waterfall_builder_name,
try_builder,
test_suite,
**kwargs,
):
return {
'test_suite': test_suite,
'try_builder': try_builder,
'waterfall_builder_group': waterfall_builder_group,
'waterfall_builder_name': waterfall_builder_name,
'p50_task_setup_duration_sec': 70,
'p50_test_harness_overhead_sec': 100,
'normally_assigned_shard_count': 10,
'experimental_shard_count': 11,
**kwargs,
}
def create_average_builds_per_hour(try_builder, avg_count=80):
return {
'try_builder': try_builder,
'avg_count': avg_count,
}
def create_cq_builder(try_builder):
return {
'builder': try_builder,
}
def boilerplate(
durations,
overheads,
avg_builds_per_hour,
cq_builders,
current_exceptions=None,
):
test_data = [
api.properties(target_runtime=15.0),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.path.exists(autoshard_exceptions_json_path),
api.override_step_data(
'gerrit get active changes',
stdout=api.raw_io.output_text(''),
),
api.time.seed(current_timestamp),
]
test_data.append(
api.override_step_data(
'read current exceptions file',
api.file.read_json(
current_exceptions
or {
'chromium.builder_group': {
'builder_name': {
'fake_test_suite': {
'try_builder': 'linux-rel',
'shards': 4,
}
}
},
}
),
)
)
if durations:
test_data.append(
api.override_step_data(
'query durations.query',
stdout=api.raw_io.output_text(api.json.dumps(durations)),
)
)
if overheads:
test_data.append(
api.override_step_data(
'query overheads.query',
stdout=api.raw_io.output_text(api.json.dumps(overheads)),
)
)
if avg_builds_per_hour:
test_data.append(
api.override_step_data(
'query average builds per hour.query',
stdout=api.raw_io.output_text(api.json.dumps(avg_builds_per_hour)),
)
)
if cq_builders:
test_data.append(
api.override_step_data(
'query cq builders.query',
stdout=api.raw_io.output_text(api.json.dumps(cq_builders)),
)
)
return sum(test_data, api.empty_test_data())
def check_exceptions_file(
check, steps, builder_group, builder, test_suite, key, value
):
# Checks that the sharding conclusions from the run match the
# provided sharding values.
exceptions_json = api.json.loads(
steps['write exceptions file'].logs['autoshard_exceptions.json']
)
check(exceptions_json[builder_group][builder][test_suite][key] == value)
def check_sharding(
check, steps, builder_group, builder, test_suite, sharding
):
check_exceptions_file(
check, steps, builder_group, builder, test_suite, 'shards', sharding
)
def check_not_sharded(check, steps, builder_group):
exceptions_json = api.json.loads(
steps['write exceptions file'].logs['autoshard_exceptions.json']
)
check(builder_group not in exceptions_json)
yield api.test(
'basic',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'browser_tests', 15
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'excludes_builder_test_suites',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.properties(
target_runtime=15.0,
exclude_test_suites=['s1', 's2'],
exclude_builders=['b1', 'b2'],
exclude_builder_test_suites={'b': {'suites': ['s1', 's2']}},
),
api.override_step_data(
'query durations.read_query',
api.file.read_text(
'{exclude_test_suites}/{exclude_builders}/{exclude_builder_suites}'
),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(
post_process.StepCommandContains,
'query durations.query',
['"s1","s2"/"b1","b2"/"b:s1","b:s2"'],
),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'dont_shard_from_1_if_previously_unsharded',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
percentile_duration_minutes=20,
shard_count=1,
)
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
normally_assigned_shard_count=1,
experimental_shard_count=2,
p50_test_harness_overhead_sec=1,
)
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(check_not_sharded, 'chromium.linux'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'old_shard_in_loopback',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
shard_count=10,
),
# This should be ignored because it is picking it up from
# a previous sharding. Not the latest
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
percentile_duration_minutes=1000,
shard_count=5,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
normally_assigned_shard_count=10,
experimental_shard_count=11,
),
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
normally_assigned_shard_count=5,
experimental_shard_count=6,
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'linux-rel',
}
}
}
},
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'browser_tests', 15
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'optimal_shards_less_than_zero',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
percentile_duration_minutes=3.0,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
p50_test_harness_overhead_sec=60 * 4,
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(check_not_sharded, 'chromium.linux'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_multiple_builders_in_group',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux GPU Tests',
try_builder='linux-rel',
test_suite='gpu_tests',
shard_count=10,
percentile_duration_minutes=30,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux GPU Tests',
try_builder='linux-rel',
test_suite='gpu_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'browser_tests', 15
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux GPU Tests', 'gpu_tests', 23
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_merge_existing_output_file',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
percentile_duration_minutes=30,
shard_count=10,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
create_cq_builder(try_builder='android-try'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'linux-rel',
},
'interactive_ui_tests': {
'shards': 3,
'try_builder': 'linux-rel',
},
},
},
'chromium.android': {
'android-12-x64-rel': {
'webview_instrumentation_test_apk': {
'shards': 11,
'try_builder': 'android-try',
}
}
},
},
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'browser_tests', 23
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'interactive_ui_tests', 3
),
api.post_process(
check_sharding,
'chromium.android',
'android-12-x64-rel',
'webview_instrumentation_test_apk',
11,
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_reduce_shards_already_autosharded',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
percentile_duration_minutes=10,
shard_count=15,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
create_cq_builder(try_builder='android-try'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 15,
'try_builder': 'linux-rel',
},
'interactive_ui_tests': {
'shards': 3,
'try_builder': 'linux-rel',
},
},
},
'chromium.android': {
'android-12-x64-rel': {
'webview_instrumentation_test_apk': {
'shards': 11,
'try_builder': 'android-try',
}
}
},
},
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'browser_tests', 10
),
api.post_process(
check_sharding, 'chromium.linux', 'Linux Tests', 'interactive_ui_tests', 3
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_multiple_overhead_values',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
shard_count=20,
percentile_duration_minutes=20,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
normally_assigned_shard_count=19,
experimental_shard_count=20,
p50_task_setup_duration_sec=30,
p50_test_harness_overhead_sec=60,
),
create_overhead_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
normally_assigned_shard_count=20,
experimental_shard_count=21,
p50_task_setup_duration_sec=30,
p50_test_harness_overhead_sec=120,
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='android-12-x64-rel'),
],
cq_builders=[
create_cq_builder(try_builder='android-12-x64-rel'),
],
),
api.post_process(
post_process.StepTextContains,
'calculate optimal shards.android-12-x64-rel:webview_instrumentation_test_apk.optimal shard calculation',
[f'overhead = {2.5}'],
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_reject_suggested_decrease_if_percentile_min_was_close_to_desired',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
shard_count=10,
percentile_duration_minutes=14.5,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='android-12-x64-rel'),
],
cq_builders=[
create_cq_builder(try_builder='android-12-x64-rel'),
],
current_exceptions={
'chromium.android': {
'android-12-x64-rel': {
'webview_instrumentation_test_apk': {
'shards': 10,
'try_builder': 'android-12-x64-rel',
}
}
}
},
),
api.post_process(
check_sharding,
'chromium.android',
'android-12-x64-rel',
'webview_instrumentation_test_apk',
10,
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_reject_suggested_decrease_if_simulated_is_too_high',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
shard_count=10,
percentile_duration_minutes=14,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='android-12-x64-rel'),
],
cq_builders=[
create_cq_builder(try_builder='android-12-x64-rel'),
],
current_exceptions={
'chromium.android': {
'android-12-x64-rel': {
'webview_instrumentation_test_apk': {
'shards': 10,
'try_builder': 'android-12-x64-rel',
}
}
}
},
),
api.post_process(
check_sharding,
'chromium.android',
'android-12-x64-rel',
'webview_instrumentation_test_apk',
10,
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_not_already_autosharded_decrease',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
shard_count=10,
percentile_duration_minutes=9,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='android-12-x64-rel'),
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='android-12-x64-rel'),
create_cq_builder(try_builder='linux-rel'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'linux-rel',
}
}
},
},
),
api.post_process(check_not_sharded, 'chromium.android'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_not_already_autosharded_increase',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
shard_count=10,
percentile_duration_minutes=20,
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.android',
waterfall_builder_name='android-12-x64-rel',
try_builder='android-12-x64-rel',
test_suite='webview_instrumentation_test_apk',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='android-12-x64-rel'),
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='android-12-x64-rel'),
create_cq_builder(try_builder='linux-rel'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'linux-rel',
}
}
},
},
),
api.post_process(
check_sharding,
'chromium.android',
'android-12-x64-rel',
'webview_instrumentation_test_apk',
15,
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_pruned',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.win',
waterfall_builder_name='Win10 Tests x64',
try_builder='win-rel',
test_suite='browser_tests',
),
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.win',
waterfall_builder_name='Win10 Tests x64',
try_builder='win-rel',
test_suite='browser_tests',
),
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='win-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
current_exceptions={
'chromium.linux': {
'Linux Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'linux-rel',
}
}
},
'chromium.win': {
'Win Tests': {
'browser_tests': {
'shards': 10,
'try_builder': 'win-rel',
}
}
},
},
),
api.post_process(check_not_sharded, 'chromium.win'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'led_job',
api.properties(
**{
'$recipe_engine/led': {
'shadowed_bucket': 'try',
},
}
),
api.time.seed(current_timestamp),
api.path.exists(autoshard_exceptions_json_path),
api.override_step_data(
'git diff',
stdout=api.raw_io.output_text(''),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_change',
api.time.seed(current_timestamp),
api.path.exists(autoshard_exceptions_json_path),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.override_step_data(
'gerrit get active changes',
stdout=api.raw_io.output_text(''),
),
api.override_step_data(
'git diff',
stdout=api.raw_io.output_text(''),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.DoesNotRun, 'regenerate targets specs'),
api.post_process(post_process.DoesNotRun, 'git cl upload'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'already merged this week',
api.time.seed(current_timestamp),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 3).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.post_process(
post_process.SummaryMarkdown,
'Skipping autosharder CL creation because it has been merged this week.',
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'too_many_existing_cls',
api.time.seed(current_timestamp),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.override_step_data(
'gerrit get active changes',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12346',
},
{'subject': 'Autosharder CL', '_number': '12347'},
]
),
),
api.post_process(post_process.DoesNotRun, 'query durations'),
api.post_process(post_process.DoesNotRun, 'regenerate targets specs'),
api.post_process(post_process.DoesNotRun, 'git cl set-close'),
api.post_process(post_process.DoesNotRun, 'git cl upload'),
api.expect_status('INFRA_FAILURE'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_existing_cl',
api.time.seed(current_timestamp),
api.path.exists(autoshard_exceptions_json_path),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.override_step_data(
'gerrit get active changes',
stdout=api.raw_io.output_text(''),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.DoesNotRun, 'git cl set-close'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'no_landed_CL_yet',
api.time.seed(current_timestamp),
api.override_step_data(
'gerrit get last merged change',
stdout=api.raw_io.output_text(''),
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'current_dry_run',
api.time.seed(current_timestamp),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.override_step_data(
'gerrit get active changes',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12346',
}
]
),
),
api.override_step_data(
'git cl status',
stdout=api.raw_io.output_text('dry-run'),
),
api.post_process(post_process.DoesNotRun, 'query durations'),
api.post_process(post_process.DoesNotRun, 'regenerate targets specs'),
api.post_process(post_process.DoesNotRun, 'git cl set-close'),
api.post_process(post_process.DoesNotRun, 'git cl upload'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'existing_waiting_cl',
api.time.seed(current_timestamp),
api.path.exists(autoshard_exceptions_json_path),
api.override_step_data(
'gerrit get last merged change',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12345',
'updated': datetime.datetime(2023, 7, 1).strftime(
'%Y-%m-%d %H:%M:%S.%f000'
),
}
]
),
),
api.override_step_data(
'gerrit get active changes',
api.json.output(
[
{
'subject': 'Autosharder CL',
'_number': '12346',
}
]
),
),
api.override_step_data(
'git cl status',
stdout=api.raw_io.output_text('waiting'),
),
api.post_process(post_process.MustRun, 'git cl set-close'),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(post_process.MustRun, 'regenerate targets specs'),
api.post_process(post_process.MustRun, 'git cl upload'),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_percentile_property',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.properties(
target_runtime=15.0,
percentile=95,
),
api.override_step_data(
'query durations.read_query',
api.file.read_text('{percentile}'),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(
post_process.StepCommandContains, 'query durations.query', ['95']
),
api.post_process(post_process.DropExpectation),
)
yield api.test(
'test_percentile_default',
boilerplate(
durations=[
create_durations_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
overheads=[
create_overhead_entry(
waterfall_builder_group='chromium.linux',
waterfall_builder_name='Linux Tests',
try_builder='linux-rel',
test_suite='browser_tests',
)
],
avg_builds_per_hour=[
create_average_builds_per_hour(try_builder='linux-rel'),
],
cq_builders=[
create_cq_builder(try_builder='linux-rel'),
],
),
api.properties(
target_runtime=15.0,
),
api.override_step_data(
'query durations.read_query',
api.file.read_text('{percentile}'),
),
api.post_process(post_process.MustRun, 'query durations'),
api.post_process(
post_process.StepCommandContains, 'query durations.query', ['80']
),
api.post_process(post_process.DropExpectation),
)