Skip to content

perf: dirty-tracking + пул + LPT для сохранения клановых сундуков (#3165)#3185

Merged
bylins merged 1 commit intomasterfrom
perf/clan-chest-save-dirty-pool
Apr 23, 2026
Merged

perf: dirty-tracking + пул + LPT для сохранения клановых сундуков (#3165)#3185
bylins merged 1 commit intomasterfrom
perf/clan-chest-save-dirty-pool

Conversation

@kvirund
Copy link
Copy Markdown
Collaborator

@kvirund kvirund commented Apr 22, 2026

Контекст

Issue #3165.

После PR #3181 сохранение клановых сундуков с ингредиентами на боевом сервере не укладывается в бюджет одного heartbeat-пульса (kPassesPerSec = 25, то есть 40 мс на пульс):

save_ingr_chests: 6 chests on 2 threads, wall 0.0491128710
save_ingr_chests: 6 chests on 2 threads, wall 0.0520457770
save_ingr_chests: 6 chests on 2 threads, wall 0.0554127890
save_ingr_chests: 6 chests on 2 threads, wall 0.0541249320
...
save_ingr_chests: 6 chests on 2 threads, wall 0.0660855630

Главный поток блокируется на WaitAll() примерно на 50 мс — пульс не успевает.

Что в PR

Подсистема сохранения клановых ингредиентных сундуков вынесена в отдельный компонент.

Новый компонент ClanSystem::IngrChestSaver

Файлы src/gameplay/clans/ingr_chest_saver.{h,cpp}. Заголовок минимальный:

class IngrChestSaver {
 public:
    IngrChestSaver();
    ~IngrChestSaver();

    void mark_dirty(Clan *clan);
    void run();

 private:
    class Impl;
    std::unique_ptr<Impl> m_impl;
};

Pimpl: пул потоков, std::unordered_set<Clan*>, логика сериализации и LPT-сортировка — всё в приватном Impl, спрятано за указателем. Изменения реализации не вызывают перекомпиляцию пользователей заголовка.

Экземпляр в GlobalObjects

Поле ClanSystem::IngrChestSaver ingr_chest_saver в GlobalObjectsStorage + геттер GlobalObjects::ingr_chest_saver(). В той же куче, что trigger_list, world_objects, zone_table, heartbeat и прочее глобальное состояние — явно и видно.

ClanSystem — это namespace со свободными функциями, положить пул туда как поле нельзя без переделки всего namespace в class. Переделка пространства имён в класс — отдельная большая задача (лучше в рамках #3180 про глобалы), здесь не трогаю.

Dirty-tracking

Clan::put_ingr_chest, Clan::take_ingr_chest, Clan::purge_ingr_chest вызывают GlobalObjects::ingr_chest_saver().mark_dirty(this). Других путей изменить содержимое сундука с ингредиентами в коде нет (проверено: только эти три функции управляют списком chest->contains, DG-скрипты прямого доступа не имеют).

В Clan нет ни поля, ни методов для отслеживания изменений — набор грязных сундуков живёт внутри IngrChestSaver::Impl.

LPT-сортировка

Время сериализации одного сундука линейно зависит от числа объектов в нём. Вес задачи — Clan::get_ingr_chest_objcount(), никаких syscall'ов не делаем. Сортировка задач по убыванию: самая большая стартует первой, wall time близок к нижней границе max(longest, total / threads).

Постоянный пул потоков

utils::ThreadPool живёт внутри IngrChestSaver::Impl и переиспользуется. Раньше пул создавался и уничтожался на каждый вызов save_ingr_chests, что давало лишние pthread_create + join каждые 10 минут.

ClanSystem::save_ingr_chests

Тонкая обёртка над GlobalObjects::ingr_chest_saver().run(). Вызовы из heartbeat и hcontrol save не менялись.

Ожидаемый эффект

Оценка на боевом сервере (6 активных сундуков, 2 ядра):

Ситуация wall time
Пустой цикл (10 минут без put/take/purge) <1 мс (только проход по Clan::ClanList + проверка в unordered_set)
1 изменившийся сундук ≈ времени его сериализации, для большинства — единицы мс
2 изменившихся сундука max(их размеров), обычно <30 мс
6 изменившихся сразу как раньше, ≈50 мс (этот случай редкий)

В часы низкой активности большинство вызовов run() будут пустыми.

Изменения

  • src/gameplay/clans/ingr_chest_saver.{h,cpp} — новый компонент.
  • src/engine/db/global_objects.{h,cpp} — поле + геттер.
  • src/gameplay/clans/house.cpp:
    • put_ingr_chest, take_ingr_chest, purge_ingr_chestmark_dirty(this).
    • save_ingr_chests ужата до одной строки вызова run().
    • Удалены save_one_ingr_chest и прежнее тело save_ingr_chests (они теперь в IngrChestSaver::Impl).
  • CMakeLists.txt — регистрация новых файлов.

Заголовок house.h не меняется, то есть для всех включающих его файлов пересборка не требуется.

Безопасность многопоточности

write_one_object (после PR #3181) читает ObjData без мутаций. Рабочие потоки пула только читают объекты, не трогают dirty-множество. mark_dirty и run вызываются из главного потока. Никаких атомиков и мьютексов не нужно.

Test plan

  • make tests && ./tests/tests — 372 теста зелёные.
  • make circle — собирается.
  • Прогон на боевом сервере, сверка wall time в логе: ожидаем большинство пустых циклов с wall <0.001 и отдельные рабочие циклы с wall <0.030 для 1–2 сундуков.

Компонент ClanSystem::IngrChestSaver владеет пулом потоков и множеством
изменённых сундуков. Экземпляр живёт в GlobalObjects рядом с остальным
глобальным состоянием.

Заголовок минимальный: mark_dirty(Clan*) и run(). Все детали (пул,
unordered_set, логика сохранения) скрыты в приватном Impl -- изменения
реализации не тянут за собой перекомпиляцию файлов, включающих заголовок.

Алгоритм run():
- собирает задачи по пересечению Clan::ClanList и dirty-множества;
- снимает флаг dirty с клана при постановке задачи; параллельный
  put_ingr_chest взведёт его заново, и следующий вызов run() подхватит
  новое состояние;
- сортирует задачи по убыванию get_ingr_chest_objcount() (LPT: время
  сериализации линейно зависит от числа объектов, крупнейшая задача
  стартует первой);
- ставит задачи в постоянный utils::ThreadPool, ждёт результатов;
- при ошибке записи возвращает dirty-флаг, чтобы следующий вызов
  повторил попытку.

Clan::put_ingr_chest, Clan::take_ingr_chest, Clan::purge_ingr_chest
помечают клан через GlobalObjects::ingr_chest_saver().mark_dirty(this).

ClanSystem::save_ingr_chests -- тонкая обёртка над run(), вызовы из
heartbeat и hcontrol save не меняются.

Issue: #3165
@kvirund kvirund force-pushed the perf/clan-chest-save-dirty-pool branch from a25e889 to c2730bd Compare April 23, 2026 00:29
@kvirund kvirund changed the title perf: dirty-tracking + постоянный пул + LPT для save_ingr_chests (#3165) perf: dirty-tracking + пул + LPT для сохранения клановых сундуков (#3165) Apr 23, 2026
@bylins bylins merged commit 2d2fce3 into master Apr 23, 2026
20 checks passed
@bylins bylins deleted the perf/clan-chest-save-dirty-pool branch April 23, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants