d45629ae29
build / mod (push) Failing after 22s
The Windows log for #122 showed the mod stopping in the UI event (fuel budget 1e9, remaining 0) with known_units <- order_raid <- launch_wave <- ui_select: order_raid recomputed the planner snapshot known_units() for every drone, and the snapshot itself rescanned the whole force and the dedup list per intel entry. A second big Smart order cost drones x intel-log x force-size inside one event. Reproduced in actual WASM with measured per-callback fuel through the exact ui_select(BTN_LAUNCH) handler (diagnostic guest, trigger.patch): before, a 111-drone second order at revealed=13172/known=144 exhausted exactly 1e9 and stopped the mod (an 11-drone order at the same state: 247805865); after, the same scenario passes with max command fuel 32946177/1e9, failures=0, and /stats at tick 2602 byte-identical. A stacked single-point layout dies to one warhead (known=0, cheap order), so the scenario deploys spread clusters with a moved player. Fix: one known_units snapshot per order_raid passed to radar_strikes and choose_target; known_units is a single O(log + force) set pass; price_grid_on reuses its snapshot. Emitter/network targeting and carrier choice semantics are unchanged (native equivalence test against the former predicate; WASM checkpoint counters equal). Limits unchanged; HD/recording untouched; wave size unrestricted. Committed regressions: native snapshot-equivalence and second-smart-order tests; the heavy WASM fixture now orders two Smart raids through the wave screen's launch function (airdef preset + airdef raid) with EW and a combat intel log, four accepted orders asserted; the light contract covers preset and the empty-wave refusal. Core release 195 + 8 scenarios; adapter 65 + 1 boundary + 5 scenarios; light WASM 30 steps; heavy WASM 283 steps (tick max 182413686/300M, command max 6939810/1e9, failures 0). Windows GUI acceptance remains external and is not claimed. Co-Authored-By: Kimi Code (Moonshot AI) <noreply@moonshot.cn>
141 lines
14 KiB
Markdown
141 lines
14 KiB
Markdown
# Два крупных налёта: проверка стабильности
|
||
|
||
Пользовательский отчёт: AirDef часто перестаёт работать при двух больших налётах,
|
||
резкого обвала FPS перед этим не замечено. RTX 3060 + Ryzen 5600X — около 50 FPS во время налёта.
|
||
Пользователь уточнил: игра продолжает работать, отключается только AirDef; режим эффектов — HD.
|
||
Точный build и старый лог не предоставлены. Horizon архивирует предыдущие логи в `logs/YYYY-MM-DD-N.log.gz`,
|
||
поэтому запись может сохраниться и без ручного копирования.
|
||
Задача: [#122](https://gitea.maoniao.team/chexit/airdef/issues/122).
|
||
|
||
**Воспроизведены и исправлены две конкретные причины отключения в этом классе нагрузки.**
|
||
Первая — тик и кэш высот (`1f28f83`, ниже). Вторая — событие UI и второй приказ
|
||
(`order_raid`, см. раздел «Второй налёт из UI: событие, не тик»). Без старого лога не
|
||
утверждается, что Windows-сеанс имел именно их; GPU/FPS отдельно не измерялись.
|
||
|
||
## Второй налёт из UI: событие, не тик
|
||
|
||
Точный лог Windows: `Horizon: mod airdef stopped in UI event (fuel budget 1000000000,
|
||
remaining Some(0))`; backtrace: `ew::Sim::known_units` ← `raid::Sim::order_raid` ←
|
||
`airdef::AirDef::launch_wave` ← `ui_select`. Прежний fix `1f28f83` — другой класс: тогда
|
||
мод останавливался в **тике** на 300M.
|
||
|
||
**Причина.** `order_raid` для каждого дрона заново вычислял снимок известных комплексов
|
||
`known_units()` (`choose_target`), а сам `known_units()` на каждую запись журнала разведки
|
||
пересканировал весь список комплексов и список уже принятых. Журнал держит каждый пуск,
|
||
каждое раскрытие РЭБ и каждую emitting-РЛС за последние 6000 тиков. Стоимость второго
|
||
большого приказа росла как `дроны × журнал × размер группировки` внутри одного UI-события.
|
||
|
||
**Воспроизведение** (настоящий WASM, измеренный fuel по callback, диагностический гость —
|
||
`trigger.patch` гоняет тот же обработчик `ui_select(BTN_LAUNCH)` → `launch_wave` →
|
||
`order_raid` с максимальным составом меню; метка callback — `command`, бюджет тот же 1e9):
|
||
разнесённая группировка 172 комплекса (40 кластеров × 4 тяжёлых + 12 РЭБ, игрок двигается
|
||
шагами сценария — стек в одной точке умирает от одной боеголовки и не воспроизводит),
|
||
первый налёт 111 дронов отбит, второй приказ на тике ~2600 при `revealed=13172`,
|
||
`known=144` живых. До fix: второй приказ исчерпывает ровно 1e9 и останавливает мод
|
||
(`before-trap.txt`); тот же приказ из 11 дронов в том же состоянии — 247805865
|
||
(`before-scaling.txt`). После fix тот же сценарий проходит целиком: max command fuel
|
||
32946177/1e9, max tick 236581288/300M, ошибок 0; `/stats` на тике 2602 совпал побайтово
|
||
до/после, включая счётчики мира.
|
||
|
||
**Фикс.** Один снимок `known_units` на весь приказ, передаётся в `radar_strikes` и
|
||
`choose_target`; сам `known_units` — один проход O(журнал + группировка) через множества;
|
||
`price_grid_on` переиспользует свой снимок. Семантика планирования не изменилась:
|
||
нативный тест сверяет новый снимок с прежним предикатом (дубликаты, погибшие, устаревшие,
|
||
чужие id, порядок «новые первыми»). Лимиты не менялись, HD/запись не отключались,
|
||
размер волны не ограничивался.
|
||
|
||
**Регрессии.** Нативные: эквивалентность снимка и «второй умный приказ с толстым журналом
|
||
принят и запущен». Закреплённые WASM: тяжёлый fixture расширен — два умных приказа через
|
||
функцию кнопки ПУСК (`airdef preset 1` + `airdef raid` — тот же `launch_wave`, что и у
|
||
`ui_select`) с РЭБ и боевым журналом, оба приняты (`expect.chat = []`), 4 принятых приказа
|
||
в отчёте; лёгкий контракт проверяет `preset` и отказ пустой волны. Данные и точный
|
||
сценарий: [summary.json](measurements/issue-122-ui-fuel/summary.json),
|
||
[trigger.patch](measurements/issue-122-ui-fuel/trigger.patch),
|
||
[до](measurements/issue-122-ui-fuel/scenario-before.toml),
|
||
[после](measurements/issue-122-ui-fuel/scenario-after.toml). Patch — только для
|
||
диагностического гостя, в мод не входит.
|
||
|
||
Приёмка конкретного Windows-сеанса остаётся внешней: нужны build identity и строки
|
||
`latest.log` вокруг остановки. Измерение fuel — не прирост FPS.
|
||
|
||
## Первая причина и фикс (тик, `1f28f83`)
|
||
|
||
Два максимальных состава меню (11 видов × 40, два принятых приказа), 64 ПВО, расстояние 300,
|
||
HD и grid, обслуживаемый плоский мир: tick 1835 исчерпал ровно 300M fuel и мод остановился.
|
||
Grid, запись и обновление дыма не были необходимы для этого spike: исследовательские абляции
|
||
не убирали остановку. В производственном исправлении эти функции не отключаются.
|
||
|
||
`Ground::forget` при каждом взрыве делал `retain` по всему кэшу известных высот. Тот же полный
|
||
обход выполнялся при обработке просроченных blast replies. Когда приходило много взрывов,
|
||
затраты росли как произведение количества взрывов и размера/ёмкости таблицы высот.
|
||
|
||
`1f28f83` удаляет непосредственно координаты прежнего квадрата: при радиусе 8 это 17×17 ключей,
|
||
а не просмотр всей таблицы. Вызов `world.touched` сохранён. Тест сравнивает весь результат со
|
||
старым predicate, включая отрицательные координаты и границы; правила урона и лимиты не менялись.
|
||
|
||
Контрольный диагностический гость с полными записью, дымом, HD и grid, без phase markers:
|
||
421 шаг, 3602 тика — PASS; max tick fuel 135273690/300M, command 92658271/1B, failures=0.
|
||
Финальные показатели: 0 в воздухе, 209 сбито, 3435 поражений, 15 столкновений.
|
||
На тике 1834 до прежней остановки вывод `/stats` до/после совпал побайтово,
|
||
включая счётчики мира. Это не утверждение о равенстве каждого скрытого поля симуляции.
|
||
|
||
Для воспроизведения огромного меню-состава использован временный hook команды mix,
|
||
вызывающий тот же `WaveConfig/order_raid`; сам hook **не входит в мод**.
|
||
Исходник — `10f24b5`, после фикса добавлен только новый `forget`. Данные, hook и точный
|
||
сценарий: [summary.json](measurements/max-raid-fuel/summary.json),
|
||
[trigger.patch](measurements/max-raid-fuel/trigger.patch),
|
||
[scenario.toml](measurements/max-raid-fuel/scenario.toml),
|
||
[до](measurements/max-raid-fuel/before.txt), [после](measurements/max-raid-fuel/after.txt).
|
||
Patch предназначен для отдельного диагностического worktree, а не для обычной игры.
|
||
Два checkpoint-лога намеренно заканчиваются несовпадением с `probe`, чтобы получить `/stats`;
|
||
это не реальные traps. Успешный полный прогон отдельно в `after.txt`.
|
||
|
||
Публичный native-тест `two_maximum_menu_raids_with_answered_flat_terrain` использует настоящий
|
||
максимальный `RaidOrder` без hook, terrain replies и запись. Он также прошёл.
|
||
|
||
## Найденные пробелы старых тестов
|
||
|
||
В первом `wasm_budget.toml` два приказа отправлялись без тика между ними. `order_raid` отклоняет
|
||
второй во время подготовки первого; ответ не проверялся. Кроме того, TestGame не отвечал на
|
||
render ground/sections/rays: пуски могли ждать неизвестную землю вместо полноценного полёта.
|
||
69 успешных шагов не подтверждали 160 одновременно летящих целей. Старый отчёт сохранён с поправкой.
|
||
|
||
## Что проверено теперь
|
||
|
||
- Native: точный состав двух меню-пресетов «Массированный удар», 61 комплекс, плоский мир с
|
||
ответами геометрии и встроенной записью. Оба приказа приняты, все 72 цели запущены,
|
||
перекрытие проверено по числу одновременно живых целей.
|
||
- Native: два приказа 40+40 «Орешников» с разделением, многими суббоеприпасами и окончанием записи.
|
||
- Actual WASM: 64 комплекса, два принятых приказа 40+40 «Орешников», плоскость y=64 с ответами
|
||
ground/sections/rays. В середине включаются shader hd и grid. 271 шаг прошёл;
|
||
на тике 2102 проверено **184 цели в воздухе**. Пик за весь прогон не измеряется.
|
||
- Кадры чередуются с тиками. 2052 вызова frame стенда не означают GPU-рендер:
|
||
у AirDef нет собственной реализации frame hook, основная работа идёт в tick и событиях.
|
||
|
||
Отчёт clean `2932364`, engine `ed7615d36`:
|
||
[flat-wasm-raid-budget.json](measurements/flat-wasm-raid-budget.json).
|
||
Memory 64 MiB и fuel init/tick/frame/event 2B/300M/10M/1B не увеличивались.
|
||
Отчёт фиксирует исходные лимиты, не измеренный расход fuel/peak memory.
|
||
|
||
```sh
|
||
python3 tools/airdef.py test-budgets
|
||
cargo test --release --manifest-path airdef-core/Cargo.toml --test scenarios overlapping_
|
||
```
|
||
|
||
Это проверка гостевого кода и контрактов с синтетическим host-миром. Она не воспроизводит
|
||
RTX-драйвер, реальный рельеф/чанки, звук и стоимость HDR, shadow/light/smoke на GPU пользователя.
|
||
Включение HD в fixture проверяет путь подготовки модовых данных, а не графическую производительность.
|
||
|
||
## Диагностика следующего воспроизведения
|
||
|
||
В `latest.log` после остановки теперь записываются mod ID, callback, заданный и оставшийся fuel,
|
||
полная цепочка ошибки. Сообщение OutOfFuel больше не объявляет тяжёлую работу бесконечным циклом.
|
||
Проверка настоящим WASM `/spin` подтверждает, что при fuel trap останавливается только нарушивший мод.
|
||
Это не доказывает, что пользовательский случай имеет ту же причину.
|
||
|
||
Нужны build identity и строки Windows `latest.log` вокруг остановки, состав двух налётов,
|
||
HD/обычный профиль, открытые grid/monitor/replay и ответ: закрылась ли игра целиком.
|
||
До этих данных не подменять диагноз увеличением fuel, отключением записи или обрезанием целей.
|
||
Измерение FPS вести отдельно от поиска trap: стабильный FPS не исключает лимит WASM, а прошедший
|
||
WASM-тест не исключает ошибку GPU или native host.
|