xref: /aosp_15_r20/build/bazel/scripts/incremental_build/plot_metrics.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 csv
15*7594170eSAndroid Build Coastguard Workerimport enum
16*7594170eSAndroid Build Coastguard Workerimport functools
17*7594170eSAndroid Build Coastguard Workerimport logging
18*7594170eSAndroid Build Coastguard Workerimport os
19*7594170eSAndroid Build Coastguard Workerimport statistics
20*7594170eSAndroid Build Coastguard Workerimport subprocess
21*7594170eSAndroid Build Coastguard Workerimport tempfile
22*7594170eSAndroid Build Coastguard Workerfrom io import StringIO
23*7594170eSAndroid Build Coastguard Workerfrom pathlib import Path
24*7594170eSAndroid Build Coastguard Workerfrom string import Template
25*7594170eSAndroid Build Coastguard Workerfrom typing import NewType, TypeVar, Iterable
26*7594170eSAndroid Build Coastguard Workerfrom typing import Optional
27*7594170eSAndroid Build Coastguard Worker
28*7594170eSAndroid Build Coastguard WorkerRow = NewType("Row", dict[str, str])
29*7594170eSAndroid Build Coastguard Worker
30*7594170eSAndroid Build Coastguard WorkerN = TypeVar("N", int, float)
31*7594170eSAndroid Build Coastguard Worker
32*7594170eSAndroid Build Coastguard Worker
33*7594170eSAndroid Build Coastguard Workerclass Aggregation(enum.Enum):
34*7594170eSAndroid Build Coastguard Worker    # naked function as value assignment doesn't seem to work,
35*7594170eSAndroid Build Coastguard Worker    # hence wrapping in a singleton tuple
36*7594170eSAndroid Build Coastguard Worker    AVG = (statistics.mean,)
37*7594170eSAndroid Build Coastguard Worker    MAX = (max,)
38*7594170eSAndroid Build Coastguard Worker    MEDIAN = (statistics.median,)
39*7594170eSAndroid Build Coastguard Worker    MIN = (min,)
40*7594170eSAndroid Build Coastguard Worker    STDEV = (statistics.stdev,)
41*7594170eSAndroid Build Coastguard Worker
42*7594170eSAndroid Build Coastguard Worker    def fn(self, xs: Iterable[N]) -> N:
43*7594170eSAndroid Build Coastguard Worker        return self.value[0](xs)
44*7594170eSAndroid Build Coastguard Worker
45*7594170eSAndroid Build Coastguard Worker
46*7594170eSAndroid Build Coastguard Workerdef _is_numeric(summary_row: Row) -> Optional[bool]:
47*7594170eSAndroid Build Coastguard Worker    for k, v in summary_row.items():
48*7594170eSAndroid Build Coastguard Worker        if k not in ("cuj", "targets"):
49*7594170eSAndroid Build Coastguard Worker            if ":" in v:
50*7594170eSAndroid Build Coastguard Worker                # presence of ':'  signifies a time field
51*7594170eSAndroid Build Coastguard Worker                return False
52*7594170eSAndroid Build Coastguard Worker            elif v.isnumeric():
53*7594170eSAndroid Build Coastguard Worker                return True
54*7594170eSAndroid Build Coastguard Worker    return None  # could not make a decision
55*7594170eSAndroid Build Coastguard Worker
56*7594170eSAndroid Build Coastguard Worker
57*7594170eSAndroid Build Coastguard Workerdef prepare_script(
58*7594170eSAndroid Build Coastguard Worker    summary_csv_data: str, output: Path, filter: bool = True
59*7594170eSAndroid Build Coastguard Worker) -> Optional[str]:
60*7594170eSAndroid Build Coastguard Worker    reader: csv.DictReader = csv.DictReader(StringIO(summary_csv_data))
61*7594170eSAndroid Build Coastguard Worker    lines: list[str] = [",".join(reader.fieldnames)]
62*7594170eSAndroid Build Coastguard Worker    isnum = None
63*7594170eSAndroid Build Coastguard Worker
64*7594170eSAndroid Build Coastguard Worker    for summary_row in reader:
65*7594170eSAndroid Build Coastguard Worker        if isnum is None:
66*7594170eSAndroid Build Coastguard Worker            isnum = _is_numeric(summary_row)
67*7594170eSAndroid Build Coastguard Worker        cuj = summary_row.get("cuj")
68*7594170eSAndroid Build Coastguard Worker        if filter and ("rebuild" in cuj or "WARMUP" in cuj):
69*7594170eSAndroid Build Coastguard Worker            continue
70*7594170eSAndroid Build Coastguard Worker        # fall back to 0 if a values is missing for plotting
71*7594170eSAndroid Build Coastguard Worker        lines.append(",".join(v or "0" for v in summary_row.values()))
72*7594170eSAndroid Build Coastguard Worker
73*7594170eSAndroid Build Coastguard Worker    if len(lines) <= 1:
74*7594170eSAndroid Build Coastguard Worker        logging.warning("No data to plot")
75*7594170eSAndroid Build Coastguard Worker        return None
76*7594170eSAndroid Build Coastguard Worker
77*7594170eSAndroid Build Coastguard Worker    template_file = Path(os.path.dirname(__file__)).joinpath(
78*7594170eSAndroid Build Coastguard Worker        "plot_metrics.template.txt"
79*7594170eSAndroid Build Coastguard Worker    )
80*7594170eSAndroid Build Coastguard Worker    with open(template_file, "r") as fp:
81*7594170eSAndroid Build Coastguard Worker        script_template = Template(fp.read())
82*7594170eSAndroid Build Coastguard Worker
83*7594170eSAndroid Build Coastguard Worker    os.makedirs(output.parent, exist_ok=True)
84*7594170eSAndroid Build Coastguard Worker    column_count = len(reader.fieldnames)
85*7594170eSAndroid Build Coastguard Worker
86*7594170eSAndroid Build Coastguard Worker    return script_template.substitute(
87*7594170eSAndroid Build Coastguard Worker        column_count=column_count,
88*7594170eSAndroid Build Coastguard Worker        data="\n".join(lines),
89*7594170eSAndroid Build Coastguard Worker        output=output,
90*7594170eSAndroid Build Coastguard Worker        term=output.suffix[1:],  # assume terminal = output suffix, e.g. png, svg
91*7594170eSAndroid Build Coastguard Worker        width=max(160 * ((len(lines) + 4) // 4), 640),
92*7594170eSAndroid Build Coastguard Worker        ydata="# default to num" if isnum else "time",
93*7594170eSAndroid Build Coastguard Worker    )
94*7594170eSAndroid Build Coastguard Worker
95*7594170eSAndroid Build Coastguard Worker
96*7594170eSAndroid Build Coastguard Workerdef _with_line_num(script: str) -> str:
97*7594170eSAndroid Build Coastguard Worker    return "".join(
98*7594170eSAndroid Build Coastguard Worker        f"{i + 1:2d}:{line}" for i, line in enumerate(script.splitlines(keepends=True))
99*7594170eSAndroid Build Coastguard Worker    )
100*7594170eSAndroid Build Coastguard Worker
101*7594170eSAndroid Build Coastguard Worker
102*7594170eSAndroid Build Coastguard Worker@functools.cache
103*7594170eSAndroid Build Coastguard Workerdef _gnuplot_available() -> bool:
104*7594170eSAndroid Build Coastguard Worker    has_gnuplot = (
105*7594170eSAndroid Build Coastguard Worker        subprocess.run(
106*7594170eSAndroid Build Coastguard Worker            "gnuplot --version",
107*7594170eSAndroid Build Coastguard Worker            shell=True,
108*7594170eSAndroid Build Coastguard Worker            check=False,
109*7594170eSAndroid Build Coastguard Worker            stdout=subprocess.DEVNULL,
110*7594170eSAndroid Build Coastguard Worker            stderr=subprocess.DEVNULL,
111*7594170eSAndroid Build Coastguard Worker            text=True,
112*7594170eSAndroid Build Coastguard Worker        ).returncode
113*7594170eSAndroid Build Coastguard Worker        == 0
114*7594170eSAndroid Build Coastguard Worker    )
115*7594170eSAndroid Build Coastguard Worker    if not has_gnuplot:
116*7594170eSAndroid Build Coastguard Worker        logging.warning("gnuplot unavailable")
117*7594170eSAndroid Build Coastguard Worker    return has_gnuplot
118*7594170eSAndroid Build Coastguard Worker
119*7594170eSAndroid Build Coastguard Worker
120*7594170eSAndroid Build Coastguard Workerdef plot(summary_csv_data: str, output: Path, filter: bool):
121*7594170eSAndroid Build Coastguard Worker    if not _gnuplot_available():
122*7594170eSAndroid Build Coastguard Worker        return
123*7594170eSAndroid Build Coastguard Worker    script = prepare_script(summary_csv_data, output, filter)
124*7594170eSAndroid Build Coastguard Worker    if script is None:
125*7594170eSAndroid Build Coastguard Worker        return  # no data to plot, probably due to the filter
126*7594170eSAndroid Build Coastguard Worker    with tempfile.NamedTemporaryFile("w+t") as gnuplot:
127*7594170eSAndroid Build Coastguard Worker        gnuplot.write(script)
128*7594170eSAndroid Build Coastguard Worker        gnuplot.flush()
129*7594170eSAndroid Build Coastguard Worker        p = subprocess.run(
130*7594170eSAndroid Build Coastguard Worker            args=["gnuplot", gnuplot.name],
131*7594170eSAndroid Build Coastguard Worker            shell=False,
132*7594170eSAndroid Build Coastguard Worker            check=False,
133*7594170eSAndroid Build Coastguard Worker            capture_output=True,
134*7594170eSAndroid Build Coastguard Worker            text=True,
135*7594170eSAndroid Build Coastguard Worker        )
136*7594170eSAndroid Build Coastguard Worker        logging.debug("GnuPlot script:\n%s", script)
137*7594170eSAndroid Build Coastguard Worker        if p.returncode:
138*7594170eSAndroid Build Coastguard Worker            logging.error("GnuPlot errors:\n%s\n%s", p.stderr, _with_line_num(script))
139*7594170eSAndroid Build Coastguard Worker        else:
140*7594170eSAndroid Build Coastguard Worker            logging.info(f"See %s\n%s", output, p.stdout)
141