# Два крупных налёта: проверка стабильности Пользовательский отчёт: 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.