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