xref: /aosp_15_r20/build/bazel/scripts/incremental_build/pretty.py (revision 7594170e27e0732bc44b93d1440d87a54b6ffe7c)
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