-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathupdate-ulimit-exe.py
More file actions
executable file
·284 lines (251 loc) · 11.1 KB
/
Copy pathupdate-ulimit-exe.py
File metadata and controls
executable file
·284 lines (251 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#!/usr/bin/env python3
"""
Set how long every model of a library may spend simulating, from what it has
spent: `ulimitExe` for the library, `ulimitExeModels` for the models that have
earned longer than that.
./update-ulimit-exe.py --db postgresql://om@openmodelica.org/omdb configs/conf.json
./update-ulimit-exe.py --db postgresql://om@openmodelica.org/omdb --write configs/*.json
The first prints what would change, the second changes it in place, leaving the
rest of the file - key order, indentation, tabs and all - alone. Neither writes
to the database, so a read-only user is enough for both.
A model is allowed --factor times the longest it has taken over --runs runs of
--branch, master by default: it runs on the slower of the test machines, so a
model fast enough there is fast enough everywhere. Both halves of that matter -
the same model has been seen to take six times its usual time when the machine
is busy, so a handful of runs does not show what a model needs, and the longest
of a few months of them still wants a margin on top. Only runs that finished
simulating are read; one killed by the timeout has no time to measure, and is
reported rather than guessed at.
"""
import argparse, math, re
import simplejson as json
import resultsdb, shared
# Rounding the timeouts keeps a re-run from rewriting them over rounding noise.
STEP = 30
# A run that spent this much of its timeout and still failed was killed by it.
KILLED_FRACTION = 0.95
def roundUp(seconds):
return int(math.ceil(seconds / float(STEP)) * STEP)
def lastRuns(cursor, db, branch, runs):
"""The dates of the newest runs of that branch, newest first."""
return [row[0] for row in cursor.execute(
"SELECT DISTINCT date FROM %s ORDER BY date DESC LIMIT %d" % (db.quote(branch), runs))]
def simulationTimes(cursor, db, branches, runs):
"""For every model of every library, the longest it has been seen to simulate
and the longest it ran before failing, over the newest runs of each branch."""
best = {}
failed = {}
for branch in branches:
dates = lastRuns(cursor, db, branch, runs)
if not dates:
print("No results for branch %s" % branch)
continue
holes = ",".join("?" * len(dates))
for (libname, model, finalphase, simulate) in cursor.execute(
"SELECT libname, model, finalphase, MAX(simulate) FROM %s WHERE date IN (%s) "
"GROUP BY libname, model, finalphase" % (db.quote(branch), holes), tuple(dates)):
key = (libname, model)
target = best if finalphase >= 6 else failed
target[key] = max(target.get(key, 0.0), simulate)
return (best, failed)
def libraryLimit(entry, default):
"""The timeout the models of that library get without one of their own."""
return int(entry.get("ulimitExe") or default)
def wanted(entry, libname, best, failed, default, factor):
"""That library's timeout, the models allowed longer and the models that were
killed, or None for a library the database has never heard of - guessing there
would replace a hand-written timeout with a default nobody measured."""
measured = dict((model, t) for ((lib, model), t) in best.items() if lib == libname)
killed = dict((model, t) for ((lib, model), t) in failed.items() if lib == libname)
if not measured and not killed:
return None
# A timeout under the default is a deliberately short one and stays; a longer
# one is what the named models replaced.
limit = min(libraryLimit(entry, default), default)
models = dict((model, roundUp(t * factor)) for (model, t) in measured.items() if t > limit)
inForce = lambda model: (entry.get("ulimitExeModels") or {}).get(model) \
or libraryLimit(entry, default)
wasKilled = sorted(model for (model, t) in killed.items()
if t >= KILLED_FRACTION * inForce(model))
return (limit, models, wasKilled)
def entrySpans(text):
"""Where each library entry starts and ends: the objects directly inside the
outermost list. The files are edited as text rather than re-serialised, so
that setting one number does not reformat the other ninety-five entries."""
spans = []
depth = 0
start = None
inString = False
escaped = False
for (i, ch) in enumerate(text):
if inString:
if escaped:
escaped = False
elif ch == "\\":
escaped = True
elif ch == '"':
inString = False
continue
if ch == '"':
inString = True
elif ch in "[{":
depth = depth + 1
if depth == 2 and ch == "{":
start = i
elif ch in "]}":
if depth == 2 and ch == "}" and start is not None:
spans.append((start, i + 1))
start = None
depth = depth - 1
return spans
keyRe = re.compile(r'^(\s*)"([A-Za-z]+)"\s*:')
# A new timeout goes after these, where the hand-written ones are.
AFTER_KEYS = ["ulimitOmc", "libraryVersionNameForTests", "libraryVersionExactMatch",
"libraryVersion", "library"]
def rewriteEntry(entry, limit, models, default):
"""That entry's text with its timeouts replaced by these, and nothing else
touched. The keys are dropped rather than written out when they say nothing."""
lines = entry.split("\n")
out = []
indent = " "
skipTo = None
for (i, line) in enumerate(lines):
if skipTo is not None:
if i < skipTo:
continue
skipTo = None
m = keyRe.match(line)
if m and m.group(2) in ("ulimitExe", "ulimitExeModels"):
indent = m.group(1)
if m.group(2) == "ulimitExeModels" and not line.rstrip().endswith(("}", "},")):
# A block spanning several lines ends at the first line that closes it.
skipTo = next(j for j in range(i + 1, len(lines))
if lines[j].strip() in ("}", "},")) + 1
continue
if m and m.group(2) in AFTER_KEYS:
indent = m.group(1)
out.append(line)
written = []
if limit != default:
written.append('%s"ulimitExe":%d,' % (indent, limit))
if models:
written.append('%s"ulimitExeModels":{' % indent)
for (i, model) in enumerate(sorted(models)):
written.append('%s "%s":%d%s' % (indent, model, models[model],
"" if i == len(models) - 1 else ","))
written.append("%s}," % indent)
# After the last of the keys that name the library, or first if it has none.
at = 1
for (i, line) in enumerate(out):
m = keyRe.match(line)
if m and m.group(2) in AFTER_KEYS:
at = i + 1
lines = out[:at] + written + out[at:]
# Inserting or removing a block moves which key is the last one, the only one
# without a comma.
if written and at > 0:
lines[at - 1] = lines[at - 1].rstrip().rstrip(",") + ","
if len(lines) > 1 and lines[-1].strip() == "}":
lines[-2] = lines[-2].rstrip().rstrip(",")
return "\n".join(lines)
def describe(libname, entry, limit, models, wasKilled, default):
"""What changes for that library, or nothing when it already says this."""
was = (libraryLimit(entry, default), entry.get("ulimitExeModels") or {})
now = (limit, models)
lines = []
if was[0] != now[0]:
lines.append(" timeout %ds -> %ds" % (was[0], now[0]))
for model in sorted(set(list(was[1]) + list(now[1]))):
old = was[1].get(model)
new = now[1].get(model)
if old == new:
continue
elif old is None:
lines.append(" + %s %ds" % (model, new))
elif new is None:
lines.append(" - %s (was %ds, no longer needed)" % (model, old))
else:
lines.append(" ~ %s %ds -> %ds" % (model, old, new))
for model in wasKilled:
lines.append(" ! %s ran into the timeout in force, so there is nothing to measure" % model)
if lines:
lines.insert(0, "%s:" % libname)
return lines
def main():
parser = argparse.ArgumentParser(
description="Set the simulation timeouts of the tested libraries from their results",
formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__)
parser.add_argument("configs", nargs="+")
parser.add_argument("--branch", default="master",
help="Branch whose results decide the timeouts, or several separated by "
"spaces, in which case each model is allowed what the slowest of them "
"needed. Defaults to master, which runs on the slower test machines.")
parser.add_argument("--runs", type=int, default=100,
help="How many of the newest runs of each branch to read (default 100). "
"Fewer than a few dozen and the occasional slow run is missed.")
parser.add_argument("--factor", type=float, default=1.25,
help="How much longer than it has ever taken a model is allowed "
"(default 1.25)")
parser.add_argument("--default", type=int, default=shared.DEFAULT_ULIMIT_EXE,
help="The timeout a model gets when nothing asks for another, which is "
"what shared.py says unless overridden here")
parser.add_argument("--write", action="store_true",
help="Change the configuration files instead of only saying what would change")
resultsdb.addArgument(parser)
args = parser.parse_args()
db = resultsdb.connect(args.db)
cursor = db.cursor()
branches = [shared.resultTable(b) for b in args.branch.split(" ") if b]
(best, failed) = simulationTimes(cursor, db, branches, args.runs)
changed = False
for path in args.configs:
entries = shared.readConfig(path)
text = open(path).read()
spans = entrySpans(text)
if len(spans) != len(entries):
raise SystemExit("%s: found %d entries but %d objects in the file"
% (path, len(entries), len(spans)))
report = []
pieces = []
intended = []
at = 0
for ((library, conf), (start, end)) in zip(entries, spans):
raw = conf["configFromFile"]
libname = shared.libname(library, conf)
w = wanted(raw, libname, best, failed, args.default, args.factor)
pieces.append(text[at:start])
at = end
if w is None:
report.append("%s: no results on %s, left alone" % (libname, ", ".join(branches)))
pieces.append(text[start:end])
intended.append(dict(raw))
continue
(limit, models, wasKilled) = w
report.extend(describe(libname, raw, limit, models, wasKilled, args.default))
pieces.append(rewriteEntry(text[start:end], limit, models, args.default))
entry = dict(raw)
entry.pop("ulimitExe", None)
entry.pop("ulimitExeModels", None)
if limit != args.default:
entry["ulimitExe"] = limit
if models:
entry["ulimitExeModels"] = models
intended.append(entry)
pieces.append(text[at:])
new = "".join(pieces)
print("== %s" % path)
print("\n".join(report) if report else " nothing to change")
if new == text:
continue
changed = True
# Editing the text keeps the formatting; it must not cost the contents.
if json.loads(new) != intended:
raise SystemExit("%s: the rewritten file does not say what it was meant to say; "
"not writing it" % path)
if args.write:
open(path, "w").write(new)
print(" written")
if changed and not args.write:
print("\nNothing was written. Pass --write to change the files.")
if __name__ == "__main__":
main()