Files
airdef/docs/RAID-STABILITY.md
T
chexit d45629ae29
build / mod (push) Failing after 22s
Fix the UI-event fuel trap on a second large raid order
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>
2026-11-06 10:29:36 +03:00

141 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Два крупных налёта: проверка стабильности
Пользовательский отчёт: 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.