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 argparse 15*7594170eSAndroid Build Coastguard Workerimport csv 16*7594170eSAndroid Build Coastguard Workerimport datetime 17*7594170eSAndroid Build Coastguard Workerimport enum 18*7594170eSAndroid Build Coastguard Workerimport logging 19*7594170eSAndroid Build Coastguard Workerimport re 20*7594170eSAndroid Build Coastguard Workerimport statistics 21*7594170eSAndroid Build Coastguard Workerimport subprocess 22*7594170eSAndroid Build Coastguard Workerimport textwrap 23*7594170eSAndroid Build Coastguard Workerfrom pathlib import Path 24*7594170eSAndroid Build Coastguard Worker 25*7594170eSAndroid Build Coastguard Workerfrom typing import Iterable, NewType, TextIO, TypeVar 26*7594170eSAndroid Build Coastguard Worker 27*7594170eSAndroid Build Coastguard Workerimport plot_metrics 28*7594170eSAndroid Build Coastguard Workerimport util 29*7594170eSAndroid Build Coastguard Worker 30*7594170eSAndroid Build Coastguard WorkerRow = NewType("Row", dict[str, str]) 31*7594170eSAndroid Build Coastguard Worker 32*7594170eSAndroid Build Coastguard Worker 33*7594170eSAndroid Build Coastguard Worker# modify the row in-place 34*7594170eSAndroid Build Coastguard Workerdef _normalize_rebuild(row: Row): 35*7594170eSAndroid Build Coastguard Worker row["description"] = re.sub( 36*7594170eSAndroid Build Coastguard Worker r"^(rebuild)-[\d+](.*)$", "\\1\\2", row.get("description") 37*7594170eSAndroid Build Coastguard Worker ) 38*7594170eSAndroid Build Coastguard Worker 39*7594170eSAndroid Build Coastguard Workerdef _get_tagged_build_type(row: Row) -> str: 40*7594170eSAndroid Build Coastguard Worker build_type = row.get("build_type") 41*7594170eSAndroid Build Coastguard Worker tag = row.get("tag") 42*7594170eSAndroid Build Coastguard Worker return build_type if not tag else f"{build_type}:{tag}" 43*7594170eSAndroid Build Coastguard Worker 44*7594170eSAndroid Build Coastguard Workerdef _build_types(rows: list[Row]) -> list[str]: 45*7594170eSAndroid Build Coastguard Worker return list(dict.fromkeys(_get_tagged_build_type(row) for row in rows).keys()) 46*7594170eSAndroid Build Coastguard Worker 47*7594170eSAndroid Build Coastguard Worker 48*7594170eSAndroid Build Coastguard Workerdef _write_table(lines: list[list[str]]) -> str: 49*7594170eSAndroid Build Coastguard Worker def join_cells(line: list[str]) -> str: 50*7594170eSAndroid Build Coastguard Worker return ",".join(str(cell) for cell in line) 51*7594170eSAndroid Build Coastguard Worker 52*7594170eSAndroid Build Coastguard Worker return "\n".join(join_cells(line) for line in lines) + "\n" 53*7594170eSAndroid Build Coastguard Worker 54*7594170eSAndroid Build Coastguard Worker 55*7594170eSAndroid Build Coastguard Workerclass Aggregation(enum.Enum): 56*7594170eSAndroid Build Coastguard Worker # naked function as value assignment doesn't seem to work, 57*7594170eSAndroid Build Coastguard Worker # hence wrapping in a singleton tuple 58*7594170eSAndroid Build Coastguard Worker AVG = (statistics.mean,) 59*7594170eSAndroid Build Coastguard Worker MAX = (max,) 60*7594170eSAndroid Build Coastguard Worker MEDIAN = (statistics.median,) 61*7594170eSAndroid Build Coastguard Worker MIN = (min,) 62*7594170eSAndroid Build Coastguard Worker STDEV = (statistics.stdev,) 63*7594170eSAndroid Build Coastguard Worker 64*7594170eSAndroid Build Coastguard Worker N = TypeVar("N", int, float) 65*7594170eSAndroid Build Coastguard Worker 66*7594170eSAndroid Build Coastguard Worker def fn(self, xs: Iterable[N]) -> N: 67*7594170eSAndroid Build Coastguard Worker return self.value[0](xs) 68*7594170eSAndroid Build Coastguard Worker 69*7594170eSAndroid Build Coastguard Worker 70*7594170eSAndroid Build Coastguard Workerdef _aggregate(prop: str, rows: list[Row], agg: Aggregation) -> str: 71*7594170eSAndroid Build Coastguard Worker """ 72*7594170eSAndroid Build Coastguard Worker compute the requested aggregation 73*7594170eSAndroid Build Coastguard Worker :return formatted values 74*7594170eSAndroid Build Coastguard Worker """ 75*7594170eSAndroid Build Coastguard Worker if not rows: 76*7594170eSAndroid Build Coastguard Worker return "" 77*7594170eSAndroid Build Coastguard Worker vals = [x.get(prop) for x in rows] 78*7594170eSAndroid Build Coastguard Worker vals = [x for x in vals if bool(x)] 79*7594170eSAndroid Build Coastguard Worker if len(vals) == 0: 80*7594170eSAndroid Build Coastguard Worker return "" 81*7594170eSAndroid Build Coastguard Worker 82*7594170eSAndroid Build Coastguard Worker isnum = any(x.isnumeric() for x in vals) 83*7594170eSAndroid Build Coastguard Worker if isnum: 84*7594170eSAndroid Build Coastguard Worker vals = [int(x) for x in vals] 85*7594170eSAndroid Build Coastguard Worker cell = f"{(agg.fn(vals)):.0f}" 86*7594170eSAndroid Build Coastguard Worker else: 87*7594170eSAndroid Build Coastguard Worker vals = [util.period_to_seconds(x) for x in vals] 88*7594170eSAndroid Build Coastguard Worker cell = util.hhmmss(datetime.timedelta(seconds=agg.fn(vals))) 89*7594170eSAndroid Build Coastguard Worker 90*7594170eSAndroid Build Coastguard Worker if len(vals) > 1: 91*7594170eSAndroid Build Coastguard Worker cell = f"{cell}[N={len(vals)}]" 92*7594170eSAndroid Build Coastguard Worker return cell 93*7594170eSAndroid Build Coastguard Worker 94*7594170eSAndroid Build Coastguard Worker 95*7594170eSAndroid Build Coastguard Workerdef acceptable(row: Row) -> bool: 96*7594170eSAndroid Build Coastguard Worker failure = row.get("build_result") == "FAILED" 97*7594170eSAndroid Build Coastguard Worker if failure: 98*7594170eSAndroid Build Coastguard Worker logging.error(f"Skipping {row.get('description')}/{row.get('build_type')}") 99*7594170eSAndroid Build Coastguard Worker return not failure 100*7594170eSAndroid Build Coastguard Worker 101*7594170eSAndroid Build Coastguard Worker 102*7594170eSAndroid Build Coastguard Workerdef summarize_helper(metrics: TextIO, regex: str, agg: Aggregation) -> dict[str, str]: 103*7594170eSAndroid Build Coastguard Worker """ 104*7594170eSAndroid Build Coastguard Worker Args: 105*7594170eSAndroid Build Coastguard Worker metrics: csv detailed input, each row corresponding to a build 106*7594170eSAndroid Build Coastguard Worker regex: regex matching properties to be summarized 107*7594170eSAndroid Build Coastguard Worker agg: aggregation to use 108*7594170eSAndroid Build Coastguard Worker """ 109*7594170eSAndroid Build Coastguard Worker reader: csv.DictReader = csv.DictReader(metrics) 110*7594170eSAndroid Build Coastguard Worker 111*7594170eSAndroid Build Coastguard Worker # get all matching properties 112*7594170eSAndroid Build Coastguard Worker p = re.compile(regex) 113*7594170eSAndroid Build Coastguard Worker properties = [f for f in reader.fieldnames if p.search(f)] 114*7594170eSAndroid Build Coastguard Worker if len(properties) == 0: 115*7594170eSAndroid Build Coastguard Worker logging.error("no matching properties found") 116*7594170eSAndroid Build Coastguard Worker return {} 117*7594170eSAndroid Build Coastguard Worker 118*7594170eSAndroid Build Coastguard Worker all_rows: list[Row] = [row for row in reader if acceptable(row)] 119*7594170eSAndroid Build Coastguard Worker for row in all_rows: 120*7594170eSAndroid Build Coastguard Worker _normalize_rebuild(row) 121*7594170eSAndroid Build Coastguard Worker build_types: list[str] = _build_types(all_rows) 122*7594170eSAndroid Build Coastguard Worker by_cuj: dict[str, list[Row]] = util.groupby( 123*7594170eSAndroid Build Coastguard Worker all_rows, lambda l: l.get("description") 124*7594170eSAndroid Build Coastguard Worker ) 125*7594170eSAndroid Build Coastguard Worker 126*7594170eSAndroid Build Coastguard Worker def extract_lines_for_cuj(prop, cuj, cuj_rows) -> list[list[str]]: 127*7594170eSAndroid Build Coastguard Worker by_targets = util.groupby(cuj_rows, lambda l: l.get("targets")) 128*7594170eSAndroid Build Coastguard Worker lines = [] 129*7594170eSAndroid Build Coastguard Worker for targets, target_rows in by_targets.items(): 130*7594170eSAndroid Build Coastguard Worker by_build_type = util.groupby(target_rows, _get_tagged_build_type) 131*7594170eSAndroid Build Coastguard Worker vals = [ 132*7594170eSAndroid Build Coastguard Worker _aggregate(prop, by_build_type.get(build_type), agg) 133*7594170eSAndroid Build Coastguard Worker for build_type in build_types 134*7594170eSAndroid Build Coastguard Worker ] 135*7594170eSAndroid Build Coastguard Worker lines.append([cuj, targets, *vals]) 136*7594170eSAndroid Build Coastguard Worker return lines 137*7594170eSAndroid Build Coastguard Worker 138*7594170eSAndroid Build Coastguard Worker def tabulate(prop) -> str: 139*7594170eSAndroid Build Coastguard Worker headers = ["cuj", "targets"] + build_types 140*7594170eSAndroid Build Coastguard Worker lines: list[list[str]] = [headers] 141*7594170eSAndroid Build Coastguard Worker for cuj, cuj_rows in by_cuj.items(): 142*7594170eSAndroid Build Coastguard Worker lines.extend(extract_lines_for_cuj(prop, cuj, cuj_rows)) 143*7594170eSAndroid Build Coastguard Worker return _write_table(lines) 144*7594170eSAndroid Build Coastguard Worker 145*7594170eSAndroid Build Coastguard Worker return {prop: tabulate(prop) for prop in properties} 146*7594170eSAndroid Build Coastguard Worker 147*7594170eSAndroid Build Coastguard Worker 148*7594170eSAndroid Build Coastguard Workerdef _display_summarized_metrics(summary_csv: Path, filter_cujs: bool): 149*7594170eSAndroid Build Coastguard Worker cmd = ( 150*7594170eSAndroid Build Coastguard Worker ( 151*7594170eSAndroid Build Coastguard Worker f'grep -v "WARMUP\\|rebuild\\|revert\\|delete" {summary_csv}' 152*7594170eSAndroid Build Coastguard Worker f" | column -t -s," 153*7594170eSAndroid Build Coastguard Worker ) 154*7594170eSAndroid Build Coastguard Worker if filter_cujs 155*7594170eSAndroid Build Coastguard Worker else f"column -t -s, {summary_csv}" 156*7594170eSAndroid Build Coastguard Worker ) 157*7594170eSAndroid Build Coastguard Worker output = subprocess.check_output(cmd, shell=True, text=True) 158*7594170eSAndroid Build Coastguard Worker logging.info( 159*7594170eSAndroid Build Coastguard Worker textwrap.dedent( 160*7594170eSAndroid Build Coastguard Worker f"""\ 161*7594170eSAndroid Build Coastguard Worker %s 162*7594170eSAndroid Build Coastguard Worker %s 163*7594170eSAndroid Build Coastguard Worker """ 164*7594170eSAndroid Build Coastguard Worker ), 165*7594170eSAndroid Build Coastguard Worker cmd, 166*7594170eSAndroid Build Coastguard Worker output, 167*7594170eSAndroid Build Coastguard Worker ) 168*7594170eSAndroid Build Coastguard Worker 169*7594170eSAndroid Build Coastguard Worker 170*7594170eSAndroid Build Coastguard Workerdef summarize( 171*7594170eSAndroid Build Coastguard Worker metrics_csv: Path, 172*7594170eSAndroid Build Coastguard Worker regex: str, 173*7594170eSAndroid Build Coastguard Worker output_dir: Path, 174*7594170eSAndroid Build Coastguard Worker agg: Aggregation = Aggregation.MEDIAN, 175*7594170eSAndroid Build Coastguard Worker filter_cujs: bool = True, 176*7594170eSAndroid Build Coastguard Worker plot_format: str = "svg", 177*7594170eSAndroid Build Coastguard Worker): 178*7594170eSAndroid Build Coastguard Worker """ 179*7594170eSAndroid Build Coastguard Worker writes `summary_data` value as a csv files under `output_dir` 180*7594170eSAndroid Build Coastguard Worker if `filter_cujs` is False, then does not filter out WARMUP and rebuild cuj steps 181*7594170eSAndroid Build Coastguard Worker """ 182*7594170eSAndroid Build Coastguard Worker with open(metrics_csv, "rt") as input_file: 183*7594170eSAndroid Build Coastguard Worker summary_data = summarize_helper(input_file, regex, agg) 184*7594170eSAndroid Build Coastguard Worker for k, v in summary_data.items(): 185*7594170eSAndroid Build Coastguard Worker summary_csv = output_dir.joinpath(f"{k}.{agg.name}.csv") 186*7594170eSAndroid Build Coastguard Worker summary_csv.parent.mkdir(parents=True, exist_ok=True) 187*7594170eSAndroid Build Coastguard Worker with open(summary_csv, mode="wt") as f: 188*7594170eSAndroid Build Coastguard Worker f.write(v) 189*7594170eSAndroid Build Coastguard Worker _display_summarized_metrics(summary_csv, filter_cujs) 190*7594170eSAndroid Build Coastguard Worker plot_file = output_dir.joinpath(f"{k}.{agg.name}.{plot_format}") 191*7594170eSAndroid Build Coastguard Worker plot_metrics.plot(v, plot_file, filter_cujs) 192*7594170eSAndroid Build Coastguard Worker 193*7594170eSAndroid Build Coastguard Worker 194*7594170eSAndroid Build Coastguard Workerdef main(): 195*7594170eSAndroid Build Coastguard Worker p = argparse.ArgumentParser() 196*7594170eSAndroid Build Coastguard Worker p.add_argument( 197*7594170eSAndroid Build Coastguard Worker "-p", 198*7594170eSAndroid Build Coastguard Worker "--properties", 199*7594170eSAndroid Build Coastguard Worker default="^time$", 200*7594170eSAndroid Build Coastguard Worker nargs="?", 201*7594170eSAndroid Build Coastguard Worker help="regex to select properties", 202*7594170eSAndroid Build Coastguard Worker ) 203*7594170eSAndroid Build Coastguard Worker p.add_argument( 204*7594170eSAndroid Build Coastguard Worker "metrics", 205*7594170eSAndroid Build Coastguard Worker nargs="?", 206*7594170eSAndroid Build Coastguard Worker default=util.get_default_log_dir().joinpath(util.METRICS_TABLE), 207*7594170eSAndroid Build Coastguard Worker help="metrics.csv file to parse", 208*7594170eSAndroid Build Coastguard Worker ) 209*7594170eSAndroid Build Coastguard Worker p.add_argument( 210*7594170eSAndroid Build Coastguard Worker "--statistic", 211*7594170eSAndroid Build Coastguard Worker nargs="?", 212*7594170eSAndroid Build Coastguard Worker type=lambda arg: Aggregation[arg], 213*7594170eSAndroid Build Coastguard Worker default=Aggregation.MEDIAN, 214*7594170eSAndroid Build Coastguard Worker help=f"Defaults to {Aggregation.MEDIAN.name}. " 215*7594170eSAndroid Build Coastguard Worker f"Choose from {[a.name for a in Aggregation]}", 216*7594170eSAndroid Build Coastguard Worker ) 217*7594170eSAndroid Build Coastguard Worker p.add_argument( 218*7594170eSAndroid Build Coastguard Worker "--filter", 219*7594170eSAndroid Build Coastguard Worker default=True, 220*7594170eSAndroid Build Coastguard Worker action=argparse.BooleanOptionalAction, 221*7594170eSAndroid Build Coastguard Worker help="Filter out 'rebuild-' and 'WARMUP' builds?", 222*7594170eSAndroid Build Coastguard Worker ) 223*7594170eSAndroid Build Coastguard Worker p.add_argument( 224*7594170eSAndroid Build Coastguard Worker "--format", 225*7594170eSAndroid Build Coastguard Worker nargs="?", 226*7594170eSAndroid Build Coastguard Worker default="svg", 227*7594170eSAndroid Build Coastguard Worker help="graph output format, e.g. png, svg etc" 228*7594170eSAndroid Build Coastguard Worker ) 229*7594170eSAndroid Build Coastguard Worker options = p.parse_args() 230*7594170eSAndroid Build Coastguard Worker metrics_csv = Path(options.metrics) 231*7594170eSAndroid Build Coastguard Worker aggregation: Aggregation = options.statistic 232*7594170eSAndroid Build Coastguard Worker if metrics_csv.exists() and metrics_csv.is_dir(): 233*7594170eSAndroid Build Coastguard Worker metrics_csv = metrics_csv.joinpath(util.METRICS_TABLE) 234*7594170eSAndroid Build Coastguard Worker if not metrics_csv.exists(): 235*7594170eSAndroid Build Coastguard Worker raise RuntimeError(f"{metrics_csv} does not exit") 236*7594170eSAndroid Build Coastguard Worker summarize( 237*7594170eSAndroid Build Coastguard Worker metrics_csv=metrics_csv, 238*7594170eSAndroid Build Coastguard Worker regex=options.properties, 239*7594170eSAndroid Build Coastguard Worker agg=aggregation, 240*7594170eSAndroid Build Coastguard Worker filter_cujs=options.filter, 241*7594170eSAndroid Build Coastguard Worker output_dir=metrics_csv.parent.joinpath("perf"), 242*7594170eSAndroid Build Coastguard Worker plot_format=options.format, 243*7594170eSAndroid Build Coastguard Worker ) 244*7594170eSAndroid Build Coastguard Worker 245*7594170eSAndroid Build Coastguard Worker 246*7594170eSAndroid Build Coastguard Workerif __name__ == "__main__": 247*7594170eSAndroid Build Coastguard Worker logging.root.setLevel(logging.INFO) 248*7594170eSAndroid Build Coastguard Worker main() 249