1# Copyright (C) 2022 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import argparse 16import dataclasses 17import functools 18import logging 19import os 20import re 21import sys 22import textwrap 23from pathlib import Path 24from typing import Optional 25 26import cuj_catalog 27import util 28from util import BuildType 29 30 31@dataclasses.dataclass(frozen=True) 32class UserInput: 33 build_types: tuple[BuildType, ...] 34 chosen_cujgroups: tuple[int, ...] 35 tag: Optional[str] 36 log_dir: Path 37 no_warmup: bool 38 targets: tuple[str, ...] 39 ci_mode: bool 40 41 42@functools.cache 43def get_user_input() -> UserInput: 44 cujgroups = cuj_catalog.get_cujgroups() 45 46 def validate_cujgroups(input_str: str) -> list[int]: 47 if input_str.isnumeric(): 48 i = int(input_str) 49 if 0 <= i < len(cujgroups): 50 return [i] 51 logging.critical( 52 f"Invalid input: {input_str}. " 53 f"Expected an index between 1 and {len(cujgroups)}. " 54 "Try --help to view the list of available CUJs" 55 ) 56 raise argparse.ArgumentTypeError() 57 else: 58 pattern = re.compile(input_str) 59 matching_cuj_groups = [ 60 i 61 for i, cujgroup in enumerate(cujgroups) 62 if pattern.search(cujgroup.description) 63 ] 64 if len(matching_cuj_groups): 65 return matching_cuj_groups 66 logging.critical( 67 f'Invalid input: "{input_str}" does not match any CUJ. ' 68 "Try --help to view the list of available CUJs" 69 ) 70 raise argparse.ArgumentTypeError() 71 72 # importing locally here to avoid chances of cyclic import 73 import incremental_build 74 75 p = argparse.ArgumentParser( 76 formatter_class=argparse.RawTextHelpFormatter, 77 description="" 78 + textwrap.dedent(incremental_build.__doc__) 79 + textwrap.dedent(incremental_build.main.__doc__), 80 ) 81 82 cuj_list = "\n".join( 83 [f"{i:2}: {cujgroup.description}" for i, cujgroup in enumerate(cujgroups)] 84 ) 85 p.add_argument( 86 "-c", 87 "--cujs", 88 nargs="*", 89 type=validate_cujgroups, 90 help="Index number(s) for the CUJ(s) from the following list. " 91 "Or substring matches for the CUJ description." 92 f"Note the ordering will be respected:\n{cuj_list}", 93 ) 94 p.add_argument( 95 "--no-warmup", 96 default=False, 97 action="store_true", 98 help="skip warmup builds; this can skew your results for the first CUJ you run.", 99 ) 100 p.add_argument( 101 "-t", 102 "--tag", 103 type=str, 104 default="", 105 help="Any additional tag for this set of builds, this helps " 106 "distinguish the new data from previously collected data, " 107 "useful for comparative analysis", 108 ) 109 110 log_levels = dict(getattr(logging, "_levelToName")).values() 111 p.add_argument( 112 "-v", 113 "--verbosity", 114 choices=log_levels, 115 default="INFO", 116 help="Log level. Defaults to %(default)s", 117 ) 118 default_log_dir = util.get_default_log_dir() 119 p.add_argument( 120 "-l", 121 "--log-dir", 122 type=Path, 123 default=default_log_dir, 124 help=textwrap.dedent( 125 """\ 126 Directory for timing logs. Defaults to %(default)s 127 TIPS: 128 1 Specify a directory outside of the source tree 129 2 To view key metrics in metrics.csv: 130 {} 131 3 To view column headers: 132 {} 133 """ 134 ).format( 135 textwrap.indent( 136 util.get_cmd_to_display_tabulated_metrics(default_log_dir, False), 137 " " * 4, 138 ), 139 textwrap.indent(util.get_csv_columns_cmd(default_log_dir), " " * 4), 140 ), 141 ) 142 def_build_types = [ 143 BuildType.SOONG_ONLY, 144 ] 145 p.add_argument( 146 "-b", 147 "--build-types", 148 nargs="+", 149 type=BuildType.from_flag, 150 default=[def_build_types], 151 help=f"Defaults to {[b.to_flag() for b in def_build_types]}. " 152 f"Choose from {[e.name.lower() for e in BuildType]}", 153 ) 154 p.add_argument( 155 "--ignore-repo-diff", 156 default=False, 157 action="store_true", 158 help='Skip "repo status" check', 159 ) 160 p.add_argument( 161 "--append-csv", 162 default=False, 163 action="store_true", 164 help="Add results to existing spreadsheet", 165 ) 166 p.add_argument( 167 "targets", 168 nargs="*", 169 default=["nothing"], 170 help='Targets to run, e.g. "libc adbd". ' "Defaults to %(default)s", 171 ) 172 p.add_argument( 173 "--ci-mode", 174 default=False, 175 action="store_true", 176 help="Only use it for CI runs.It will copy the " 177 "first metrics after warmup to the logs directory in CI", 178 ) 179 180 options = p.parse_args() 181 182 if options.verbosity: 183 logging.root.setLevel(options.verbosity) 184 185 chosen_cujgroups: tuple[int, ...] = ( 186 tuple(int(i) for sublist in options.cujs for i in sublist) 187 if options.cujs 188 else tuple() 189 ) 190 191 bazel_labels: list[str] = [ 192 target for target in options.targets if target.startswith("//") 193 ] 194 if 0 < len(bazel_labels) < len(options.targets): 195 logging.critical( 196 f"Don't mix bazel labels {bazel_labels} with soong targets " 197 f"{[t for t in options.targets if t not in bazel_labels]}" 198 ) 199 sys.exit(1) 200 if os.getenv("BUILD_BROKEN_DISABLE_BAZEL") is not None: 201 raise RuntimeError( 202 f"use -b {BuildType.SOONG_ONLY.to_flag()} " 203 f"instead of BUILD_BROKEN_DISABLE_BAZEL" 204 ) 205 build_types: tuple[BuildType, ...] = tuple( 206 BuildType(i) for sublist in options.build_types for i in sublist 207 ) 208 if len(bazel_labels) > 0: 209 non_b = [ 210 b.name for b in build_types if b != BuildType.B_BUILD and b != BuildType.B_ANDROID 211 ] 212 if len(non_b): 213 raise RuntimeError(f"bazel labels can not be used with {non_b}") 214 215 pretty_str = "\n".join( 216 [f"{i:2}: {cujgroups[i].description}" for i in chosen_cujgroups] 217 ) 218 logging.info(f"%d CUJs chosen:\n%s", len(chosen_cujgroups), pretty_str) 219 220 if not options.ignore_repo_diff and util.has_uncommitted_changes(): 221 error_message = ( 222 "THERE ARE UNCOMMITTED CHANGES (TIP: repo status). " 223 "Use --ignore-repo-diff to skip this check." 224 ) 225 if not util.is_interactive_shell(): 226 logging.critical(error_message) 227 sys.exit(1) 228 logging.error(error_message) 229 response = input("Continue?[Y/n]") 230 if response.upper() != "Y": 231 sys.exit(1) 232 233 log_dir = Path(options.log_dir).resolve() 234 if not options.append_csv and log_dir.exists(): 235 error_message = ( 236 f"{log_dir} already exists. " 237 "Use --append-csv to skip this check." 238 "Consider --tag to your new runs" 239 ) 240 if not util.is_interactive_shell(): 241 logging.critical(error_message) 242 sys.exit(1) 243 logging.error(error_message) 244 response = input("Continue?[Y/n]") 245 if response.upper() != "Y": 246 sys.exit(1) 247 248 if log_dir.is_relative_to(util.get_top_dir()): 249 logging.critical( 250 f"choose a log_dir outside the source tree; " 251 f"'{options.log_dir}' resolves to {log_dir}" 252 ) 253 sys.exit(1) 254 255 if options.ci_mode: 256 if len(chosen_cujgroups) > 1: 257 logging.critical( 258 "CI mode can only allow one cuj group. " 259 "Remove --ci-mode flag to skip this check." 260 ) 261 sys.exit(1) 262 if len(build_types) > 1: 263 logging.critical( 264 "CI mode can only allow one build type. " 265 "Remove --ci-mode flag to skip this check." 266 ) 267 sys.exit(1) 268 269 if options.no_warmup: 270 logging.warning( 271 "WARMUP runs will be skipped. Note this is not advised " 272 "as it gives unreliable results." 273 ) 274 return UserInput( 275 build_types=build_types, 276 chosen_cujgroups=chosen_cujgroups, 277 tag=options.tag, 278 log_dir=Path(options.log_dir).resolve(), 279 no_warmup=options.no_warmup, 280 targets=options.targets, 281 ci_mode=options.ci_mode, 282 ) 283