# Copyright (C) 2022 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import dataclasses import functools import logging import os import re import sys import textwrap from pathlib import Path from typing import Optional import cuj_catalog import util from util import BuildType @dataclasses.dataclass(frozen=True) class UserInput: build_types: tuple[BuildType, ...] chosen_cujgroups: tuple[int, ...] tag: Optional[str] log_dir: Path no_warmup: bool targets: tuple[str, ...] ci_mode: bool @functools.cache def get_user_input() -> UserInput: cujgroups = cuj_catalog.get_cujgroups() def validate_cujgroups(input_str: str) -> list[int]: if input_str.isnumeric(): i = int(input_str) if 0 <= i < len(cujgroups): return [i] logging.critical( f"Invalid input: {input_str}. " f"Expected an index between 1 and {len(cujgroups)}. " "Try --help to view the list of available CUJs" ) raise argparse.ArgumentTypeError() else: pattern = re.compile(input_str) matching_cuj_groups = [ i for i, cujgroup in enumerate(cujgroups) if pattern.search(cujgroup.description) ] if len(matching_cuj_groups): return matching_cuj_groups logging.critical( f'Invalid input: "{input_str}" does not match any CUJ. ' "Try --help to view the list of available CUJs" ) raise argparse.ArgumentTypeError() # importing locally here to avoid chances of cyclic import import incremental_build p = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, description="" + textwrap.dedent(incremental_build.__doc__) + textwrap.dedent(incremental_build.main.__doc__), ) cuj_list = "\n".join( [f"{i:2}: {cujgroup.description}" for i, cujgroup in enumerate(cujgroups)] ) p.add_argument( "-c", "--cujs", nargs="*", type=validate_cujgroups, help="Index number(s) for the CUJ(s) from the following list. " "Or substring matches for the CUJ description." f"Note the ordering will be respected:\n{cuj_list}", ) p.add_argument( "--no-warmup", default=False, action="store_true", help="skip warmup builds; this can skew your results for the first CUJ you run.", ) p.add_argument( "-t", "--tag", type=str, default="", help="Any additional tag for this set of builds, this helps " "distinguish the new data from previously collected data, " "useful for comparative analysis", ) log_levels = dict(getattr(logging, "_levelToName")).values() p.add_argument( "-v", "--verbosity", choices=log_levels, default="INFO", help="Log level. Defaults to %(default)s", ) default_log_dir = util.get_default_log_dir() p.add_argument( "-l", "--log-dir", type=Path, default=default_log_dir, help=textwrap.dedent( """\ Directory for timing logs. Defaults to %(default)s TIPS: 1 Specify a directory outside of the source tree 2 To view key metrics in metrics.csv: {} 3 To view column headers: {} """ ).format( textwrap.indent( util.get_cmd_to_display_tabulated_metrics(default_log_dir, False), " " * 4, ), textwrap.indent(util.get_csv_columns_cmd(default_log_dir), " " * 4), ), ) def_build_types = [ BuildType.SOONG_ONLY, ] p.add_argument( "-b", "--build-types", nargs="+", type=BuildType.from_flag, default=[def_build_types], help=f"Defaults to {[b.to_flag() for b in def_build_types]}. " f"Choose from {[e.name.lower() for e in BuildType]}", ) p.add_argument( "--ignore-repo-diff", default=False, action="store_true", help='Skip "repo status" check', ) p.add_argument( "--append-csv", default=False, action="store_true", help="Add results to existing spreadsheet", ) p.add_argument( "targets", nargs="*", default=["nothing"], help='Targets to run, e.g. "libc adbd". ' "Defaults to %(default)s", ) p.add_argument( "--ci-mode", default=False, action="store_true", help="Only use it for CI runs.It will copy the " "first metrics after warmup to the logs directory in CI", ) options = p.parse_args() if options.verbosity: logging.root.setLevel(options.verbosity) chosen_cujgroups: tuple[int, ...] = ( tuple(int(i) for sublist in options.cujs for i in sublist) if options.cujs else tuple() ) bazel_labels: list[str] = [ target for target in options.targets if target.startswith("//") ] if 0 < len(bazel_labels) < len(options.targets): logging.critical( f"Don't mix bazel labels {bazel_labels} with soong targets " f"{[t for t in options.targets if t not in bazel_labels]}" ) sys.exit(1) if os.getenv("BUILD_BROKEN_DISABLE_BAZEL") is not None: raise RuntimeError( f"use -b {BuildType.SOONG_ONLY.to_flag()} " f"instead of BUILD_BROKEN_DISABLE_BAZEL" ) build_types: tuple[BuildType, ...] = tuple( BuildType(i) for sublist in options.build_types for i in sublist ) if len(bazel_labels) > 0: non_b = [ b.name for b in build_types if b != BuildType.B_BUILD and b != BuildType.B_ANDROID ] if len(non_b): raise RuntimeError(f"bazel labels can not be used with {non_b}") pretty_str = "\n".join( [f"{i:2}: {cujgroups[i].description}" for i in chosen_cujgroups] ) logging.info(f"%d CUJs chosen:\n%s", len(chosen_cujgroups), pretty_str) if not options.ignore_repo_diff and util.has_uncommitted_changes(): error_message = ( "THERE ARE UNCOMMITTED CHANGES (TIP: repo status). " "Use --ignore-repo-diff to skip this check." ) if not util.is_interactive_shell(): logging.critical(error_message) sys.exit(1) logging.error(error_message) response = input("Continue?[Y/n]") if response.upper() != "Y": sys.exit(1) log_dir = Path(options.log_dir).resolve() if not options.append_csv and log_dir.exists(): error_message = ( f"{log_dir} already exists. " "Use --append-csv to skip this check." "Consider --tag to your new runs" ) if not util.is_interactive_shell(): logging.critical(error_message) sys.exit(1) logging.error(error_message) response = input("Continue?[Y/n]") if response.upper() != "Y": sys.exit(1) if log_dir.is_relative_to(util.get_top_dir()): logging.critical( f"choose a log_dir outside the source tree; " f"'{options.log_dir}' resolves to {log_dir}" ) sys.exit(1) if options.ci_mode: if len(chosen_cujgroups) > 1: logging.critical( "CI mode can only allow one cuj group. " "Remove --ci-mode flag to skip this check." ) sys.exit(1) if len(build_types) > 1: logging.critical( "CI mode can only allow one build type. " "Remove --ci-mode flag to skip this check." ) sys.exit(1) if options.no_warmup: logging.warning( "WARMUP runs will be skipped. Note this is not advised " "as it gives unreliable results." ) return UserInput( build_types=build_types, chosen_cujgroups=chosen_cujgroups, tag=options.tag, log_dir=Path(options.log_dir).resolve(), no_warmup=options.no_warmup, targets=options.targets, ci_mode=options.ci_mode, )