blob: 671667296f574efb262b57cb72f5d0f4291a09b7 [file] [log] [blame]
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import csv
import datetime
import enum
import logging
import re
import statistics
import subprocess
import textwrap
from pathlib import Path
from typing import Iterable, NewType, TextIO, TypeVar
import plot_metrics
import util
Row = NewType("Row", dict[str, str])
# modify the row in-place
def _normalize_rebuild(row: Row):
row["description"] = re.sub(
r"^(rebuild)-[\d+](.*)$", "\\1\\2", row.get("description")
)
def _get_tagged_build_type(row: Row) -> str:
build_type = row.get("build_type")
tag = row.get("tag")
return build_type if not tag else f"{build_type}:{tag}"
def _build_types(rows: list[Row]) -> list[str]:
return list(dict.fromkeys(_get_tagged_build_type(row) for row in rows).keys())
def _write_table(lines: list[list[str]]) -> str:
def join_cells(line: list[str]) -> str:
return ",".join(str(cell) for cell in line)
return "\n".join(join_cells(line) for line in lines) + "\n"
class Aggregation(enum.Enum):
# naked function as value assignment doesn't seem to work,
# hence wrapping in a singleton tuple
AVG = (statistics.mean,)
MAX = (max,)
MEDIAN = (statistics.median,)
MIN = (min,)
STDEV = (statistics.stdev,)
N = TypeVar("N", int, float)
def fn(self, xs: Iterable[N]) -> N:
return self.value[0](xs)
def _aggregate(prop: str, rows: list[Row], agg: Aggregation) -> str:
"""
compute the requested aggregation
:return formatted values
"""
if not rows:
return ""
vals = [x.get(prop) for x in rows]
vals = [x for x in vals if bool(x)]
if len(vals) == 0:
return ""
isnum = any(x.isnumeric() for x in vals)
if isnum:
vals = [int(x) for x in vals]
cell = f"{(agg.fn(vals)):.0f}"
else:
vals = [util.period_to_seconds(x) for x in vals]
cell = util.hhmmss(datetime.timedelta(seconds=agg.fn(vals)))
if len(vals) > 1:
cell = f"{cell}[N={len(vals)}]"
return cell
def acceptable(row: Row) -> bool:
failure = row.get("build_result") == "FAILED"
if failure:
logging.error(f"Skipping {row.get('description')}/{row.get('build_type')}")
return not failure
def summarize_helper(metrics: TextIO, regex: str, agg: Aggregation) -> dict[str, str]:
"""
Args:
metrics: csv detailed input, each row corresponding to a build
regex: regex matching properties to be summarized
agg: aggregation to use
"""
reader: csv.DictReader = csv.DictReader(metrics)
# get all matching properties
p = re.compile(regex)
properties = [f for f in reader.fieldnames if p.search(f)]
if len(properties) == 0:
logging.error("no matching properties found")
return {}
all_rows: list[Row] = [row for row in reader if acceptable(row)]
for row in all_rows:
_normalize_rebuild(row)
build_types: list[str] = _build_types(all_rows)
by_cuj: dict[str, list[Row]] = util.groupby(
all_rows, lambda l: l.get("description")
)
def extract_lines_for_cuj(prop, cuj, cuj_rows) -> list[list[str]]:
by_targets = util.groupby(cuj_rows, lambda l: l.get("targets"))
lines = []
for targets, target_rows in by_targets.items():
by_build_type = util.groupby(target_rows, _get_tagged_build_type)
vals = [
_aggregate(prop, by_build_type.get(build_type), agg)
for build_type in build_types
]
lines.append([cuj, targets, *vals])
return lines
def tabulate(prop) -> str:
headers = ["cuj", "targets"] + build_types
lines: list[list[str]] = [headers]
for cuj, cuj_rows in by_cuj.items():
lines.extend(extract_lines_for_cuj(prop, cuj, cuj_rows))
return _write_table(lines)
return {prop: tabulate(prop) for prop in properties}
def _display_summarized_metrics(summary_csv: Path, filter_cujs: bool):
cmd = (
(
f'grep -v "WARMUP\\|rebuild\\|revert\\|delete" {summary_csv}'
f" | column -t -s,"
)
if filter_cujs
else f"column -t -s, {summary_csv}"
)
output = subprocess.check_output(cmd, shell=True, text=True)
logging.info(
textwrap.dedent(
f"""\
%s
%s
"""
),
cmd,
output,
)
def summarize(
metrics_csv: Path,
regex: str,
output_dir: Path,
agg: Aggregation = Aggregation.MEDIAN,
filter_cujs: bool = True,
plot_format: str = "svg",
):
"""
writes `summary_data` value as a csv files under `output_dir`
if `filter_cujs` is False, then does not filter out WARMUP and rebuild cuj steps
"""
with open(metrics_csv, "rt") as input_file:
summary_data = summarize_helper(input_file, regex, agg)
for k, v in summary_data.items():
summary_csv = output_dir.joinpath(f"{k}.{agg.name}.csv")
summary_csv.parent.mkdir(parents=True, exist_ok=True)
with open(summary_csv, mode="wt") as f:
f.write(v)
_display_summarized_metrics(summary_csv, filter_cujs)
plot_file = output_dir.joinpath(f"{k}.{agg.name}.{plot_format}")
plot_metrics.plot(v, plot_file, filter_cujs)
def main():
p = argparse.ArgumentParser()
p.add_argument(
"-p",
"--properties",
default="^time$",
nargs="?",
help="regex to select properties",
)
p.add_argument(
"metrics",
nargs="?",
default=util.get_default_log_dir().joinpath(util.METRICS_TABLE),
help="metrics.csv file to parse",
)
p.add_argument(
"--statistic",
nargs="?",
type=lambda arg: Aggregation[arg],
default=Aggregation.MEDIAN,
help=f"Defaults to {Aggregation.MEDIAN.name}. "
f"Choose from {[a.name for a in Aggregation]}",
)
p.add_argument(
"--filter",
default=True,
action=argparse.BooleanOptionalAction,
help="Filter out 'rebuild-' and 'WARMUP' builds?",
)
p.add_argument(
"--format",
nargs="?",
default="svg",
help="graph output format, e.g. png, svg etc"
)
options = p.parse_args()
metrics_csv = Path(options.metrics)
aggregation: Aggregation = options.statistic
if metrics_csv.exists() and metrics_csv.is_dir():
metrics_csv = metrics_csv.joinpath(util.METRICS_TABLE)
if not metrics_csv.exists():
raise RuntimeError(f"{metrics_csv} does not exit")
summarize(
metrics_csv=metrics_csv,
regex=options.properties,
agg=aggregation,
filter_cujs=options.filter,
output_dir=metrics_csv.parent.joinpath("perf"),
plot_format=options.format,
)
if __name__ == "__main__":
logging.root.setLevel(logging.INFO)
main()