preflight печатал последнюю строку вывода упавшего прогона, а у паники Rust
последняя строка — «note: run with RUST_BACKTRACE=1», то есть ноль сведений о
причине. Теперь из вывода достаётся собственное сообщение решателя, а для паники —
то, что стоит ПОСЛЕ строки «panicked at». Вместо
A10_turb_n2048_kbc note: run with `RUST_BACKTRACE=1` ...
печатается
A10_turb_n2048_kbc
wgpu error: Validation Error | In Device::create_bind_group, label = 'level'
| Buffer binding 0 range 150994944 exceeds `max_*_buffer_binding_size` limit 134217728
Заодно задокументировано само ограничение. dzn объявляет
max_storage_buffer_binding_size = 128 МиБ против гигабайтов у нативных драйверов,
при том что max_buffer_size у него 2047 МиБ: держать большой буфер можно, показать
шейдеру одной привязкой — нет. Потолок выходит 3.73 млн узлов (около 1920x1920),
и 14 прогонов кампании из 115 в него не влезают. На эти 14 приходится 70.7%
стоимости, так что для кампании ограничение решающее.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.8 KiB
Python
114 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
# Предполётная проверка кампании: каждый сценарий запускается на два шага.
|
|
#
|
|
# python preflight.py # проверить все
|
|
# python preflight.py --group I # только группу
|
|
#
|
|
# Ловит опечатки в ключах, несовместимые сочетания и пределы железа ДО того, как кампания
|
|
# уйдёт считать на девяносто часов. Не бесплатная привычка, а окупившаяся: именно так
|
|
# выяснилось, что вся группа сверхмелких сеток падала на пределе GPU в 65535 рабочих групп
|
|
# на измерение, а девять прогонов группы F передавали --body-x дважды.
|
|
#
|
|
# Физику проверка не трогает вовсе: два шага — это ровно «запустилось и не упало».
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
BIN = os.environ.get("KBC2D_BIN") or os.path.join(
|
|
HERE, "..", "target", "release", "kbc2d" + (".exe" if os.name == "nt" else ""))
|
|
|
|
# Всё, что пишет файлы, выкидываем: проверяем запуск, а не вывод. Ключ идёт со значением,
|
|
# поэтому следующий за ним аргумент тоже пропускается.
|
|
DROP = {"--gif", "--xt", "--case-csv", "--gif-field", "--gif-downsample", "--gif-scale",
|
|
"--gif-every", "--gif-fps", "--series-every"}
|
|
|
|
|
|
# Строки, которые ничего не объясняют и только вытесняют полезные.
|
|
NOISE = (
|
|
"note: run with `RUST_BACKTRACE",
|
|
"note: Some details are omitted",
|
|
"WARNING: dzn is not a conformant",
|
|
"stack backtrace",
|
|
"Caused by:",
|
|
)
|
|
|
|
|
|
def explain(output):
|
|
"""Достать из вывода упавшего прогона то, ради чего его вообще читают.
|
|
|
|
Раньше бралась последняя строка вывода — а у паники Rust последняя строка это
|
|
«note: run with RUST_BACKTRACE=1», то есть ровно ноль сведений о причине.
|
|
Полезное лежит либо в собственном сообщении решателя, либо на строке ПОСЛЕ
|
|
«panicked at»: там текст самой паники.
|
|
"""
|
|
out = [l.rstrip() for l in output.splitlines() if l.strip()]
|
|
out = [l for l in out if not any(n in l for n in NOISE)]
|
|
if not out:
|
|
return "(без вывода)"
|
|
for l in out:
|
|
if l.lstrip().startswith("ОШИБКА"):
|
|
return l.strip()
|
|
for i, l in enumerate(out):
|
|
if "panicked at" in l:
|
|
rest = [x.strip() for x in out[i + 1:]]
|
|
return " | ".join(rest[:4]) if rest else l.strip()
|
|
return out[-1].strip()
|
|
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--group", help="только эта группа, например I")
|
|
ap.add_argument("--scenarios", default=os.path.join(HERE, "scenarios.json"))
|
|
args = ap.parse_args()
|
|
|
|
runs = json.load(open(args.scenarios, encoding="utf-8"))["runs"]
|
|
if args.group:
|
|
runs = [r for r in runs if r["group"].upper() == args.group.upper()]
|
|
if not runs:
|
|
print("под выборку не попал ни один прогон")
|
|
return 0
|
|
if not os.path.exists(BIN):
|
|
print(f"не найден бинарь решателя: {BIN}")
|
|
return 1
|
|
|
|
bad = []
|
|
for i, r in enumerate(runs, 1):
|
|
argv, skip = [], False
|
|
for a in r["args"]:
|
|
if skip:
|
|
skip = False
|
|
continue
|
|
if a in DROP:
|
|
skip = True
|
|
continue
|
|
argv.append(a)
|
|
if "--steps" in argv:
|
|
argv[argv.index("--steps") + 1] = "2"
|
|
else:
|
|
argv += ["--steps", "2"]
|
|
|
|
p = subprocess.run([BIN] + argv + ["--verbose", "quiet", "--report-every", "0"],
|
|
capture_output=True, cwd=HERE)
|
|
ok = p.returncode == 0
|
|
print("." if ok else "X", end="", flush=True)
|
|
if not ok:
|
|
out = (p.stdout + p.stderr).decode("utf-8", "replace")
|
|
bad.append((r["id"], explain(out)))
|
|
if i % 40 == 0:
|
|
print(f" {i}/{len(runs)}", flush=True)
|
|
|
|
print(f"\n\nпроверено сценариев: {len(runs)}, не запустились: {len(bad)}")
|
|
for id_, msg in bad:
|
|
print(f" {id_}")
|
|
print(f" {msg}")
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|