blob: 845d73fe62932b3eaef0d9f4dc5562ce663a8acb [file] [log] [blame]
Joe Onorato88ede352023-12-19 02:56:38 +00001#!/usr/bin/env python3
2# Copyright (C) 2023 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import sys
17if __name__ == "__main__":
18 sys.dont_write_bytecode = True
19
20import argparse
21import dataclasses
22import datetime
23import json
24import os
25import pathlib
26import statistics
27import zoneinfo
28
29import pretty
30import utils
31
32# TODO:
33# - Flag if the last postroll build was more than 15 seconds or something. That's
34# an indicator that something is amiss.
35# - Add a mode to print all of the values for multi-iteration runs
36# - Add a flag to reorder the tags
37# - Add a flag to reorder the headers in order to show grouping more clearly.
38
39
40def FindSummaries(args):
41 def find_summaries(directory):
42 return [str(p.resolve()) for p in pathlib.Path(directory).glob("**/summary.json")]
43 if not args:
44 # If they didn't give an argument, use the default dir
45 root = utils.get_root()
46 if not root:
47 return []
48 return find_summaries(root.joinpath("..", utils.DEFAULT_REPORT_DIR))
49 results = list()
50 for arg in args:
51 if os.path.isfile(arg):
52 # If it's a file add that
53 results.append(arg)
54 elif os.path.isdir(arg):
55 # If it's a directory, find all of the files there
56 results += find_summaries(arg)
57 else:
58 sys.stderr.write(f"Invalid summary argument: {arg}\n")
59 sys.exit(1)
60 return sorted(list(results))
61
62
63def LoadSummary(filename):
64 with open(filename) as f:
65 return json.load(f)
66
67# Columns:
68# Date
69# Branch
70# Tag
71# --
72# Lunch
73# Rows:
74# Benchmark
75
Joe Onorato88ede352023-12-19 02:56:38 +000076def lunch_str(d):
77 "Convert a lunch dict to a string"
78 return f"{d['TARGET_PRODUCT']}-{d['TARGET_RELEASE']}-{d['TARGET_BUILD_VARIANT']}"
79
80def group_by(l, key):
81 "Return a list of tuples, grouped by key, sorted by key"
82 result = {}
83 for item in l:
84 result.setdefault(key(item), []).append(item)
85 return [(k, v) for k, v in result.items()]
86
87
88class Table:
89 def __init__(self):
90 self._data = {}
91 self._rows = []
92 self._cols = []
93
94 def Set(self, column_key, row_key, data):
95 self._data[(column_key, row_key)] = data
96 if not column_key in self._cols:
97 self._cols.append(column_key)
98 if not row_key in self._rows:
99 self._rows.append(row_key)
100
101 def Write(self, out):
102 table = []
103 # Expand the column items
104 for row in zip(*self._cols):
105 if row.count(row[0]) == len(row):
106 continue
107 table.append([""] + [col for col in row])
108 if table:
109 table.append(pretty.SEPARATOR)
110 # Populate the data
111 for row in self._rows:
112 table.append([str(row)] + [str(self._data.get((col, row), "")) for col in self._cols])
113 out.write(pretty.FormatTable(table))
114
115
116def format_duration_sec(ns):
117 "Format a duration in ns to second precision"
118 sec = round(ns / 1000000000)
119 h, sec = divmod(sec, 60*60)
120 m, sec = divmod(sec, 60)
121 result = ""
122 if h > 0:
123 result += f"{h:2d}h "
124 if h > 0 or m > 0:
125 result += f"{m:2d}m "
126 return result + f"{sec:2d}s"
127
Joe Onorato67c944b2023-12-21 13:29:18 -0800128
Joe Onorato88ede352023-12-19 02:56:38 +0000129def main(argv):
130 parser = argparse.ArgumentParser(
131 prog="format_benchmarks",
132 allow_abbrev=False, # Don't let people write unsupportable scripts.
133 description="Print analysis tables for benchmarks")
134
Joe Onorato67c944b2023-12-21 13:29:18 -0800135 parser.add_argument("--tags", nargs="*",
136 help="The tags to print, in order.")
137
Joe Onorato88ede352023-12-19 02:56:38 +0000138 parser.add_argument("summaries", nargs="*",
139 help="A summary.json file or a directory in which to look for summaries.")
140
141 args = parser.parse_args()
142
143 # Load the summaries
144 summaries = [(s, LoadSummary(s)) for s in FindSummaries(args.summaries)]
145
146 # Convert to MTV time
147 for filename, s in summaries:
148 dt = datetime.datetime.fromisoformat(s["start_time"])
149 dt = dt.astimezone(zoneinfo.ZoneInfo("America/Los_Angeles"))
150 s["datetime"] = dt
151 s["date"] = datetime.date(dt.year, dt.month, dt.day)
152
Joe Onorato67c944b2023-12-21 13:29:18 -0800153 # Filter out tags we don't want
154 if args.tags:
155 summaries = [(f, s) for f, s in summaries if s.get("tag", "") in args.tags]
156
157 # If they supplied tags, sort in that order, otherwise sort by tag
158 if args.tags:
159 tagsort = lambda tag: args.tags.index(tag)
160 else:
161 tagsort = lambda tag: tag
162
Joe Onorato88ede352023-12-19 02:56:38 +0000163 # Sort the summaries
Joe Onorato67c944b2023-12-21 13:29:18 -0800164 summaries.sort(key=lambda s: (s[1]["date"], s[1]["branch"], tagsort(s[1]["tag"])))
Joe Onorato88ede352023-12-19 02:56:38 +0000165
166 # group the benchmarks by column and iteration
167 def bm_key(b):
168 return (
169 lunch_str(b["lunch"]),
170 )
171 for filename, summary in summaries:
172 summary["columns"] = [(key, group_by(bms, lambda b: b["id"])) for key, bms
173 in group_by(summary["benchmarks"], bm_key)]
174
175 # Build the table
176 table = Table()
177 for filename, summary in summaries:
178 for key, column in summary["columns"]:
179 for id, cell in column:
180 duration_ns = statistics.median([b["duration_ns"] for b in cell])
Joe Onorato519c9da2024-01-02 16:46:26 -0800181 table.Set(tuple([summary["date"].strftime("%Y-%m-%d"),
Joe Onorato88ede352023-12-19 02:56:38 +0000182 summary["branch"],
183 summary["tag"]]
184 + list(key)),
185 cell[0]["title"], format_duration_sec(duration_ns))
186
187 table.Write(sys.stdout)
188
189if __name__ == "__main__":
190 main(sys.argv)
191