с оповещенеим игроков-владельцев структур и переводом структуры в статику
(Нужен плагин Profiler)
Конфигурация:
- Enable physics guard - вкл/выкл проверку нагрузки на физику
- Physics checks before punish - Количество проваленных проверок до оповещения или конвертации в статику
- Physics ms to alert - Если стуктура обсчитывается дольше указанного времени то игрок получает оповещение
- Physics ms to punish - Если стуктура обсчитывается дольше указанного времени то структура становится статичной через некоторое количество проверок(Physics checks before punish)
- Physics ms to punish immediately - Если стуктура обсчитывается дольше указанного времени то структура становится статичной немедленно
Структуры рядом с которыми нет игроков перестают обсчитываться на сервере
Если на структуре есть производственные блоки то раз в Min WakeUp Interval In Sec секунд структура просыпается на несколько секунд,
и заводы/сборщики компенсируют время заморозки переработывая за следующий после разморозки тик большую пачку руды или собирая большую пачку помпонентов

- Freeze Physics - Если параметр включен, то динамические структуры которые не закреплены об что-то статическое, в некотором смысле становятся статикой
- Сварка/распил динамики в сз, или особенно на границе сз может вызывать ощутимые микрофризы, такие динамические структуры в сз автоматически переводятся в статику если вызывают фризы больше Safe zone Physics Threshold миллисекунд
Сварка целиком выполняется в игровом потоке: мутации блоков, конвейеры и Havok небезопасно трогать из воркеров. Параметр Async weld больше ни на что не влияет и оставлен только для совместимости конфига. Вместо переноса в потоки работа сварщиков по проекции ограничена бюджетом на кадр:
- Projection builds per frame (по умолчанию
1, минимум1) - сколько блоков проекции может материализовать весь сервер за один кадр симуляции. Лимит общий для всех сварщиков: право строить передаётся между ними по кругу, поэтому несколько сварщиков в одном кадре не складывают своиProjector.Buildв один длинный кадр. - Projection checks per activation (по умолчанию
24, минимум1) - сколько новых проверокCanBuildделает одна активация сварщика. Вместо ванильного полного скана голограммы на каждой активации проверяется небольшой кусок, курсор продолжает с того же места в следующий раз, а соседи только что построенного блока проверяются первыми.
Оба параметра меняются на лету. Множитель радиуса сварщика (плагин SentisGameplayImprovements, toolradius welder)
не имеет верхнего предела, минимум 0.1.
Почему так: профиль dotTrace на 3 сварщиках показал, что время уходит не на сам Build
(в среднем ~0.5 мс), а на ванильный поиск строящихся блоков: FindProjectedBlocks + CanBuild +
TestPlacementAreaCube суммарно ~25 с CPU за 90 с сварки, и весь скан выполнялся в одном кадре.
Результат (стенд welder_perf из SentisTests: 3 сварщика, проекция 10×10×10 из тяжёлой брони, 1000 блоков,
время работы кадра симуляции без сна до 60 Гц):
| avg | p99 | p99.9 | max | кадров > 16.7 мс | |
|---|---|---|---|---|---|
| прогретый сервер | ~1.1 мс | ~4 мс | 6-8 мс | 10-13 мс | 0 из 7470 |
Самый долгий Projector.Build на прогретом сервере 2-6 мс, самые тяжёлые кадры сварки 8-10 мс (совпадают с GC).
Единственный оставшийся пик от сварки - первый Build после рестарта сервера (24-36 мс, один раз): холодный путь (JIT или первая загрузка модели блока),
причина не подтверждена. Подробности замеров и методика: docs/welder-perf.md в репозитории SentisTests.
Все изменения выполняются в игровом потоке. Общий объём переработки не уменьшается, а сама переработка идёт быстрее ванили.
- Разнесение таймеров. Ванильный таймер производственного блока срабатывает раз в 60 кадров. Блоки, созданные одновременно (загрузка мира, спавн, вставка), срабатывают в одном и том же кадре, и на большой базе это давало кадры 50-77 мс. Каждому таймеру назначается случайная фаза, и работа заводов равномерно распределяется по кадрам. Действует при создании блока (загрузка мира, спавн, вставка); касается и сборщиков. Параметров нет.
- Добор руды до своей доли вместо ванильного 30%/60%. Ваниль тянет по 2000 кг, пока вход не заполнен на 60%, поэтому первые заводы забирают руду, а остальные простаивают. Доля завода - вся руда сети (контейнеры плюс входы всех включённых работающих заводов), делённая на число таких заводов. Завод добирает до доли; доборы меньше 500 кг пропускаются, почти пустой завод (<100 кг) всегда получает руду.
- Бюджет доборов: не больше 30 доборов руды за кадр на весь сервер, первыми обслуживаются самые пустые заводы.
- Пустая сеть: если в сети нет руды, завод не сканирует её каждую секунду, а ждёт 2, 4, 8... до 10 с (с разбросом по заводам). Как только руда появляется в любом отдающем инвентаре сети (контейнер, коннектор, бур) или меняется граф конвейеров, ожидание сразу отменяется.
В .NET Framework нет GC без пауз: каждая сборка gen0/gen1 останавливает игровой поток, и чаще всего это случается в тяжёлом кадре. Плагин запоминает, насколько обычно вырастает куча до естественной сборки gen0. Когда израсходовано 60% этого бюджета, он сам вызывает сборку gen0 в конце лёгкого кадра (работа кадра не больше 4 мс). Сборок становится больше, но они короче (~2 мс) и приходятся на кадры с запасом. Действует на весь сервер, параметров нет.
Результат на стенде refinery_perf из SentisTests (64 грида, 6464 больших завода, только железная руда, 45 тыс. т),
время работы кадра симуляции:
| время переработки | p95 | p99 | max | кадров > 16.7 мс | |
|---|---|---|---|---|---|
| ваниль | 112-115 с | 33-38 мс | 41-43 мс | 68-77 мс | 960-1065 |
| все оптимизации | 99 с | 6.7 мс | 9.8 мс | 122 мс* | 4 |
* Одиночные паузы GC вне заводов. Такие кадры 120-190 мс были и без плагина и, по всей видимости, связаны с автосейвом большого мира.
Подробности, все промежуточные замеры и найденные ловушки: docs/refinery-perf.md в репозитории SentisTests.
Сохранение (автосейв, !save) сначала собирает снимок мира в игровом потоке, и только потом пишет его на диск в фоне.
Почти всё время снимка уходит на GetObjectBuilder гридов, а игроки видят это как один долгий кадр.
- Параллельный снимок гридов.
BeforeSaveи object builder остальных сущностей по-прежнему создаются в игровом потоке в ванильном порядке. Object builder гридов собираются на всех ядрах, пока игровой поток ждёт завершения, поэтому мир во время снимка не меняется и сейв остаётся атомарным. Если параллельная сборка бросит исключение, снимок пересобирается последовательно, а параллельный режим отключается до рестарта сервера. - Сериализация компонентов блоков без лишних выделений. Ванильный
MyComponentContainer.Serializeсоздаёт временный список на каждый тип компонента каждого блока и пустой контейнер, даже когда сохранять нечего (например, у конвейеров). Замена даёт тот же XML без этих объектов. Действует везде, где сериализуются блоки (сейв, чертежи, репликация).
Параметров нет. Результат на стенде save_perf из SentisTests (64 грида, 32 тыс. блоков):
| ваниль | сейчас | |
|---|---|---|
| кадр со снимком | 135-151 мс | 49-70 мс |
| выделения на один грид (501 блок) | 604 КБ | 416 КБ |
Остаток кадра - сборки мусора (4 gen0 и 1 gen1), которые вызывает создание object builder. Подробности: docs/save-perf.md в SentisTests.
- Замороженные гриды собираются заранее. Если фризер включён и есть замороженные гриды, запрос на сохранение
(автосейв,
/save,!save) откладывается на несколько кадров. В конце каждого кадра собираются object builder замороженных гридов, не больше 3 мс работы на кадр. Потом запускается обычное сохранение, и в кадре снимка собираются только незамороженные гриды. Замороженный грид не обновляется, поэтому его заранее собранный снимок совпадает со снимком в момент сохранения (на стенде проверено сравнением XML). Заранее собираются только гриды, замороженные не меньше 5 секунд; уровни присутствия (tiers), которые меняются и у замороженных гридов, берутся с грида в момент снимка. Заранее собранный снимок отбрасывается, если грид разморозился (в том числе при пробуждении), закрылся, у него добавили или удалили блок или изменился инвентарь. Сохранение при выгрузке мира не откладывается.
Результат на стенде frozen_save_perf (64 грида, 56 заморожено, 8 держатся маяком из AntifreezeBlocksSubtypes):
кадр снимка больше не выделяется, максимальный кадр за время сохранения - 17 мс (без фризера 49-70 мс),
подготовка - 20-24 кадра по 3-5 мс, сохранение целиком занимает на ~0.3 с дольше.
Изменения замороженного грида другими путями (смена владельца, переименование, свойства блоков через терминал или моды)
за эти доли секунды не отслеживаются.
- Параметр Fix Voxel Freeze Enabled включает механизм при котором сжатие и отправка сущностей клиентам игры происходит в отдельном потоке. В результате этого основной поток симуляции не фризит при сжатии данных для больших обёмов вокселей или больших структур
Изменения не настраиваются и включены всегда. Замеры - сценарий replication_perf из SentisTests
(64 статичных завода, 10 летающих гридов, 64 клиента с эмуляцией сети), подробности в docs/replication-perf.md того плагина.
- Сравнение адресов без боксинга (
EndpointComparers) -EqualityComparer<T>.DefaultдляEndpointиEndpointIdуходил в сравнение черезobject, то есть каждое обращение к словарям репликации выделяло память. - Компактные данные синхронизации свойств (
PropertySyncClientData) - вместо массива на 256 пакетов для каждого клиента хранится таблица (ключ, биты), растущая с 4 записей. - Грязные группы состояния по индексу клиентов (
StateGroupClients) - ваниль искала каждую грязную группу у каждого клиента, и при 64 клиентах один кадр уходил на это до 70 мс. Теперь у группы есть список клиентов, которым она реплицируется, а дубликаты в очереди отбрасываются. - Кэш билдеров гридов для стрима (
GridStreamBuilders) - собранныйMyObjectBuilder_CubeGridпереживает кадры, в нём обновляется только изменившееся: позиция и скорости каждый раз, блоки - по событиям, а блоки, меняющиеся сами по себе (батареи, баки, поршни, двери, таймеры), пересобираются при выдаче. Варианты со скриптами и без нужныFuckScriptThief, который отдаёт скрипт программного блока только тем, у кого есть права. Массовое подключение 64 клиентов: 140 с → 81 с. - Бюджет на стрим гридов (
StreamingSerializeBudget) - сборка новых билдеров ограничена 3 мс на кадр, вся работа по стриму - 6 мс; отложенное возвращается в очередь клиента на следующий кадр. - Инвентари только тем, кто смотрит (
IdleInventorySync) - игра считает приоритет группы инвентаря (1 тому, у кого блок открыт, 0 остальным), но при рассылке его не использует, поэтому инвентари всех ящиков и заводов в зоне видимости идут каждому клиенту каждые несколько кадров. Клиенту, у которого ничего не открыто, они планируются со случайной задержкой в пределах ~600 кадров, а в момент открытия терминала все его инвентари тем же кадром переставляются в начало очереди. Максимальный кадр в тесте 106 мс → 61 мс, кадров дольше 33 мс: 161 → 68.
