1*7594170eSAndroid Build Coastguard Worker# Copyright (C) 2022 The Android Open Source Project 2*7594170eSAndroid Build Coastguard Worker# 3*7594170eSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*7594170eSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*7594170eSAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*7594170eSAndroid Build Coastguard Worker# 7*7594170eSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*7594170eSAndroid Build Coastguard Worker# 9*7594170eSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*7594170eSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*7594170eSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*7594170eSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*7594170eSAndroid Build Coastguard Worker# limitations under the License. 14*7594170eSAndroid Build Coastguard Workerimport csv 15*7594170eSAndroid Build Coastguard Workerimport dataclasses 16*7594170eSAndroid Build Coastguard Workerimport datetime 17*7594170eSAndroid Build Coastguard Workerimport enum 18*7594170eSAndroid Build Coastguard Workerimport functools 19*7594170eSAndroid Build Coastguard Workerimport json 20*7594170eSAndroid Build Coastguard Workerimport logging 21*7594170eSAndroid Build Coastguard Workerimport os 22*7594170eSAndroid Build Coastguard Workerimport re 23*7594170eSAndroid Build Coastguard Workerimport subprocess 24*7594170eSAndroid Build Coastguard Workerimport sys 25*7594170eSAndroid Build Coastguard Workerfrom datetime import date 26*7594170eSAndroid Build Coastguard Workerfrom pathlib import Path 27*7594170eSAndroid Build Coastguard Workerimport textwrap 28*7594170eSAndroid Build Coastguard Workerfrom typing import Callable 29*7594170eSAndroid Build Coastguard Workerfrom typing import Final 30*7594170eSAndroid Build Coastguard Workerfrom typing import Generator 31*7594170eSAndroid Build Coastguard Workerfrom typing import TypeVar 32*7594170eSAndroid Build Coastguard Worker 33*7594170eSAndroid Build Coastguard WorkerINDICATOR_FILE: Final[str] = "build/soong/soong_ui.bash" 34*7594170eSAndroid Build Coastguard Worker# metrics.csv is written to but not read by this tool. 35*7594170eSAndroid Build Coastguard Worker# It's supposed to be viewed as a spreadsheet that compiles data from multiple 36*7594170eSAndroid Build Coastguard Worker# builds to be analyzed by other external tools. 37*7594170eSAndroid Build Coastguard WorkerMETRICS_TABLE: Final[str] = "metrics.csv" 38*7594170eSAndroid Build Coastguard WorkerRUN_DIR_PREFIX: Final[str] = "run" 39*7594170eSAndroid Build Coastguard WorkerBUILD_INFO_JSON: Final[str] = "build_info.json" 40*7594170eSAndroid Build Coastguard Worker 41*7594170eSAndroid Build Coastguard Worker 42*7594170eSAndroid Build Coastguard Worker@functools.cache 43*7594170eSAndroid Build Coastguard Workerdef _is_important(column) -> bool: 44*7594170eSAndroid Build Coastguard Worker patterns = { 45*7594170eSAndroid Build Coastguard Worker "actions", 46*7594170eSAndroid Build Coastguard Worker r"build_ninja_(?:hash|size)", 47*7594170eSAndroid Build Coastguard Worker "build_type", 48*7594170eSAndroid Build Coastguard Worker "cquery_out_size", 49*7594170eSAndroid Build Coastguard Worker "description", 50*7594170eSAndroid Build Coastguard Worker "log", 51*7594170eSAndroid Build Coastguard Worker r"mixed\.enabled", 52*7594170eSAndroid Build Coastguard Worker "targets", 53*7594170eSAndroid Build Coastguard Worker # the following are time-based values 54*7594170eSAndroid Build Coastguard Worker "bp2build", 55*7594170eSAndroid Build Coastguard Worker r"kati/kati (?:build|package)", 56*7594170eSAndroid Build Coastguard Worker "ninja/ninja", 57*7594170eSAndroid Build Coastguard Worker "soong/soong", 58*7594170eSAndroid Build Coastguard Worker r"soong_build/\*(?:\.bazel)?", 59*7594170eSAndroid Build Coastguard Worker "symlink_forest", 60*7594170eSAndroid Build Coastguard Worker "time", 61*7594170eSAndroid Build Coastguard Worker } 62*7594170eSAndroid Build Coastguard Worker for pattern in patterns: 63*7594170eSAndroid Build Coastguard Worker if re.fullmatch(pattern, column): 64*7594170eSAndroid Build Coastguard Worker return True 65*7594170eSAndroid Build Coastguard Worker return False 66*7594170eSAndroid Build Coastguard Worker 67*7594170eSAndroid Build Coastguard Worker 68*7594170eSAndroid Build Coastguard Workerclass BuildResult(enum.Enum): 69*7594170eSAndroid Build Coastguard Worker SUCCESS = enum.auto() 70*7594170eSAndroid Build Coastguard Worker FAILED = enum.auto() 71*7594170eSAndroid Build Coastguard Worker TEST_FAILURE = enum.auto() 72*7594170eSAndroid Build Coastguard Worker 73*7594170eSAndroid Build Coastguard Worker 74*7594170eSAndroid Build Coastguard Workerclass BuildType(enum.Enum): 75*7594170eSAndroid Build Coastguard Worker # see https://docs.python.org/3/library/enum.html#enum.Enum._ignore_ 76*7594170eSAndroid Build Coastguard Worker _ignore_ = "_soong_cmd" 77*7594170eSAndroid Build Coastguard Worker # _soong_cmd_ will not be listed as an enum constant because of `_ignore_` 78*7594170eSAndroid Build Coastguard Worker _soong_cmd = ["build/soong/soong_ui.bash", "--make-mode", "--skip-soong-tests"] 79*7594170eSAndroid Build Coastguard Worker 80*7594170eSAndroid Build Coastguard Worker SOONG_ONLY = [*_soong_cmd, "BUILD_BROKEN_DISABLE_BAZEL=true"] 81*7594170eSAndroid Build Coastguard Worker MIXED_PROD = [*_soong_cmd, "--bazel-mode"] 82*7594170eSAndroid Build Coastguard Worker MIXED_STAGING = [*_soong_cmd, "--bazel-mode-staging"] 83*7594170eSAndroid Build Coastguard Worker B_BUILD = ["build/bazel/bin/b", "build"] 84*7594170eSAndroid Build Coastguard Worker B_ANDROID = [*B_BUILD, "--config=android"] 85*7594170eSAndroid Build Coastguard Worker 86*7594170eSAndroid Build Coastguard Worker @staticmethod 87*7594170eSAndroid Build Coastguard Worker def from_flag(s: str) -> list["BuildType"]: 88*7594170eSAndroid Build Coastguard Worker chosen: list[BuildType] = [] 89*7594170eSAndroid Build Coastguard Worker for e in BuildType: 90*7594170eSAndroid Build Coastguard Worker if s.lower() in e.name.lower(): 91*7594170eSAndroid Build Coastguard Worker chosen.append(e) 92*7594170eSAndroid Build Coastguard Worker if len(chosen) == 0: 93*7594170eSAndroid Build Coastguard Worker raise RuntimeError(f"no such build type: {s}") 94*7594170eSAndroid Build Coastguard Worker return chosen 95*7594170eSAndroid Build Coastguard Worker 96*7594170eSAndroid Build Coastguard Worker def to_flag(self): 97*7594170eSAndroid Build Coastguard Worker return self.name.lower() 98*7594170eSAndroid Build Coastguard Worker 99*7594170eSAndroid Build Coastguard Worker 100*7594170eSAndroid Build Coastguard WorkerCURRENT_BUILD_TYPE: BuildType 101*7594170eSAndroid Build Coastguard Worker"""global state capturing what the current build type is""" 102*7594170eSAndroid Build Coastguard Worker 103*7594170eSAndroid Build Coastguard Worker 104*7594170eSAndroid Build Coastguard Worker@dataclasses.dataclass 105*7594170eSAndroid Build Coastguard Workerclass BuildInfo: 106*7594170eSAndroid Build Coastguard Worker actions: int 107*7594170eSAndroid Build Coastguard Worker bp_size_total: int 108*7594170eSAndroid Build Coastguard Worker build_ninja_hash: str # hash 109*7594170eSAndroid Build Coastguard Worker build_ninja_size: int 110*7594170eSAndroid Build Coastguard Worker build_result: BuildResult 111*7594170eSAndroid Build Coastguard Worker build_type: BuildType 112*7594170eSAndroid Build Coastguard Worker bz_size_total: int 113*7594170eSAndroid Build Coastguard Worker cquery_out_size: int 114*7594170eSAndroid Build Coastguard Worker description: str 115*7594170eSAndroid Build Coastguard Worker product: str 116*7594170eSAndroid Build Coastguard Worker targets: tuple[str, ...] 117*7594170eSAndroid Build Coastguard Worker time: datetime.timedelta 118*7594170eSAndroid Build Coastguard Worker tag: str = None 119*7594170eSAndroid Build Coastguard Worker rebuild: bool = False 120*7594170eSAndroid Build Coastguard Worker warmup: bool = False 121*7594170eSAndroid Build Coastguard Worker 122*7594170eSAndroid Build Coastguard Worker 123*7594170eSAndroid Build Coastguard Workerclass CustomEncoder(json.JSONEncoder): 124*7594170eSAndroid Build Coastguard Worker def default(self, obj): 125*7594170eSAndroid Build Coastguard Worker if isinstance(obj, BuildInfo): 126*7594170eSAndroid Build Coastguard Worker return self.default(dataclasses.asdict(obj)) 127*7594170eSAndroid Build Coastguard Worker if isinstance(obj, dict): 128*7594170eSAndroid Build Coastguard Worker return {k: v for k, v in obj.items() if v is not None} 129*7594170eSAndroid Build Coastguard Worker if isinstance(obj, datetime.timedelta): 130*7594170eSAndroid Build Coastguard Worker return hhmmss(obj, decimal_precision=True) 131*7594170eSAndroid Build Coastguard Worker if isinstance(obj, enum.Enum): 132*7594170eSAndroid Build Coastguard Worker return obj.name 133*7594170eSAndroid Build Coastguard Worker return json.JSONEncoder.default(self, obj) 134*7594170eSAndroid Build Coastguard Worker 135*7594170eSAndroid Build Coastguard Worker 136*7594170eSAndroid Build Coastguard Workerdef get_csv_columns_cmd(d: Path) -> str: 137*7594170eSAndroid Build Coastguard Worker """ 138*7594170eSAndroid Build Coastguard Worker :param d: the log directory 139*7594170eSAndroid Build Coastguard Worker :return: a quick shell command to view columns in metrics.csv 140*7594170eSAndroid Build Coastguard Worker """ 141*7594170eSAndroid Build Coastguard Worker csv_file = d.joinpath(METRICS_TABLE).resolve() 142*7594170eSAndroid Build Coastguard Worker return f'head -n 1 "{csv_file}" | sed "s/,/\\n/g" | less -N' 143*7594170eSAndroid Build Coastguard Worker 144*7594170eSAndroid Build Coastguard Worker 145*7594170eSAndroid Build Coastguard Workerdef get_cmd_to_display_tabulated_metrics(d: Path, ci_mode: bool) -> str: 146*7594170eSAndroid Build Coastguard Worker """ 147*7594170eSAndroid Build Coastguard Worker :param d: the log directory 148*7594170eSAndroid Build Coastguard Worker :param ci_mode: if true all top-level events are displayed 149*7594170eSAndroid Build Coastguard Worker :return: a quick shell command to view some collected metrics 150*7594170eSAndroid Build Coastguard Worker """ 151*7594170eSAndroid Build Coastguard Worker csv_file = d.joinpath(METRICS_TABLE) 152*7594170eSAndroid Build Coastguard Worker headers: list[str] = [] 153*7594170eSAndroid Build Coastguard Worker if csv_file.exists(): 154*7594170eSAndroid Build Coastguard Worker with open(csv_file) as r: 155*7594170eSAndroid Build Coastguard Worker reader = csv.DictReader(r) 156*7594170eSAndroid Build Coastguard Worker headers = reader.fieldnames or [] 157*7594170eSAndroid Build Coastguard Worker 158*7594170eSAndroid Build Coastguard Worker cols: list[int] = [i + 1 for i, h in enumerate(headers) if _is_important(h)] 159*7594170eSAndroid Build Coastguard Worker if ci_mode: 160*7594170eSAndroid Build Coastguard Worker # ci mode contains all information about the top level events 161*7594170eSAndroid Build Coastguard Worker for i, h in enumerate(headers): 162*7594170eSAndroid Build Coastguard Worker if re.match(r"^\w+/[^.]+$", h) and i not in cols: 163*7594170eSAndroid Build Coastguard Worker cols.append(i) 164*7594170eSAndroid Build Coastguard Worker 165*7594170eSAndroid Build Coastguard Worker if len(cols) == 0: 166*7594170eSAndroid Build Coastguard Worker # syntactically correct command even if the file doesn't exist 167*7594170eSAndroid Build Coastguard Worker cols.append(1) 168*7594170eSAndroid Build Coastguard Worker 169*7594170eSAndroid Build Coastguard Worker f = ",".join(str(i) for i in cols) 170*7594170eSAndroid Build Coastguard Worker # the sed invocations are to account for 171*7594170eSAndroid Build Coastguard Worker # https://man7.org/linux/man-pages/man1/column.1.html#BUGS 172*7594170eSAndroid Build Coastguard Worker # example: if a row were `,,,hi,,,,` 173*7594170eSAndroid Build Coastguard Worker # the successive sed conversions would be 174*7594170eSAndroid Build Coastguard Worker # `,,,hi,,,,` => 175*7594170eSAndroid Build Coastguard Worker # `,--,,hi,--,,--,` => 176*7594170eSAndroid Build Coastguard Worker # `,--,--,hi,--,--,--,` => 177*7594170eSAndroid Build Coastguard Worker # `--,--,--,hi,--,--,--,` => 178*7594170eSAndroid Build Coastguard Worker # `--,--,--,hi,--,--,--,--` 179*7594170eSAndroid Build Coastguard Worker # Note sed doesn't support lookahead or lookbehinds 180*7594170eSAndroid Build Coastguard Worker return textwrap.dedent( 181*7594170eSAndroid Build Coastguard Worker f"""\ 182*7594170eSAndroid Build Coastguard Worker grep -v "WARMUP\\|rebuild-" "{csv_file}" | \\ 183*7594170eSAndroid Build Coastguard Worker sed "s/,,/,--,/g" | \\ 184*7594170eSAndroid Build Coastguard Worker sed "s/,,/,--,/g" | \\ 185*7594170eSAndroid Build Coastguard Worker sed "s/^,/--,/" | \\ 186*7594170eSAndroid Build Coastguard Worker sed "s/,$/,--/" | \\ 187*7594170eSAndroid Build Coastguard Worker cut -d, -f{f} | column -t -s,""" 188*7594170eSAndroid Build Coastguard Worker ) 189*7594170eSAndroid Build Coastguard Worker 190*7594170eSAndroid Build Coastguard Worker 191*7594170eSAndroid Build Coastguard Worker@functools.cache 192*7594170eSAndroid Build Coastguard Workerdef get_top_dir(d: Path = Path(".").resolve()) -> Path: 193*7594170eSAndroid Build Coastguard Worker """Get the path to the root of the Android source tree""" 194*7594170eSAndroid Build Coastguard Worker top_dir = os.environ.get("ANDROID_BUILD_TOP") 195*7594170eSAndroid Build Coastguard Worker if top_dir: 196*7594170eSAndroid Build Coastguard Worker logging.info("ANDROID BUILD TOP = %s", d) 197*7594170eSAndroid Build Coastguard Worker return Path(top_dir).resolve() 198*7594170eSAndroid Build Coastguard Worker logging.debug("Checking if Android source tree root is %s", d) 199*7594170eSAndroid Build Coastguard Worker if d.parent == d: 200*7594170eSAndroid Build Coastguard Worker sys.exit( 201*7594170eSAndroid Build Coastguard Worker "Unable to find ROOT source directory, specifically," 202*7594170eSAndroid Build Coastguard Worker f"{INDICATOR_FILE} not found anywhere. " 203*7594170eSAndroid Build Coastguard Worker "Try `m nothing` and `repo sync`" 204*7594170eSAndroid Build Coastguard Worker ) 205*7594170eSAndroid Build Coastguard Worker if d.joinpath(INDICATOR_FILE).is_file(): 206*7594170eSAndroid Build Coastguard Worker logging.info("ANDROID BUILD TOP assumed to be %s", d) 207*7594170eSAndroid Build Coastguard Worker return d 208*7594170eSAndroid Build Coastguard Worker return get_top_dir(d.parent) 209*7594170eSAndroid Build Coastguard Worker 210*7594170eSAndroid Build Coastguard Worker 211*7594170eSAndroid Build Coastguard Worker@functools.cache 212*7594170eSAndroid Build Coastguard Workerdef get_out_dir() -> Path: 213*7594170eSAndroid Build Coastguard Worker out_dir = os.environ.get("OUT_DIR") 214*7594170eSAndroid Build Coastguard Worker return Path(out_dir).resolve() if out_dir else get_top_dir().joinpath("out") 215*7594170eSAndroid Build Coastguard Worker 216*7594170eSAndroid Build Coastguard Worker 217*7594170eSAndroid Build Coastguard Worker@functools.cache 218*7594170eSAndroid Build Coastguard Workerdef get_default_log_dir() -> Path: 219*7594170eSAndroid Build Coastguard Worker return get_top_dir().parent.joinpath(f'timing-{date.today().strftime("%b%d")}') 220*7594170eSAndroid Build Coastguard Worker 221*7594170eSAndroid Build Coastguard Worker 222*7594170eSAndroid Build Coastguard Workerdef is_interactive_shell() -> bool: 223*7594170eSAndroid Build Coastguard Worker return ( 224*7594170eSAndroid Build Coastguard Worker sys.__stdin__.isatty() and sys.__stdout__.isatty() and sys.__stderr__.isatty() 225*7594170eSAndroid Build Coastguard Worker ) 226*7594170eSAndroid Build Coastguard Worker 227*7594170eSAndroid Build Coastguard Worker 228*7594170eSAndroid Build Coastguard Worker# see test_next_path_helper() for examples 229*7594170eSAndroid Build Coastguard Workerdef _next_path_helper(basename: str) -> str: 230*7594170eSAndroid Build Coastguard Worker def padded_suffix(i: int) -> str: 231*7594170eSAndroid Build Coastguard Worker return f"{i:03d}" 232*7594170eSAndroid Build Coastguard Worker 233*7594170eSAndroid Build Coastguard Worker # find the sequence digits following "-" and preceding the file extension 234*7594170eSAndroid Build Coastguard Worker name = re.sub( 235*7594170eSAndroid Build Coastguard Worker r"(?<=-)(?P<suffix>\d+)$", 236*7594170eSAndroid Build Coastguard Worker lambda d: padded_suffix(int(d.group("suffix")) + 1), 237*7594170eSAndroid Build Coastguard Worker basename, 238*7594170eSAndroid Build Coastguard Worker ) 239*7594170eSAndroid Build Coastguard Worker 240*7594170eSAndroid Build Coastguard Worker if name == basename: 241*7594170eSAndroid Build Coastguard Worker # basename didn't have any numeric suffix 242*7594170eSAndroid Build Coastguard Worker name = f"{name}-{padded_suffix(1)}" 243*7594170eSAndroid Build Coastguard Worker return name 244*7594170eSAndroid Build Coastguard Worker 245*7594170eSAndroid Build Coastguard Worker 246*7594170eSAndroid Build Coastguard Workerdef next_path(path: Path) -> Generator[Path, None, None]: 247*7594170eSAndroid Build Coastguard Worker """ 248*7594170eSAndroid Build Coastguard Worker Generator for indexed paths 249*7594170eSAndroid Build Coastguard Worker :returns a new Path with an increasing number suffix to the name 250*7594170eSAndroid Build Coastguard Worker e.g. _to_file('a.txt') = a-5.txt (if a-4.txt already exists) 251*7594170eSAndroid Build Coastguard Worker """ 252*7594170eSAndroid Build Coastguard Worker while True: 253*7594170eSAndroid Build Coastguard Worker name = _next_path_helper(path.name) 254*7594170eSAndroid Build Coastguard Worker path = path.parent.joinpath(name) 255*7594170eSAndroid Build Coastguard Worker if not path.exists(): 256*7594170eSAndroid Build Coastguard Worker yield path 257*7594170eSAndroid Build Coastguard Worker 258*7594170eSAndroid Build Coastguard Worker 259*7594170eSAndroid Build Coastguard Workerdef has_uncommitted_changes() -> bool: 260*7594170eSAndroid Build Coastguard Worker """ 261*7594170eSAndroid Build Coastguard Worker effectively a quick 'repo status' that fails fast 262*7594170eSAndroid Build Coastguard Worker if any project has uncommitted changes 263*7594170eSAndroid Build Coastguard Worker """ 264*7594170eSAndroid Build Coastguard Worker for cmd in ["diff", "diff --staged"]: 265*7594170eSAndroid Build Coastguard Worker diff = subprocess.run( 266*7594170eSAndroid Build Coastguard Worker f"repo forall -c git {cmd} --quiet --exit-code", 267*7594170eSAndroid Build Coastguard Worker cwd=get_top_dir(), 268*7594170eSAndroid Build Coastguard Worker shell=True, 269*7594170eSAndroid Build Coastguard Worker text=True, 270*7594170eSAndroid Build Coastguard Worker capture_output=True, 271*7594170eSAndroid Build Coastguard Worker ) 272*7594170eSAndroid Build Coastguard Worker if diff.returncode != 0: 273*7594170eSAndroid Build Coastguard Worker logging.error(diff.stderr) 274*7594170eSAndroid Build Coastguard Worker return True 275*7594170eSAndroid Build Coastguard Worker return False 276*7594170eSAndroid Build Coastguard Worker 277*7594170eSAndroid Build Coastguard Worker 278*7594170eSAndroid Build Coastguard Workerdef hhmmss(t: datetime.timedelta, decimal_precision: bool = False) -> str: 279*7594170eSAndroid Build Coastguard Worker """pretty prints time periods, prefers mm:ss.sss and resorts to hh:mm:ss.sss 280*7594170eSAndroid Build Coastguard Worker only if t >= 1 hour. 281*7594170eSAndroid Build Coastguard Worker Examples(non_decimal_precision): 02:12, 1:12:13 282*7594170eSAndroid Build Coastguard Worker Examples(decimal_precision): 02:12.231, 00:00.512, 00:01:11.321, 1:12:13.121 283*7594170eSAndroid Build Coastguard Worker See unit test for more examples.""" 284*7594170eSAndroid Build Coastguard Worker h, f = divmod(t.seconds, 60 * 60) 285*7594170eSAndroid Build Coastguard Worker m, f = divmod(f, 60) 286*7594170eSAndroid Build Coastguard Worker s = f + t.microseconds / 1000_000 287*7594170eSAndroid Build Coastguard Worker if decimal_precision: 288*7594170eSAndroid Build Coastguard Worker return f"{h}:{m:02d}:{s:06.3f}" if h else f"{m:02d}:{s:06.3f}" 289*7594170eSAndroid Build Coastguard Worker else: 290*7594170eSAndroid Build Coastguard Worker return f"{h}:{m:02}:{s:02.0f}" if h else f"{m:02}:{s:02.0f}" 291*7594170eSAndroid Build Coastguard Worker 292*7594170eSAndroid Build Coastguard Worker 293*7594170eSAndroid Build Coastguard Workerdef period_to_seconds(s: str) -> float: 294*7594170eSAndroid Build Coastguard Worker """converts a time period into seconds. The input is expected to be in the 295*7594170eSAndroid Build Coastguard Worker format used by hhmmss(). 296*7594170eSAndroid Build Coastguard Worker Example: 02:04 -> 125 297*7594170eSAndroid Build Coastguard Worker See unit test for more examples.""" 298*7594170eSAndroid Build Coastguard Worker if s == "": 299*7594170eSAndroid Build Coastguard Worker return 0.0 300*7594170eSAndroid Build Coastguard Worker acc = 0.0 301*7594170eSAndroid Build Coastguard Worker while True: 302*7594170eSAndroid Build Coastguard Worker [left, *right] = s.split(":", 1) 303*7594170eSAndroid Build Coastguard Worker acc = acc * 60 + float(left) 304*7594170eSAndroid Build Coastguard Worker if right: 305*7594170eSAndroid Build Coastguard Worker s = right[0] 306*7594170eSAndroid Build Coastguard Worker else: 307*7594170eSAndroid Build Coastguard Worker return acc 308*7594170eSAndroid Build Coastguard Worker 309*7594170eSAndroid Build Coastguard Worker 310*7594170eSAndroid Build Coastguard WorkerR = TypeVar("R") 311*7594170eSAndroid Build Coastguard Worker 312*7594170eSAndroid Build Coastguard Worker 313*7594170eSAndroid Build Coastguard Workerdef groupby(xs: list[R], key: Callable[[R], str]) -> dict[str, list[R]]: 314*7594170eSAndroid Build Coastguard Worker grouped: dict[str, list[R]] = {} 315*7594170eSAndroid Build Coastguard Worker for x in xs: 316*7594170eSAndroid Build Coastguard Worker grouped.setdefault(key(x), []).append(x) 317*7594170eSAndroid Build Coastguard Worker return grouped 318