1
|
|
|
from __future__ import division |
2
|
|
|
from __future__ import print_function |
3
|
|
|
|
4
|
|
|
import argparse |
5
|
|
|
import json |
6
|
|
|
import os |
7
|
|
|
import platform |
8
|
|
|
import re |
9
|
|
|
import subprocess |
10
|
|
|
import sys |
11
|
|
|
import types |
12
|
|
|
from datetime import datetime |
13
|
|
|
from decimal import Decimal |
14
|
|
|
|
15
|
|
|
from .compat import PY3 |
16
|
|
|
|
17
|
|
|
try: |
18
|
|
|
from subprocess import check_output |
19
|
|
|
except ImportError: |
20
|
|
|
def check_output(*popenargs, **kwargs): |
21
|
|
|
if 'stdout' in kwargs: |
22
|
|
|
raise ValueError('stdout argument not allowed, it will be overridden.') |
23
|
|
|
process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) |
24
|
|
|
output, unused_err = process.communicate() |
25
|
|
|
retcode = process.poll() |
26
|
|
|
if retcode: |
27
|
|
|
cmd = kwargs.get("args") |
28
|
|
|
if cmd is None: |
29
|
|
|
cmd = popenargs[0] |
30
|
|
|
raise subprocess.CalledProcessError(retcode, cmd) |
31
|
|
|
return output |
32
|
|
|
|
33
|
|
|
|
34
|
|
|
class SecondsDecimal(Decimal): |
35
|
|
|
def __float__(self): |
36
|
|
|
return float(super(SecondsDecimal, self).__str__()) |
37
|
|
|
|
38
|
|
|
def __str__(self): |
39
|
|
|
return "{0}s".format(format_time(float(super(SecondsDecimal, self).__str__()))) |
40
|
|
|
|
41
|
|
|
@property |
42
|
|
|
def as_string(self): |
43
|
|
|
return super(SecondsDecimal, self).__str__() |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
class NameWrapper(object): |
47
|
|
|
def __init__(self, target): |
48
|
|
|
self.target = target |
49
|
|
|
|
50
|
|
|
def __str__(self): |
51
|
|
|
name = self.target.__module__ + "." if hasattr(self.target, '__module__') else "" |
52
|
|
|
name += self.target.__name__ if hasattr(self.target, '__name__') else repr(self.target) |
53
|
|
|
return name |
54
|
|
|
|
55
|
|
|
def __repr__(self): |
56
|
|
|
return "NameWrapper(%s)" % repr(self.target) |
57
|
|
|
|
58
|
|
|
|
59
|
|
|
def get_tag(): |
60
|
|
|
info = get_commit_info() |
61
|
|
|
return '%s_%s%s' % (info['id'], get_current_time(), '_uncommitted-changes' if info['dirty'] else '') |
62
|
|
|
|
63
|
|
|
|
64
|
|
|
def get_commit_info(): |
65
|
|
|
dirty = False |
66
|
|
|
commit = 'unversioned' |
67
|
|
|
try: |
68
|
|
|
if os.path.exists('.git'): |
69
|
|
|
desc = check_output('git describe --dirty --always --long --abbrev=40'.split(), |
70
|
|
|
universal_newlines=True).strip() |
71
|
|
|
desc = desc.split('-') |
72
|
|
|
if desc[-1].strip() == 'dirty': |
73
|
|
|
dirty = True |
74
|
|
|
desc.pop() |
75
|
|
|
commit = desc[-1].strip('g') |
76
|
|
|
elif os.path.exists('.hg'): |
77
|
|
|
desc = check_output('hg id --id --debug'.split(), universal_newlines=True).strip() |
78
|
|
|
if desc[-1] == '+': |
79
|
|
|
dirty = True |
80
|
|
|
commit = desc.strip('+') |
81
|
|
|
return { |
82
|
|
|
'id': commit, |
83
|
|
|
'dirty': dirty |
84
|
|
|
} |
85
|
|
|
except Exception as exc: |
86
|
|
|
return { |
87
|
|
|
'id': 'unknown', |
88
|
|
|
'dirty': dirty, |
89
|
|
|
'error': repr(exc), |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
|
93
|
|
|
def get_current_time(): |
94
|
|
|
return datetime.now().strftime("%Y%m%d_%H%M%S") |
95
|
|
|
|
96
|
|
|
|
97
|
|
|
def first_or_value(obj, value): |
98
|
|
|
if obj: |
99
|
|
|
value, = obj |
100
|
|
|
|
101
|
|
|
return value |
102
|
|
|
|
103
|
|
|
|
104
|
|
|
def load_timer(string): |
105
|
|
|
if "." not in string: |
106
|
|
|
raise argparse.ArgumentTypeError("Value for --benchmark-timer must be in dotted form. Eg: 'module.attr'.") |
107
|
|
|
mod, attr = string.rsplit(".", 1) |
108
|
|
|
if mod == 'pep418': |
109
|
|
|
if PY3: |
110
|
|
|
import time |
111
|
|
|
return NameWrapper(getattr(time, attr)) |
112
|
|
|
else: |
113
|
|
|
from . import pep418 |
114
|
|
|
return NameWrapper(getattr(pep418, attr)) |
115
|
|
|
else: |
116
|
|
|
__import__(mod) |
117
|
|
|
mod = sys.modules[mod] |
118
|
|
|
return NameWrapper(getattr(mod, attr)) |
119
|
|
|
|
120
|
|
|
|
121
|
|
|
class RegressionCheck(object): |
122
|
|
|
def __init__(self, field, threshold): |
123
|
|
|
self.field = field |
124
|
|
|
self.threshold = threshold |
125
|
|
|
|
126
|
|
|
def fails(self, current, compared): |
127
|
|
|
val = self.compute(current, compared) |
128
|
|
|
if val > self.threshold: |
129
|
|
|
return "Field %s has failed %s: %.9f > %.9f" % ( |
130
|
|
|
self.field, self.__class__.__name__, val, self.threshold |
131
|
|
|
) |
132
|
|
|
|
133
|
|
|
|
134
|
|
|
class PercentageRegressionCheck(RegressionCheck): |
135
|
|
|
def compute(self, current, compared): |
136
|
|
|
val = compared[self.field] |
137
|
|
|
if not val: |
138
|
|
|
return float("inf") |
139
|
|
|
return current[self.field] / val * 100 - 100 |
140
|
|
|
|
141
|
|
|
|
142
|
|
|
class DifferenceRegressionCheck(RegressionCheck): |
143
|
|
|
def compute(self, current, compared): |
144
|
|
|
return current[self.field] - compared[self.field] |
145
|
|
|
|
146
|
|
|
|
147
|
|
|
def parse_compare_fail(string, |
148
|
|
|
rex=re.compile('^(?P<field>min|max|mean|median|stddev|iqr):' |
149
|
|
|
'((?P<percentage>[0-9]?[0-9])%|(?P<difference>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?))$')): |
150
|
|
|
m = rex.match(string) |
151
|
|
|
if m: |
152
|
|
|
g = m.groupdict() |
153
|
|
|
if g['percentage']: |
154
|
|
|
return PercentageRegressionCheck(g['field'], int(g['percentage'])) |
155
|
|
|
elif g['difference']: |
156
|
|
|
return DifferenceRegressionCheck(g['field'], float(g['difference'])) |
157
|
|
|
|
158
|
|
|
raise argparse.ArgumentTypeError("Could not parse value: %r." % string) |
159
|
|
|
|
160
|
|
|
|
161
|
|
|
def parse_warmup(string): |
162
|
|
|
string = string.lower().strip() |
163
|
|
|
if string == "auto": |
164
|
|
|
return platform.python_implementation() == "PyPy" |
165
|
|
|
elif string in ["off", "false", "no"]: |
166
|
|
|
return False |
167
|
|
|
elif string in ["on", "true", "yes", ""]: |
168
|
|
|
return True |
169
|
|
|
else: |
170
|
|
|
raise argparse.ArgumentTypeError("Could not parse value: %r." % string) |
171
|
|
|
|
172
|
|
|
|
173
|
|
|
def parse_timer(string): |
174
|
|
|
return str(load_timer(string)) |
175
|
|
|
|
176
|
|
|
|
177
|
|
|
def parse_sort(string): |
178
|
|
|
string = string.lower().strip() |
179
|
|
|
if string not in ("min", "max", "mean", "stddev"): |
180
|
|
|
raise argparse.ArgumentTypeError( |
181
|
|
|
"Unacceptable value: %r. " |
182
|
|
|
"Value for --benchmark-sort must be one of: 'min', 'max', 'mean' or 'stddev'." % string) |
183
|
|
|
return string |
184
|
|
|
|
185
|
|
|
|
186
|
|
|
def parse_columns(string): |
187
|
|
|
allowed_columns = ["min", "max", "mean", "stddev", "median", "iqr", |
188
|
|
|
"outliers", "rounds", "iterations"] |
189
|
|
|
columns = [str.strip(s) for s in string.lower().split(',')] |
190
|
|
|
invalid = set(columns) - set(allowed_columns) |
191
|
|
|
if invalid: |
192
|
|
|
# there are extra items in columns! |
193
|
|
|
msg = "Invalid column name(s): %s. " % ', '.join(invalid) |
194
|
|
|
msg += "The only valid column names are: %s" % ', '.join(allowed_columns) |
195
|
|
|
raise argparse.ArgumentTypeError(msg) |
196
|
|
|
return columns |
197
|
|
|
|
198
|
|
|
|
199
|
|
|
def parse_rounds(string): |
200
|
|
|
try: |
201
|
|
|
value = int(string) |
202
|
|
|
except ValueError as exc: |
203
|
|
|
raise argparse.ArgumentTypeError(exc) |
204
|
|
|
else: |
205
|
|
|
if value < 1: |
206
|
|
|
raise argparse.ArgumentTypeError("Value for --benchmark-rounds must be at least 1.") |
207
|
|
|
return value |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
def parse_seconds(string): |
211
|
|
|
try: |
212
|
|
|
return SecondsDecimal(string).as_string |
213
|
|
|
except Exception as exc: |
214
|
|
|
raise argparse.ArgumentTypeError("Invalid decimal value %r: %r" % (string, exc)) |
215
|
|
|
|
216
|
|
|
|
217
|
|
|
def parse_save(string): |
218
|
|
|
if not string: |
219
|
|
|
raise argparse.ArgumentTypeError("Can't be empty.") |
220
|
|
|
illegal = ''.join(c for c in r"\/:*?<>|" if c in string) |
221
|
|
|
if illegal: |
222
|
|
|
raise argparse.ArgumentTypeError("Must not contain any of these characters: /:*?<>|\\ (it has %r)" % illegal) |
223
|
|
|
return string |
224
|
|
|
|
225
|
|
|
|
226
|
|
|
def time_unit(value): |
227
|
|
|
if value < 1e-6: |
228
|
|
|
return "n", 1e9 |
229
|
|
|
elif value < 1e-3: |
230
|
|
|
return "u", 1e6 |
231
|
|
|
elif value < 1: |
232
|
|
|
return "m", 1e3 |
233
|
|
|
else: |
234
|
|
|
return "", 1. |
235
|
|
|
|
236
|
|
|
|
237
|
|
|
def format_time(value): |
238
|
|
|
unit, adjustment = time_unit(value) |
239
|
|
|
return "{0:.2f}{1:s}".format(value * adjustment, unit) |
240
|
|
|
|
241
|
|
|
|
242
|
|
|
class cached_property(object): |
243
|
|
|
def __init__(self, func): |
244
|
|
|
self.__doc__ = getattr(func, '__doc__') |
245
|
|
|
self.func = func |
246
|
|
|
|
247
|
|
|
def __get__(self, obj, cls): |
248
|
|
|
if obj is None: |
249
|
|
|
return self |
250
|
|
|
value = obj.__dict__[self.func.__name__] = self.func(obj) |
251
|
|
|
return value |
252
|
|
|
|
253
|
|
|
|
254
|
|
|
def clonefunc(f): |
255
|
|
|
"""Deep clone the given function to create a new one. |
256
|
|
|
|
257
|
|
|
By default, the PyPy JIT specializes the assembler based on f.__code__: |
258
|
|
|
clonefunc makes sure that you will get a new function with a **different** |
259
|
|
|
__code__, so that PyPy will produce independent assembler. This is useful |
260
|
|
|
e.g. for benchmarks and microbenchmarks, so you can make sure to compare |
261
|
|
|
apples to apples. |
262
|
|
|
|
263
|
|
|
Use it with caution: if abused, this might easily produce an explosion of |
264
|
|
|
produced assembler. |
265
|
|
|
|
266
|
|
|
from: https://bitbucket.org/antocuni/pypytools/src/tip/pypytools/util.py?at=default |
267
|
|
|
""" |
268
|
|
|
|
269
|
|
|
# first of all, we clone the code object |
270
|
|
|
try: |
271
|
|
|
co = f.__code__ |
272
|
|
|
if PY3: |
273
|
|
|
co2 = types.CodeType(co.co_argcount, co.co_kwonlyargcount, |
274
|
|
|
co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code, |
275
|
|
|
co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name, |
276
|
|
|
co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars) |
277
|
|
|
else: |
278
|
|
|
co2 = types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code, |
279
|
|
|
co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name, |
280
|
|
|
co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars) |
281
|
|
|
# |
282
|
|
|
# then, we clone the function itself, using the new co2 |
283
|
|
|
return types.FunctionType(co2, f.__globals__, f.__name__, f.__defaults__, f.__closure__) |
284
|
|
|
except AttributeError: |
285
|
|
|
return f |
286
|
|
|
|
287
|
|
|
|
288
|
|
|
def format_dict(obj): |
289
|
|
|
return "{%s}" % ", ".join("%s: %s" % (k, json.dumps(v)) for k, v in sorted(obj.items())) |
290
|
|
|
|
291
|
|
|
|
292
|
|
|
def report_progress(iterable, terminal_reporter, format_string, **kwargs): |
293
|
|
|
total = len(iterable) |
294
|
|
|
|
295
|
|
|
def progress_reporting_wrapper(): |
296
|
|
|
for pos, item in enumerate(iterable): |
297
|
|
|
string = format_string.format(pos=pos + 1, total=total, value=item, **kwargs) |
298
|
|
|
terminal_reporter.rewrite(string, black=True, bold=True) |
299
|
|
|
yield string, item |
300
|
|
|
return progress_reporting_wrapper() |
301
|
|
|
|