Skip to content

Commit 6c3b886

Browse files
committed
scripts
1 parent 093dabf commit 6c3b886

File tree

2 files changed

+131
-88
lines changed

2 files changed

+131
-88
lines changed

scripts/Dockerfile.pypy3

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM pypy:3-bookworm
2+
3+
RUN apt-get update && \
4+
apt-get upgrade -y && \
5+
apt-get install -y libgeos-dev
6+
7+
COPY requirements.txt /
8+
RUN pip install -r requirements.txt

scripts/runall.py

Lines changed: 123 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from operator import itemgetter
1414
from pathlib import Path
1515
from zlib import crc32
16+
import sqlite3
1617

1718
RED = "\033[91m"
1819
GREEN = "\033[92m"
@@ -32,114 +33,128 @@
3233

3334
LANGUAGES = {
3435
"Python": "{year}/day{day}/day{day}.py",
35-
"PyPy": "{year}/day{day}/day{day}.py",
36+
# "PyPy": "{year}/day{day}/day{day}.py",
3637
"Rust": "{year}/target/release/day{day}",
3738
"C": "{year}/build/day{day}_c",
3839
"C++": "{year}/build/day{day}_cpp",
3940
}
4041

42+
INTERPRETERS = {
43+
"Python": "python3",
44+
"PyPy": "pypy3",
45+
}
46+
4147

4248
def get_cache():
4349
"""Retrieve the cache instance from memory or load it from disk."""
4450
cache = globals().get("_cache")
4551
if cache is None:
46-
cache_file = Path(__file__).parent.parent / "data" / "cache.json"
47-
if cache_file.is_file():
48-
cache = json.loads(cache_file.read_bytes())
49-
else:
50-
cache = {}
52+
cache_file = Path(__file__).parent.parent / "data" / "cache.db"
53+
54+
cache_db = sqlite3.connect(cache_file)
55+
56+
cache_db.executescript(
57+
"create table if not exists solutions ("
58+
" key text primary key not null,"
59+
" mtime_ns int,"
60+
" elapsed float,"
61+
" status text,"
62+
" answers text);"
63+
)
64+
cache = {"db": cache_db, "modified": False}
5165
globals()["_cache"] = cache
52-
cache["modified"] = False
66+
5367
return cache
5468

5569

5670
def save_cache():
57-
cache = get_cache()
58-
if cache["modified"]:
59-
cache.pop("modified")
60-
cache_file = Path(__file__).parent.parent / "data" / "cache.json"
61-
cache_file.write_text(json.dumps(cache, indent=2, ensure_ascii=True))
62-
cache["modified"] = False
63-
print(f"{FEINT}{ITALIC}cache commited{RESET}")
71+
pass
72+
# cache = get_cache()
73+
# if cache["modified"]:
74+
# cache["db"].commit()
75+
# cache["modified"] = False
76+
# print(f"{FEINT}{ITALIC}cache commited{RESET}")
6477

6578

66-
def check_cache(key, file_timestamp: Path, no_check=False):
79+
def check_cache(key, file_timestamp: Path, no_age_check=False):
6780
cache = get_cache()
6881
key = str(key)
69-
e = cache.get(key, None)
70-
if e:
82+
db = cache["db"]
83+
cursor = db.execute("select mtime_ns,elapsed,status,answers from solutions where key=?", (key,))
84+
row = cursor.fetchone()
85+
if row:
7186
timestamp = file_timestamp.stat().st_mtime_ns
72-
if e["timestamp"] == timestamp or no_check:
73-
return e
87+
if row[0] == timestamp or no_age_check:
88+
return {
89+
"elapsed": row[1],
90+
"status": row[2],
91+
"answers": row[3].split("\n"),
92+
}
7493
else:
75-
seconds = round((timestamp - e["timestamp"]) / 1000000000)
76-
delta = timedelta(seconds=seconds)
94+
# seconds = round((timestamp - e["timestamp"]) / 1000000000)
95+
# delta = timedelta(seconds=seconds)
96+
# print(f"{FEINT}{ITALIC}entry {key} is out of date for {delta}{RESET}", end=f"{CR}")
7797

78-
print(f"{FEINT}{ITALIC}entry {key} is out of date for {delta}{RESET}", end=f"{CR}")
98+
print(f"{FEINT}{ITALIC}entry {key} is out of date{RESET}", end=f"{CR}")
7999

80100
else:
81-
print(f"{FEINT}{ITALIC}missing cache for {key}{RESET}", end=f"{CR}")
101+
print(f"{FEINT}{ITALIC}missing cache for {key}{RESET}", end=f"{CLEAR_EOL}{CR}")
82102

83103

84-
def update_cache(key, timestamp: Path, elapsed, status, answers):
104+
def update_cache(key, timestamp: Path, elapsed: float, status: str, answers: t.Iterable):
85105
cache = get_cache()
106+
db = cache["db"]
86107
key = str(key)
87-
e = cache.get(key, {})
88-
e["timestamp"] = timestamp.stat().st_mtime_ns
89-
e["elapsed"] = elapsed
90-
e["status"] = status
91-
e["answers"] = answers
92-
cache[key] = e
93-
cache["modified"] = True
94-
return e
95108

109+
db.execute(
110+
"insert or replace into solutions (key,mtime_ns,elapsed,status,answers) values (?,?,?,?,?)",
111+
(key, timestamp.stat().st_mtime_ns, elapsed, status, "\n".join(answers)),
112+
)
113+
114+
# cache["modified"] = True
115+
db.commit()
96116

97-
def run(key: str, prog: Path, lang: str, file: Path, solution: t.List, refresh: bool, dry_run: bool):
117+
return {
118+
"elapsed": elapsed,
119+
"status": status,
120+
"answers": answers,
121+
}
122+
123+
124+
def run(prog: Path, lang: str, file: Path, solution: t.List, warmup: bool) -> t.Dict[str, t.Any]:
98125
if not prog.is_file():
99126
return
100127

101128
cmd = [prog.absolute().as_posix()]
102129

103-
if lang == "Python":
104-
cmd.insert(0, "python3")
105-
elif lang == "PyPy":
106-
cmd.insert(0, "pypy3")
107-
108-
if refresh:
109-
e = None
110-
else:
111-
e = check_cache(key, prog, dry_run)
112-
if dry_run and not e:
113-
return None
114-
if e:
115-
in_cache = True
130+
# add the interpreter
131+
interpreter = INTERPRETERS.get(lang)
132+
if interpreter:
133+
cmd.insert(0, interpreter)
134+
135+
if warmup and lang == "Rust":
136+
# under macOS, the first launch of a Rust program is slower (why ???)
137+
subprocess.call(cmd + ["--help"], stdout=subprocess.DEVNULL)
138+
139+
start = time.time_ns()
140+
out = subprocess.run(cmd + [file.absolute()], stdout=subprocess.PIPE)
141+
elapsed = time.time_ns() - start
142+
answers = out.stdout.decode().strip()
143+
144+
status = "unknown"
145+
if solution:
146+
solution = solution.read_text()
147+
if answers == solution.strip():
148+
status = "ok"
149+
else:
150+
status = "error"
116151
else:
117-
in_cache = False
118-
119-
start = time.time_ns()
120-
out = subprocess.run(cmd + [file.absolute()], stdout=subprocess.PIPE)
121-
elapsed = time.time_ns() - start
122-
answers = out.stdout.decode().strip()
123-
124-
status = "unknown"
125-
if solution:
126-
solution = solution.read_text()
127-
if answers == solution.strip():
128-
status = "ok"
129-
else:
130-
status = "error"
152+
if answers == "":
153+
status = "missing"
131154
else:
132-
if answers == "":
133-
status = "fail"
134-
else:
135-
status = "unknown"
136-
137-
e = update_cache(key, prog, elapsed, status, answers.split("\n"))
138-
139-
e = deepcopy(e)
140-
e["cache"] = in_cache
155+
status = "unknown"
141156

142-
return e
157+
return {"elapsed": elapsed, "status": status, "answers": answers.split("\n")}
143158

144159

145160
def make(year: Path, source: Path, dest: Path, cmd: str):
@@ -155,11 +170,11 @@ def make(year: Path, source: Path, dest: Path, cmd: str):
155170
return
156171

157172
cmdline = f"{cmd} -o {output} -Wall -Wextra -O3 -DSTANDALONE -I{source.parent} {source}"
158-
print(cmdline)
173+
print(f"{CR}{cmdline}", end="")
159174
subprocess.check_call(cmdline, shell=True)
160175

161176

162-
def build_all(filter_year):
177+
def build_all(filter_year: int):
163178
for year in range(2015, 2024):
164179
if filter_year != 0 and year != filter_year:
165180
continue
@@ -183,13 +198,20 @@ def build_all(filter_year):
183198
make(year, src, f"day{day}_cpp", "c++ -std=c++17")
184199

185200

186-
def load_data(filter_year):
201+
def load_data(filter_year, filter_user):
187202
inputs = defaultdict(dict)
188203
solutions = defaultdict(dict)
189204

190205
for f in Path("data").rglob("*.in"):
206+
207+
if f.name.startswith("._"):
208+
continue
209+
191210
assert len(f.parts) == 4
192211

212+
if filter_user and f.parent.parent.name != filter_user:
213+
continue
214+
193215
year = int(f.parent.name)
194216
day = int(f.stem)
195217

@@ -201,7 +223,7 @@ def load_data(filter_year):
201223
crc = e["status"]
202224
else:
203225
crc = hex(crc32(f.read_bytes().strip()) & 0xFFFFFFFF)
204-
update_cache(f, f, 0, crc, 0)
226+
update_cache(f, f, 0, crc, [])
205227

206228
if crc not in inputs[year, day]:
207229
inputs[year, day][crc] = f
@@ -219,7 +241,7 @@ def run_day(
219241
):
220242
elapsed = defaultdict(list)
221243

222-
first = True
244+
warmup = defaultdict(lambda: True)
223245

224246
day_suffix = mday.removeprefix(str(day))
225247
name_max_len = 16 - len(day_suffix)
@@ -242,12 +264,20 @@ def run_day(
242264
prog = Path(pattern.format(year=year, day=mday))
243265
key = ":".join(map(str, (year, day, crc, prog, lang.lower())))
244266

245-
if lang.lower() == "rust" and first and prog.is_file():
246-
# under macOS, the first launch of a program is slower
247-
first = False
248-
subprocess.call([prog, "--help"], stdout=subprocess.DEVNULL)
267+
if refresh:
268+
e = None
269+
in_cache = False
270+
else:
271+
e = check_cache(key, prog, dry_run)
272+
in_cache = e is not None
273+
274+
if not in_cache and not dry_run:
275+
276+
e = run(prog, lang, file, day_sols.get(crc), warmup[lang])
249277

250-
e = run(key, prog, lang, file, day_sols.get(crc), refresh, dry_run)
278+
if e:
279+
warmup[lang] = False
280+
e = update_cache(key, prog, e["elapsed"], e["status"], e["answers"])
251281

252282
if not e:
253283
continue
@@ -257,24 +287,24 @@ def run_day(
257287
else:
258288
info = ""
259289

260-
status_color = {"fail": MAGENTA, "unknown": GRAY, "error": RED, "ok": GREEN}[e["status"]]
290+
status_color = {"missing": MAGENTA, "unknown": GRAY, "error": RED, "ok": GREEN}[e["status"]]
261291

262292
line = (
263293
f"{CR}{RESET}{CLEAR_EOL}"
264294
f"{prefix}"
265295
f" {YELLOW}{lang:<7}{RESET}:"
266296
f" {status_color}{e['status']:7}{RESET}"
267297
f" {WHITE}{e['elapsed']/1e9:7.3f}s"
268-
f" {GRAY}{'☽' if e['cache'] else ' '}"
298+
f" {GRAY}{'☽' if in_cache else ' '}"
269299
f" {status_color}{str(e['answers']):<40}{RESET}"
270300
f"{info}"
271301
)
272302
print(line)
273303

274-
if e["status"] == "fail" or e["status"] == "error":
304+
if e["status"] == "missing" or e["status"] == "error":
275305
problems.append(line)
276306

277-
if not e["cache"] and e["elapsed"] / 1e9 > 5:
307+
if not in_cache and e["elapsed"] / 1e9 > 5:
278308
save_cache()
279309

280310
results.add(" ".join(e["answers"]))
@@ -298,11 +328,13 @@ def main():
298328
parser.add_argument("-l", "--language", type=str, metavar="LANG", help="filter by language")
299329
parser.add_argument("-r", "--refresh", action="store_true", help="relaunch solutions")
300330
parser.add_argument("-n", "--dry-run", action="store_true", help="do not run")
331+
parser.add_argument("--no-build", action="store_true", help="do not build")
332+
parser.add_argument("-u", "--user", dest="filter_user", type=str, help="filter by user id")
301333
parser.add_argument("n", type=int, nargs="*", help="filter by year or year/day")
302334

303335
args = parser.parse_args()
304336

305-
filter_year = 0 if len(args.n) == 0 else args.n.pop(0)
337+
filter_year = 0 if len(args.n) == 0 else int(args.n.pop(0))
306338
filter_day = set(args.n)
307339
if args.language == "cpp":
308340
args.language = "c++"
@@ -313,8 +345,11 @@ def main():
313345
problems = []
314346
stats_elapsed = dict()
315347

316-
build_all(filter_year)
317-
inputs, sols = load_data(filter_year)
348+
if not args.no_build:
349+
build_all(filter_year)
350+
print(end=f"{CR}{CLEAR_EOL}")
351+
352+
inputs, sols = load_data(filter_year, args.filter_user)
318353

319354
for year in range(2015, 2024):
320355
if filter_year != 0 and year != filter_year:
@@ -342,7 +377,7 @@ def main():
342377

343378
if elapsed:
344379
print(
345-
"--> ",
380+
f"{CLEAR_EOL}--> ",
346381
" | ".join((f"{lang} : {t:.3f}s" for lang, t in elapsed.items())),
347382
f"{FEINT}({nb_samples} input{'s' if nb_samples>1 else ''}){RESET}",
348383
)

0 commit comments

Comments
 (0)