blob: b78027685d163aad465a31d066caf86fb4dc16a8 [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.
"""Runs the rts-suite-analysis tool against builder suite combinations"""
import datetime
import re
from recipe_engine import post_process
from RECIPE_MODULES.build.attr_utils import attrib, attrs
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.recipe_engine import (
cipd,
file,
futures,
json,
path,
platform,
raw_io,
step,
time,
)
@dataclass
class DEPS(RecipeScriptApi):
cipd: cipd.API
file: file.API
futures: futures.API
json: json.API
path: path.API
platform: platform.API
raw_io: raw_io.API
step: step.API
time: time.API
@dataclass
class TEST_DEPS(RecipeTestApi):
file: file.TEST_API
json: json.TEST_API
path: path.TEST_API
platform: platform.TEST_API
raw_io: raw_io.TEST_API
step: step.TEST_API
time: time.TEST_API
# These are actually 4 week periods
REJECTION_DATA_MONTHS = 3
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
LOGGED_REJECTIONS = 100
_CLOUD_PROJECT_ID = 'chrome-trooper-analytics'
@attrs()
class SavingsAnalysis:
builder = attrib(str)
test_suite = attrib(str)
recall = attrib(float)
savings = attrib(float)
def report(self):
return f'{self.builder}:{self.test_suite} {self.recall}%, {self.savings}%'
def RunSteps(api: DEPS):
# Install rts-suite-analysis executables.
exec_path = api.cipd.ensure_tool(
'chromium/rts/rts-suite-analysis/${platform}', 'latest'
)
# 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,
duration_date_range=(
yesterday - TEST_DURATION_DATA_WINDOW,
yesterday,
),
)
bq_cmd = [
"bq",
"query",
"--project_id=" + _CLOUD_PROJECT_ID,
"--format=json",
"--max_rows=10000",
"--nouse_legacy_sql",
builder_suite_query,
]
step_result = api.step(
'get builder suites', bq_cmd, timeout=60, stdout=api.json.output()
)
builder_suites = step_result.stdout
futures = []
for builder_suite in builder_suites:
test_suite = builder_suite['test_suite']
builder = builder_suite['builder']
futures.append(
api.futures.spawn_immediate(
_analyze_builder_suite,
api,
builder,
test_suite,
rejections_dir,
durations_dir,
exec_path,
)
)
analyses = []
# Check future's exception.
for f in futures:
result = f.result()
if result:
analyses.append(result)
# Sort by recall. There will be lots of low savings but it should be easy
# to scan for the best candidates
analyses.sort(
key=lambda analysis: (analysis.recall, analysis.savings), reverse=True
)
summary = 'Analysis Summary (recall, savings):\n\n' + '\n\n'.join(
analysis.report() for analysis in analyses
)
return result_pb2.RawResult(
status=common_pb.SUCCESS, summary_markdown=summary[:4000]
)
def _analyze_builder_suite(
api: DEPS, builder, test_suite, rejections_dir, durations_dir, exec_path
):
# Request 1 CPU core (1000 millicores) and 2 GiB RAM (2048 MiB) per step.
# On a 60-core c2-standard-60 VM (60,000 millicores, ~240 GiB RAM), the recipe
# engine's ResourceWaiter automatically throttles execution to at most 60
# concurrent processes (60,000 / 1,000 = 60).
step_result = api.step(
f'analyze {test_suite} on {builder}',
[
exec_path,
'analyze',
f'-rejections={rejections_dir}',
f'-durations={durations_dir}',
f'-builder={builder}',
f'-testSuite={test_suite}',
f'-log-furthest={LOGGED_REJECTIONS}',
],
stdout=api.raw_io.output_text(),
cost=api.step.ResourceCost(cpu=1000),
)
match = re.search(r'(\d+\.\d+)%\s*\|\s*<?(\d+\.\d+)%', step_result.stdout)
if not match:
# No summary table implies something went wrong with the analysis
step_result.presentation.status = api.step.FAILURE
step_result.presentation.step_text = step_result.stdout
else:
recall = float(match.group(1))
savings = float(match.group(2))
step_result.presentation.step_text = (
f'Recall {recall} Savings {savings}\n' + step_result.stdout
)
return SavingsAnalysis(
builder=builder,
test_suite=test_suite,
recall=recall,
savings=savings,
)
def _fetch_model_data(api: DEPS, exec_path, 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-suite-analysis-model-data'
rejections_dir = data_dir / 'rejections'
durations_dir = data_dir / 'durations'
# Using -append flag doesn't clean the rejections folder so ensure it's
# cleaned before the fetches
api.file.rmcontents('clean rejections', rejections_dir)
fetches = []
end = api.time.utcnow().date() - datetime.timedelta(days=1)
for _ in range(REJECTION_DATA_MONTHS):
start = end - datetime.timedelta(weeks=4)
fetches.append(
api.futures.spawn_immediate(
api.step,
'fetch rejections (fast)',
[
str(exec_path),
'fetch-rejections-fast',
'-ignore-file',
'-append',
f'-out={rejections_dir}',
]
+ _date_range_flags((start, end)),
)
)
end = start
fetches.append(
api.futures.spawn_immediate(
api.step,
'fetch durations',
[
str(exec_path),
'fetch-durations',
f'-frac={TEST_DURATION_DATA_PERCENTAGE / 100.0:.3f}',
f'-out={durations_dir}',
]
+ _date_range_flags(duration_date_range),
)
)
with api.futures.iwait(fetches) as itr:
for future in itr:
future.result()
return rejections_dir, durations_dir
def _date_range_flags(date_range):
from_date, to_date = date_range
return [
f'-from={from_date.strftime("%Y-%m-%d")}',
f'-to={to_date.strftime("%Y-%m-%d")}',
]
def GenTests(api: TEST_DEPS):
yield api.test(
'basic',
api.platform.name('linux'),
api.platform.arch('intel'),
api.platform.bits(64),
api.step_data(
'get builder suites',
stdout=api.json.output(
[
{'builder': 'linux-chromeos-rel', 'test_suite': 'browser_tests'},
{
'builder': 'android-nougat-x86-rel',
'test_suite': 'chrome_public_test_apk',
},
{'builder': 'mac', 'test_suite': 'suite_fails_to_complete'},
]
),
),
api.step_data(
'analyze browser_tests on linux-chromeos-rel',
stdout=api.raw_io.output_text('''
Rejection:
Most affected test: +Inf distance
https://chromium-review.googlesource.com/c/4398410/4
//chrome/browser/media/encrypted_media_browsertest.cc
Failed and not selected tests:
- builder:win-rel | os:Windows-10-19045 | test_suite:browser_tests
in //chrome/browser/media/encrypted_media_browsertest.cc
ninja://chrome/test:browser_tests/MediaFoundationEncryptedMediaTest.Playback_EncryptedAudioCbcs_MediaTypeUnsupported
ChangeRecall | Savings
----------------------
99.16% | 8.37%
based on 837 rejections, 862565 test failures, 2 years 52 days 20 hours 22 minutes 37 seconds testing time
'''),
),
api.step_data(
'analyze chrome_public_test_apk on android-nougat-x86-rel',
stdout=api.raw_io.output_text('''
Rejection:
Most affected test: +Inf distance
https://chromium-review.googlesource.com/c/4398410/4
//chrome/file.cc
Failed and not selected tests:
- builder:android-nougat-x86-rel | os:android | test_suite:chrome_public_test_apk
in //chrome/file.cc
ninja://chrome/test:chrome_public_test_apk/test.case
ChangeRecall | Savings
----------------------
100.00% | 90.37%
based on 837 rejections, 862565 test failures, 2 years 52 days 20 hours 22 minutes 37 seconds testing time
'''),
),
api.step_data(
'analyze suite_fails_to_complete on mac',
stdout=api.raw_io.output_text('Failed to run'),
),
api.post_process(
post_process.MustRun, 'analyze browser_tests on linux-chromeos-rel'
),
api.post_process(
post_process.MustRun,
'analyze chrome_public_test_apk on android-nougat-x86-rel',
),
api.post_process(
post_process.SummaryMarkdown,
'Analysis Summary (recall, savings):\n\nandroid-nougat-x86-rel:chrome_public_test_apk 100.0%, 90.37%\n\nlinux-chromeos-rel:browser_tests 99.16%, 8.37%',
),
api.post_process(
post_process.StepCommandContains,
'analyze suite_fails_to_complete on mac',
['-log-furthest=100'],
),
api.post_process(
post_process.StepCommandContains,
'fetch rejections (fast)',
['-from=2012-04-15', '-to=2012-05-13'],
),
api.post_process(
post_process.StepCommandContains,
'fetch rejections (fast) (2)',
['-from=2012-03-18', '-to=2012-04-15'],
),
api.post_process(
post_process.StepCommandContains,
'fetch rejections (fast) (3)',
['-from=2012-02-19', '-to=2012-03-18'],
),
api.post_check(post_process.StatusSuccess),
api.post_process(post_process.DropExpectation),
)
builder_suite_query = '''
SELECT
(SELECT v.value FROM tr.variant AS v WHERE v.key = 'builder') AS builder,
(SELECT v.value FROM tr.variant AS v WHERE v.key = 'test_suite') AS test_suite,
FROM chrome-luci-data.chromium.try_test_results tr
INNER JOIN `chrome-trooper-analytics.metrics.cq_builders` cq_builders
ON (SELECT v.value FROM tr.variant AS v WHERE v.key = 'builder') = cq_builders.builder
WHERE DATE(tr.partition_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
GROUP BY builder, test_suite
ORDER BY COUNT(*) DESC
'''