benchstats 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchstats/__init__.py +0 -0
- benchstats/__main__.py +114 -0
- benchstats/cli_parser.py +297 -0
- benchstats/common.py +153 -0
- benchstats/compare.py +280 -0
- benchstats/parser_GbenchJson.py +179 -0
- benchstats/parsers.py +52 -0
- benchstats/render.py +318 -0
- benchstats-1.0.0.dist-info/METADATA +286 -0
- benchstats-1.0.0.dist-info/RECORD +14 -0
- benchstats-1.0.0.dist-info/WHEEL +5 -0
- benchstats-1.0.0.dist-info/entry_points.txt +2 -0
- benchstats-1.0.0.dist-info/licenses/LICENSE +9 -0
- benchstats-1.0.0.dist-info/top_level.txt +1 -0
benchstats/__init__.py
ADDED
|
File without changes
|
benchstats/__main__.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# using abs path only here to alleviate debugging
|
|
2
|
+
from benchstats.compare import compareStats
|
|
3
|
+
from benchstats.common import LoggingConsole, detectExportFormat, bmNamesTransform
|
|
4
|
+
from benchstats.cli_parser import makeParser
|
|
5
|
+
from benchstats.render import renderComparisonResults
|
|
6
|
+
from benchstats.parsers import getParserFor
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from rich.terminal_theme import DIMMED_MONOKAI as DarkTheme, DEFAULT_TERMINAL_THEME as LightTheme
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
parser = makeParser()
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
export_fmt = detectExportFormat(args.export_to, args.export_fmt)
|
|
17
|
+
if export_fmt is not None:
|
|
18
|
+
if os.path.isfile(args.export_to):
|
|
19
|
+
os.remove(args.export_to)
|
|
20
|
+
else:
|
|
21
|
+
assert not os.path.exists(args.export_to), "Invalid --export_to value"
|
|
22
|
+
|
|
23
|
+
console = LoggingConsole(
|
|
24
|
+
no_color=not args.colors,
|
|
25
|
+
record=export_fmt is not None,
|
|
26
|
+
log_level=(
|
|
27
|
+
LoggingConsole.LogLevel.Debug if args.show_debug else LoggingConsole.LogLevel.Warning
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if args.alpha > 0.5 or args.alpha <= 0:
|
|
32
|
+
console.critical(
|
|
33
|
+
"--alpha must be a positive number less than 0.5 (%.2f is given)" % args.alpha
|
|
34
|
+
)
|
|
35
|
+
exit(2)
|
|
36
|
+
|
|
37
|
+
Parser1 = getParserFor(args.files_parser if args.file1_parser is None else args.file1_parser)
|
|
38
|
+
Parser2 = getParserFor(args.files_parser if args.file2_parser is None else args.file2_parser)
|
|
39
|
+
|
|
40
|
+
s1 = Parser1(args.file1, args.filter1, args.metrics, debug_log=console).getStats()
|
|
41
|
+
s2 = Parser2(args.file2, args.filter2, args.metrics, debug_log=console).getStats()
|
|
42
|
+
|
|
43
|
+
s1 = bmNamesTransform(
|
|
44
|
+
s1,
|
|
45
|
+
getattr(args, "from") if args.from1 is None else args.from1,
|
|
46
|
+
args.to if args.to1 is None else args.to1,
|
|
47
|
+
1,
|
|
48
|
+
console,
|
|
49
|
+
)
|
|
50
|
+
s2 = bmNamesTransform(
|
|
51
|
+
s2,
|
|
52
|
+
getattr(args, "from") if args.from2 is None else args.from2,
|
|
53
|
+
args.to if args.to2 is None else args.to2,
|
|
54
|
+
2,
|
|
55
|
+
console,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
alpha = args.alpha
|
|
59
|
+
if args.bonferroni:
|
|
60
|
+
alpha = alpha / len(s1)
|
|
61
|
+
console.info(
|
|
62
|
+
f"Bonferroni correction for {len(s1)} comparisons turns alpha={args.alpha:.3e} into {alpha:.3e}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if args.main_metrics is None or len(args.main_metrics) < 1:
|
|
66
|
+
main_metrics = [args.metrics[0]]
|
|
67
|
+
else:
|
|
68
|
+
main_metrics = [args.metrics[mi] for mi in args.main_metrics]
|
|
69
|
+
|
|
70
|
+
cr = compareStats(
|
|
71
|
+
s1,
|
|
72
|
+
s2,
|
|
73
|
+
method=args.method,
|
|
74
|
+
main_metrics=main_metrics,
|
|
75
|
+
alpha=alpha,
|
|
76
|
+
debug_log=console,
|
|
77
|
+
store_sets=args.sample_stats is not None,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
renderComparisonResults(
|
|
81
|
+
cr,
|
|
82
|
+
console,
|
|
83
|
+
expect_same=args.expect_same,
|
|
84
|
+
main_metrics=main_metrics,
|
|
85
|
+
sample_stats=args.sample_stats,
|
|
86
|
+
dark_theme=not args.export_light,
|
|
87
|
+
show_sample_sizes=args.sample_sizes,
|
|
88
|
+
always_show_pvalues=args.always_show_pvalues,
|
|
89
|
+
multiline=args.multiline,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if cr.at_least_one_differs:
|
|
93
|
+
console.warning(
|
|
94
|
+
"At least one significant difference in main metrics was detected. Returning with exit(1)."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if export_fmt is not None:
|
|
98
|
+
if "txt" == export_fmt:
|
|
99
|
+
console.save_text(args.export_to)
|
|
100
|
+
elif "svg" == export_fmt:
|
|
101
|
+
console.save_svg(
|
|
102
|
+
args.export_to, title="", theme=LightTheme if args.export_light else DarkTheme
|
|
103
|
+
)
|
|
104
|
+
elif "html" == export_fmt:
|
|
105
|
+
console.save_html(args.export_to, theme=LightTheme if args.export_light else DarkTheme)
|
|
106
|
+
else:
|
|
107
|
+
assert False, "NOT IMPLEMENTED?!"
|
|
108
|
+
|
|
109
|
+
if cr.at_least_one_differs:
|
|
110
|
+
exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
main()
|
benchstats/cli_parser.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from .common import kAvailableFormats
|
|
3
|
+
from .compare import kMethods, kDefaultAlpha
|
|
4
|
+
from .parsers import getBuiltinParsers
|
|
5
|
+
from .render import kPossibleStatNames
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def makeParser():
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
description="A tool to compare two sets of the same benchmarks with repetitions.\n"
|
|
11
|
+
"Homepage: https://github.com/Arech/benchstats\n",
|
|
12
|
+
epilog="On custom parsers:\n"
|
|
13
|
+
"You could supply virtually any source data to the tool by utilizing --files_parser (or "
|
|
14
|
+
"--file1_parser or --file2_parser) command line argument. Each of these arguments in "
|
|
15
|
+
"addition to built-in parser identifiers also accepts a path to a Python file that "
|
|
16
|
+
"defines a custom parser. The simplest possible parser to read a single one column CSV "
|
|
17
|
+
"file is this:\n\n"
|
|
18
|
+
"""# save to ./myCSV.py
|
|
19
|
+
import numpy as np
|
|
20
|
+
from benchstats.common import ParserBase
|
|
21
|
+
|
|
22
|
+
class myCSV(ParserBase):
|
|
23
|
+
def __init__(self, fpath, filter, metrics, debug_log=None) -> None:
|
|
24
|
+
self.stats = np.loadtxt(fpath, dtype=np.float64)
|
|
25
|
+
|
|
26
|
+
def getStats(self) -> dict[str, dict[str, np.ndarray]]:
|
|
27
|
+
return {"bm": {"real_time": self.stats}}
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
"It doesn't support filtering, different benchmarks and the metric is hardcoded - but you "
|
|
31
|
+
"get the idea. It's that simple.\n"
|
|
32
|
+
"Now to compare two datasets in csvs, just run:\n"
|
|
33
|
+
"python -m benchstats ./src1.csv ./src2.csv --files_parser ./myCSV.py\n\n"
|
|
34
|
+
"If you'll make a parser that could be useful to other people, please consider adding it "
|
|
35
|
+
"to the project's built-in parsers set by opening a thread with a suggestion in "
|
|
36
|
+
"https://github.com/Arech/benchstats/issues or by making a PR into the repo.",
|
|
37
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--show_debug",
|
|
42
|
+
help="Shows some additional debugging info. Default: %(default)s",
|
|
43
|
+
action=argparse.BooleanOptionalAction,
|
|
44
|
+
default=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
g_inputs = parser.add_argument_group(
|
|
48
|
+
"Input data",
|
|
49
|
+
"Arguments describing how a source data sets are obtained and are optionally transformed.",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
g_inputs.add_argument(
|
|
53
|
+
"file1",
|
|
54
|
+
help="Path to the first data file with benchmark results. See also --file1_parser and "
|
|
55
|
+
"--filter1 arguments",
|
|
56
|
+
metavar="<path/to/file1>",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
g_inputs.add_argument(
|
|
60
|
+
"file2",
|
|
61
|
+
help="Path to the second data file with benchmark results. See also --file2_parser and "
|
|
62
|
+
"--filter2 arguments",
|
|
63
|
+
metavar="<path/to/file2>",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
g_inputs.add_argument(
|
|
67
|
+
"--files_parser",
|
|
68
|
+
help="Sets files parser class identifier, if a built-in parser is used (options are: "
|
|
69
|
+
f"{', '.join(getBuiltinParsers())}). Or sets a path to .py file defining a custom parser, "
|
|
70
|
+
"inherited from 'benchstats.common.ParserBase' class. The parser class name must be the "
|
|
71
|
+
"same as the file name. See below for an example.",
|
|
72
|
+
default="GbenchJson",
|
|
73
|
+
metavar="<files parser class or path>",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
g_inputs.add_argument(
|
|
77
|
+
"--file1_parser",
|
|
78
|
+
help="Same as --files_parser, but only applies to file1.",
|
|
79
|
+
default=None,
|
|
80
|
+
metavar="<file1 parser class or path>",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
g_inputs.add_argument(
|
|
84
|
+
"--filter1",
|
|
85
|
+
help="If specified, sets a Python regular expression (see "
|
|
86
|
+
"https://docs.python.org/3/howto/regex.html#regex-howto) to select benchmarks by name "
|
|
87
|
+
"from <file1>",
|
|
88
|
+
metavar="<reg expr>",
|
|
89
|
+
default=None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
g_inputs.add_argument(
|
|
93
|
+
"--file2_parser",
|
|
94
|
+
help="Same as --files_parser, but only applies to file2.",
|
|
95
|
+
default=None,
|
|
96
|
+
metavar="<file2 parser class or path>",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
g_inputs.add_argument(
|
|
100
|
+
"--filter2",
|
|
101
|
+
help="Same as --filter1, but for <file2>",
|
|
102
|
+
metavar="<reg expr>",
|
|
103
|
+
default=None,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
g_inputs.add_argument(
|
|
107
|
+
"--from",
|
|
108
|
+
help="--from and --to works in a pair and are used to describe benchmark names transformation. "
|
|
109
|
+
"--from sets a regular expression (Python re module flavor) to define a pattern to replace to --to replacement "
|
|
110
|
+
"string. "
|
|
111
|
+
"Typical use-cases include removing unnecessary parts of benchmark names (for example, Google Benchmark adds "
|
|
112
|
+
"some suffixes that might not convey useful information) or \"glueing\" results of different benchmarks "
|
|
113
|
+
"so they become comparable by the tool (for example, you have two benchmarks, the first is 'old_foo' with old algorithm "
|
|
114
|
+
"implementation and the other is 'foo' with a new competing algorithm implementation, - to compare their performance "
|
|
115
|
+
"against each other, you need to remove 'old_' prefix from the first benchmark with `--from old_`"
|
|
116
|
+
" and apply --filter1 and --filter2 to restrict loading only corresponding data for old or new variations). "
|
|
117
|
+
"Remember to escape characters that have special meaning for the shell.",
|
|
118
|
+
metavar="<reg expr>",
|
|
119
|
+
default=None,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
g_inputs.add_argument(
|
|
123
|
+
"--to",
|
|
124
|
+
help="See help for --from.",
|
|
125
|
+
metavar="<replacement string>",
|
|
126
|
+
default=None,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
g_inputs.add_argument(
|
|
130
|
+
"--from1",
|
|
131
|
+
help="Like --from, but for <file1> only",
|
|
132
|
+
metavar="<reg expr>",
|
|
133
|
+
default=None,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
g_inputs.add_argument(
|
|
137
|
+
"--to1",
|
|
138
|
+
help="Like --to, but for <file1> only",
|
|
139
|
+
metavar="<replacement string>",
|
|
140
|
+
default=None,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
g_inputs.add_argument(
|
|
144
|
+
"--from2",
|
|
145
|
+
help="Like --from, but for <file2> only",
|
|
146
|
+
metavar="<reg expr>",
|
|
147
|
+
default=None,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
g_inputs.add_argument(
|
|
151
|
+
"--to2",
|
|
152
|
+
help="Like --to, but for <file2> only",
|
|
153
|
+
metavar="<replacement string>",
|
|
154
|
+
default=None,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
g_inputs.add_argument(
|
|
158
|
+
"metrics",
|
|
159
|
+
help="List of metric identifiers to use in tests for each benchmark. Default: %(default)s. "
|
|
160
|
+
"For deterministic algorithms highly recommend to measure and to use a minimum latency per "
|
|
161
|
+
"repetition.",
|
|
162
|
+
nargs="*",
|
|
163
|
+
default=["real_time"],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
g_inputs.add_argument(
|
|
167
|
+
"--main_metrics",
|
|
168
|
+
help="Indexes in 'metrics' list specifying the main metrics. These are displayed differently and differences "
|
|
169
|
+
"detected cause script to exit(1). Default: %(default)s.",
|
|
170
|
+
nargs="+",
|
|
171
|
+
metavar="<metric idx>",
|
|
172
|
+
type=int,
|
|
173
|
+
default=[0],
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
g_stats = parser.add_argument_group("Statistical settings")
|
|
177
|
+
|
|
178
|
+
g_stats.add_argument(
|
|
179
|
+
"--method",
|
|
180
|
+
help=(
|
|
181
|
+
"Selects a method of statistical testing. Possible values are are: "
|
|
182
|
+
+ ", ".join(
|
|
183
|
+
[
|
|
184
|
+
"'" + id + f"' for {descr['name']} ({descr['url'].replace('%','%%')})"
|
|
185
|
+
for (id, descr) in kMethods.items()
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
+ ". Default is %(default)s"
|
|
189
|
+
),
|
|
190
|
+
metavar="<method id>",
|
|
191
|
+
choices=kMethods.keys(),
|
|
192
|
+
default=list(kMethods.keys())[0],
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
g_stats.add_argument(
|
|
196
|
+
"--alpha",
|
|
197
|
+
help="Set statistical significance level (a desired mistake probability level)\n"
|
|
198
|
+
"Note! This is an actual probability of a mistake only when all preconditions of the "
|
|
199
|
+
"chosen statistical test are met. One of the most important preconditions like independence of "
|
|
200
|
+
"individual measurements, or constancy of underlying distribution parameters are almost "
|
|
201
|
+
"never met in a real-life benchmarking, even on a properly quiesced hardware. Real "
|
|
202
|
+
"mistake rates are higher. Default %(default)s",
|
|
203
|
+
metavar="<positive float less than 0.5>",
|
|
204
|
+
type=float,
|
|
205
|
+
default=kDefaultAlpha,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
g_stats.add_argument(
|
|
209
|
+
"--bonferroni",
|
|
210
|
+
help="If set, applies a Bonferroni multiple comparisons correction "
|
|
211
|
+
"(https://en.wikipedia.org/wiki/Bonferroni_correction).",
|
|
212
|
+
action="store_true",
|
|
213
|
+
default=False,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
g_rendering=parser.add_argument_group("Rendering", "Controls how results are rendered")
|
|
217
|
+
|
|
218
|
+
g_rendering.add_argument(
|
|
219
|
+
"--always_show_pvalues",
|
|
220
|
+
help="If set, always show pvalues. By default it shows pvalues only for significant differences. Note that "
|
|
221
|
+
"when two sets are compared stochastically same (~), or more precisely, not stochastically less and not "
|
|
222
|
+
"stochastically greater (see https://en.wikipedia.org/wiki/Stochastic_ordering), pvalue shown is a minimum of "
|
|
223
|
+
"two pvalues for less and greater comparison.",
|
|
224
|
+
action="store_true",
|
|
225
|
+
default=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
g_rendering.add_argument(
|
|
229
|
+
"--sample_sizes",
|
|
230
|
+
help="Controls whether to show sizes of datasets used in a test. Default %(default)s",
|
|
231
|
+
action=argparse.BooleanOptionalAction,
|
|
232
|
+
default=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# py3.11 doesn't accept backslashes in f-strings, so preparing that in advance
|
|
236
|
+
stat_options = "', '".join(kPossibleStatNames.keys())
|
|
237
|
+
g_rendering.add_argument(
|
|
238
|
+
"--sample_stats",
|
|
239
|
+
help="Sets which additional statistics about compared samples sets to show. Could be any sequence of floats "
|
|
240
|
+
"(must be in range [0,100], designating a percentile value to calculate), or aliases '"
|
|
241
|
+
f"{stat_options}' (shortenings are accepted). Use of 'std', though isn't "
|
|
242
|
+
"recommended as standard deviation doesn't really mean anything for most distributions "
|
|
243
|
+
"beyond Normal.",
|
|
244
|
+
nargs="+", # default value doesn't work with *, while it must
|
|
245
|
+
metavar="<stat ids>",
|
|
246
|
+
default=None,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
g_rendering.add_argument(
|
|
250
|
+
"--expect_same",
|
|
251
|
+
help="If set, assumes that distributions are the same (i.e. H0 hypothesis is true) and shows some additional "
|
|
252
|
+
"statistics useful for ensuring that a benchmark code is stable enough, or the machine is quiesced enough. "
|
|
253
|
+
"One good example is when <file1> and <file2> were made with exactly the same binary running on exactly the "
|
|
254
|
+
"same machine in the same state - on a machine with a proper setup tests shouldn't find a difference.",
|
|
255
|
+
action="store_true",
|
|
256
|
+
default=False,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
g_rendering.add_argument(
|
|
260
|
+
"--colors",
|
|
261
|
+
help="Controls if the output should be colored. Default: %(default)s",
|
|
262
|
+
action=argparse.BooleanOptionalAction,
|
|
263
|
+
default=True,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
g_rendering.add_argument(
|
|
267
|
+
"--multiline",
|
|
268
|
+
help="Controls if benchmark results could span several lines. Default: %(default)s",
|
|
269
|
+
action=argparse.BooleanOptionalAction,
|
|
270
|
+
default=False,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
g_export = parser.add_argument_group("Export","Controls how to export results.")
|
|
274
|
+
|
|
275
|
+
g_export.add_argument(
|
|
276
|
+
"--export_to",
|
|
277
|
+
help="Path to file to store comparison results to.",
|
|
278
|
+
metavar="<path/to/export_file>",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
g_export.add_argument(
|
|
282
|
+
"--export_fmt",
|
|
283
|
+
help=f"Format of the export file. Options are: {', '.join(kAvailableFormats)}. If not set, "
|
|
284
|
+
"inferred from --export_to file extension",
|
|
285
|
+
choices=kAvailableFormats,
|
|
286
|
+
default=None,
|
|
287
|
+
metavar="<format id>",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
g_export.add_argument(
|
|
291
|
+
"--export_light",
|
|
292
|
+
help="If set, uses light theme instead of dark",
|
|
293
|
+
action="store_true",
|
|
294
|
+
default=False,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return parser
|
benchstats/common.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Describes a base class for a source of statistics capable of producing (from a file or whatnot)
|
|
2
|
+
data in format suitable for calling compare::compareStats()
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import rich.console
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
import enum
|
|
10
|
+
|
|
11
|
+
kAvailableFormats = ("txt", "svg", "html")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ParserBase:
|
|
15
|
+
def __init__(self, source_id, filter, metrics, debug_log=None) -> None:
|
|
16
|
+
"""Expected source constructor.
|
|
17
|
+
- source_id is an identifier of a data source that the class knows how to process and turn
|
|
18
|
+
into inputs of compare::compareStats(). Data type is derived class implementation
|
|
19
|
+
dependent.
|
|
20
|
+
Typically it's a string referencing a file to read and parse.
|
|
21
|
+
- filter is an object describing how to filter data source when not everything from it
|
|
22
|
+
is needed.
|
|
23
|
+
Typically, it's a string with a regular expression to match against a benchmark name.
|
|
24
|
+
- metrics is a list of string representing metrics to extract for each benchmark repetition.
|
|
25
|
+
- debug_log is a flag to enable/disable logging, or a logger object like LoggingConsole to
|
|
26
|
+
send logs to. When the parser is called by benchstats CLI handler, it's always an
|
|
27
|
+
instance of LoggingConsole, so support for other variants (None or bool) are needed only
|
|
28
|
+
for built-in parsers to alleviate use of the package components in a source code.
|
|
29
|
+
"""
|
|
30
|
+
pass # derived class knows what to do
|
|
31
|
+
|
|
32
|
+
def getStats(self) -> dict[str, dict[str, Iterable[float]]]:
|
|
33
|
+
"""Return a dict that describes benchmarks and its measured statistics so it and can be
|
|
34
|
+
directly passed as any of the first two arguments to compare::compareStats(). See that
|
|
35
|
+
method for the details.
|
|
36
|
+
"""
|
|
37
|
+
raise RuntimeError("DERIVED CLASS MUST IMPLEMENT METHOD")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def detectExportFormat(export_to, export_fmt):
|
|
41
|
+
assert (export_to is None and export_fmt is None) or (
|
|
42
|
+
isinstance(export_to, str) and len(export_to) > 0
|
|
43
|
+
)
|
|
44
|
+
assert export_fmt is None or export_fmt in kAvailableFormats
|
|
45
|
+
|
|
46
|
+
if export_to is not None and export_fmt is None:
|
|
47
|
+
root, ext = os.path.splitext(export_to)
|
|
48
|
+
assert ext in [
|
|
49
|
+
"." + e for e in kAvailableFormats
|
|
50
|
+
], f"Unrecognized export file extension '{ext}' of a file in --export_to parameter"
|
|
51
|
+
export_fmt = ext[1:]
|
|
52
|
+
|
|
53
|
+
return export_fmt
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class LoggingConsole(rich.console.Console):
|
|
57
|
+
|
|
58
|
+
@enum.verify(enum.CONTINUOUS)
|
|
59
|
+
class LogLevel(enum.IntEnum):
|
|
60
|
+
Debug = (0,)
|
|
61
|
+
Info = (1,)
|
|
62
|
+
Warning = (2,)
|
|
63
|
+
Error = (3,)
|
|
64
|
+
Failure = (4,)
|
|
65
|
+
Critical = 5
|
|
66
|
+
|
|
67
|
+
def __init__(self, log_level: LogLevel = LogLevel.Debug, **kwargs):
|
|
68
|
+
assert isinstance(log_level, LoggingConsole.LogLevel)
|
|
69
|
+
self.log_level = log_level
|
|
70
|
+
if "emoji" not in kwargs:
|
|
71
|
+
kwargs["emoji"] = False
|
|
72
|
+
if "highlight" not in kwargs:
|
|
73
|
+
kwargs["highlight"] = False
|
|
74
|
+
super().__init__(**kwargs)
|
|
75
|
+
|
|
76
|
+
def _do_log(self, color: str, lvl: str, *args, **kwargs):
|
|
77
|
+
if "sep" in kwargs:
|
|
78
|
+
sep = kwargs["sep"] if len(kwargs["sep"]) > 0 else " "
|
|
79
|
+
else:
|
|
80
|
+
sep = " "
|
|
81
|
+
kwargs["sep"] = sep
|
|
82
|
+
return super().print(f"\[[{color}]{lvl:4s}[/{color}]]{sep}", *args, **kwargs)
|
|
83
|
+
|
|
84
|
+
def debug(self, *args, **kwargs):
|
|
85
|
+
if self.log_level > LoggingConsole.LogLevel.Debug:
|
|
86
|
+
return None
|
|
87
|
+
return self._do_log("gray", "dbg", *args, **kwargs)
|
|
88
|
+
|
|
89
|
+
def info(self, *args, **kwargs):
|
|
90
|
+
if self.log_level > LoggingConsole.LogLevel.Info:
|
|
91
|
+
return None
|
|
92
|
+
return self._do_log("white", "info", *args, **kwargs)
|
|
93
|
+
|
|
94
|
+
def warning(self, *args, **kwargs):
|
|
95
|
+
if self.log_level > LoggingConsole.LogLevel.Warning:
|
|
96
|
+
return None
|
|
97
|
+
return self._do_log("yellow", "warn", *args, **kwargs)
|
|
98
|
+
|
|
99
|
+
def error(self, *args, **kwargs):
|
|
100
|
+
if self.log_level > LoggingConsole.LogLevel.Error:
|
|
101
|
+
return None
|
|
102
|
+
return self._do_log("orange", "Err", *args, **kwargs)
|
|
103
|
+
|
|
104
|
+
def failure(self, *args, **kwargs):
|
|
105
|
+
if self.log_level > LoggingConsole.LogLevel.Failure:
|
|
106
|
+
return None
|
|
107
|
+
return self._do_log("red", "FAIL", *args, **kwargs)
|
|
108
|
+
|
|
109
|
+
def critical(self, *args, **kwargs):
|
|
110
|
+
if self.log_level > LoggingConsole.LogLevel.Critical:
|
|
111
|
+
return None
|
|
112
|
+
return self._do_log("magenta", "CRIT", *args, **kwargs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def bmNamesTransform(
|
|
116
|
+
stats: dict[str, dict[str, Iterable[float]]],
|
|
117
|
+
re_from: str | None,
|
|
118
|
+
re_to: str | None,
|
|
119
|
+
set_idx: int,
|
|
120
|
+
console: LoggingConsole,
|
|
121
|
+
) -> dict[str, dict[str, Iterable[float]]]:
|
|
122
|
+
assert isinstance(stats, dict) and isinstance(set_idx, int)
|
|
123
|
+
assert isinstance(console, LoggingConsole)
|
|
124
|
+
if re_from is None:
|
|
125
|
+
# not nice that it glues func args with CLI args, but ok to simplify err handling
|
|
126
|
+
assert (
|
|
127
|
+
re_to is None
|
|
128
|
+
), f"--to{set_idx} can only be used when there's a corresponding --from{set_idx}"
|
|
129
|
+
return stats
|
|
130
|
+
assert isinstance(re_from, str) and len(re_from) > 0
|
|
131
|
+
if re_to is None:
|
|
132
|
+
re_to = ""
|
|
133
|
+
else:
|
|
134
|
+
assert isinstance(re_to, str)
|
|
135
|
+
|
|
136
|
+
r = re.compile(re_from)
|
|
137
|
+
|
|
138
|
+
old_bms = stats.keys()
|
|
139
|
+
new_bms = [r.subn(re_to, bm_name)[0] for bm_name in old_bms]
|
|
140
|
+
unique_len = len(frozenset(new_bms))
|
|
141
|
+
if unique_len != len(new_bms):
|
|
142
|
+
console.warning(
|
|
143
|
+
f"--from{set_idx} -> --to{set_idx} regexp '{re_from}'->'{re_to}' is a narrowing conversion "
|
|
144
|
+
f"generates only {unique_len} unique name from {len(new_bms)}. This most likely"
|
|
145
|
+
" isn't what you want, since the order of narrowing is not specified and you might end"
|
|
146
|
+
" up having a wrong data for some new benchmark name."
|
|
147
|
+
)
|
|
148
|
+
console.debug("Old names are:", ", ".join(old_bms))
|
|
149
|
+
console.debug("New names are:", ", ".join(new_bms))
|
|
150
|
+
else:
|
|
151
|
+
assert unique_len == len(stats)
|
|
152
|
+
|
|
153
|
+
return {r.subn(re_to, bm_name)[0]: bm_metrics for bm_name, bm_metrics in stats.items()}
|