You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Первый конкретный шаг под зонтиком #3180 (уменьшение глобального состояния): оптимизация делается без нового глобала — активный набор становится индексом внутри уже существующего реестра Characters. Заодно служит DI-примером, которого просит #3180.
Проблема
Шаг хартбита «Point updating» (heartbeat.cpp:482, вызывает только point_update() из game_limits.cpp) периодически занимает ~111 мс. По коду внутри цикла — дешёвая арифметика (in_used_zone, get_real_max_hit/move, average_day_temp, hit_gain/move_gain), никаких квадратов и тяжёлых перерасчётов. Значит спайк даёт не работа, а сам проход.
point_update() идёт по глобальному character_list (std::list ВСЕХ загруженных мобов мира — десятки тысяч) и почти всех пропускает фильтром:
in_used_zone() (char_data.cpp:109) для NPC возвращает zone_table[world[in_room]->zone_rn].used, для PC — всегда false. Зона помечается used=true при входе игрока (handler.cpp:266, char_movement.cpp:797, db.cpp:after_reset_zone), снимается при деактивации (db.cpp:2094/2898).
Итог: игроки занимают единицы зон из сотен, поэтому подавляющее большинство мобов каждый MUD-час просматриваются только чтобы быть пропущенными. O(всех мобов мира) ради O(активных).
Тот же паттерн в mobile_activity() (mobact.cpp:903) — полный проход character_list с фильтром purged() || !IsNpc() || !in_used_zone().
Профайлер частей (PR #3412) подтвердит прямо в логе: при просмотрено ≫ обработано и всего ≫ суммы частей время уходит на скан/пропуск, а не на работу.
Идея
Не сканировать весь мир в point_update, а обрабатывать только активный набор: PC + мобы, реально проявившие активность. mobile_activity уже определяет активного моба — там его и запоминать; point_update затем идёт по запомненному набору.
Наивная реализация — завести глобальный std::unordered_set<CharData*> g_active_chars. Это новый singleton-глобал, прямо против #3180. Поэтому владельца выбираем осознанно:
character_list — это объект класса Characters (world_characters.h), который УЖЕ:
владеет списком персонажей (std::list<CharData::shared_ptr>);
ведёт индекс vnum → мобы (m_vnum_to_characters_set);
управляет жизненным циклом — push_front() при спауне и remove() при extract (world_characters.cpp:130), где уже чистятся вспомогательные индексы.
Значит «активный набор» — это ещё один индекс внутри Characters, а не новый глобал. Когда Characters со временем станет инжектируемой в контекст мира (цель #3180), индекс уедет вместе с ней. И висячие указатели решаются даром: удаление активного цепляется к существующему Characters::remove.
Способ реализации (по шагам)
1. Новый индекс в Characters
В классе Characters (world_characters.{h,cpp}):
// внутри Characters
std::unordered_set<CharData *> m_active; // активные с прошлого point_updatepublic:voidmark_active(CharData *ch) { m_active.insert(ch); } // O(1)constauto &active() const { return m_active; }
voidclear_active() { m_active.clear(); }
В существующем Characters::remove(CharData *ch) добавить m_active.erase(ch); рядом с чисткой прочих индексов — висячих указателей не будет by design.
2. Производитель — mobile_activity (врезка строго O(1)!)
mobile_activityсам перегружен и тоже сканирует character_list, поэтому туда можно добавить только одну дешёвую вставку — без новых перерасчётов/проходов. Точка — где моб признан активным в этот пульс, сразу после ++processed_mobs (mobact.cpp, уже после in_used_zone и совпадения пульса активности):
++processed_mobs;
character_list.mark_active(ch.get()); // единственная добавленная стоимость, O(1)
Набор наполняется из уже идущего прохода — отдельного скана ради набора не появляется.
3. Игроки — по их активности, не через affect-update
PC мобакт не обрабатывает (!IsNpc() пропускается), вносим отдельно. У игрока активность трекается: idle-таймер сбрасывается на вводе команды (interpreter.cpp:1575), есть check_idling() (game_limits.cpp:1064). Игроков десятки, поэтому надёжнее всего проходить их в point_update отдельно — из descriptor_list (или лёгкого списка PC в игре). По ним идём всегда: регенерация/голод/жажда не должны зависеть от активности, иначе AFK-игрок перестанет голодать. Вариант «гейтить PC по активности» — опция с этой оговоркой.
4. Потребитель — point_update
for (auto *m : character_list.active()) { // мобы — только активныеif (m->purged()) continue; // страховка/* тот же per-char код */
}
for (/* PC из descriptor_list / списка игроков */) { // PC — всегда все/* тот же per-char код */
}
character_list.clear_active(); // наберётся заново мобактом
clear_active() делает набор «активные с прошлого point_update»: между двумя вызовами мобакт срабатывает многократно и наполнит его заново.
Что НЕ входит в scope
Убираем избыточный скан в point_update (целевые 111 мс). Полный проход character_listв самом mobile_activity остаётся — отдельная, более крупная задача (мобакт перегружен, трогать осторожно). Здесь в мобакт добавляется ровно одна O(1)-вставка.
Поведенческое следствие (обсудить)
point_update начнёт регенерировать только «активных» мобов, а не всех в used-зоне. Для простаивающих мобов безвредно (стоят с полным HP, на ресете восстановятся), но это сознательная смена семантики — фиксируем явно.
Метрика mob.activity.duration (уже есть) — врезка её не ухудшила.
Паритет поведения: HP/move/conditions активных персонажей до/после идентичны; AFK-игрок продолжает голодать.
Стресс: массовая смерть мобов между мобактом и point_update — нет обращений к освобождённым (страхует remove+purged()).
Связанная находка
Рядом, в «Remember some spells» (game_limits.cpp:1657), условие GET_SPELL_MEM(i, sp) > GET_SPELL_MEM(i, sp) сравнивает значение само с собой → всегда false: мобы не запоминают спеллы, а while вхолостую крутится ~108 раз на каждом говорящем мобе. Отдельный баг — вынести в свою issue.
Связано с #3180 (уменьшение глобального состояния) — реализуется без новых глобалов, как пример DI-владельца.
Проблема
Шаг хартбита «Point updating» (
heartbeat.cpp:482, вызывает толькоpoint_update()изgame_limits.cpp) периодически занимает ~111 мс. По коду внутри цикла — дешёвая арифметика (in_used_zone,get_real_max_hit/move,average_day_temp,hit_gain/move_gain), никаких квадратов и тяжёлых перерасчётов. Значит спайк даёт не работа, а сам проход.point_update()идёт по глобальномуcharacter_list(std::listВСЕХ загруженных мобов мира — десятки тысяч) и почти всех пропускает фильтром:in_used_zone()(char_data.cpp:109) для NPC возвращаетzone_table[world[in_room]->zone_rn].used, для PC — всегдаfalse. Зона помечаетсяused=trueпри входе игрока (handler.cpp:266,char_movement.cpp:797,db.cpp:after_reset_zone), снимается при деактивации (db.cpp:2094/2898).Итог: игроки занимают единицы зон из сотен, поэтому подавляющее большинство мобов каждый MUD-час просматриваются только чтобы быть пропущенными. O(всех мобов мира) ради O(активных).
Тот же паттерн в
mobile_activity()(mobact.cpp:903) — полный проходcharacter_listс фильтромpurged() || !IsNpc() || !in_used_zone().Профайлер частей (PR #3412) подтвердит прямо в логе: при
просмотрено ≫ обработаноивсего ≫ суммы частейвремя уходит на скан/пропуск, а не на работу.Идея
Не сканировать весь мир в
point_update, а обрабатывать только активный набор: PC + мобы, реально проявившие активность.mobile_activityуже определяет активного моба — там его и запоминать;point_updateзатем идёт по запомненному набору.Согласование с #3180 (важно)
Наивная реализация — завести глобальный
std::unordered_set<CharData*> g_active_chars. Это новый singleton-глобал, прямо против #3180. Поэтому владельца выбираем осознанно:character_list— это объект классаCharacters(world_characters.h), который УЖЕ:std::list<CharData::shared_ptr>);vnum → мобы(m_vnum_to_characters_set);push_front()при спауне иremove()при extract (world_characters.cpp:130), где уже чистятся вспомогательные индексы.Значит «активный набор» — это ещё один индекс внутри
Characters, а не новый глобал. КогдаCharactersсо временем станет инжектируемой в контекст мира (цель #3180), индекс уедет вместе с ней. И висячие указатели решаются даром: удаление активного цепляется к существующемуCharacters::remove.Способ реализации (по шагам)
1. Новый индекс в
CharactersВ классе
Characters(world_characters.{h,cpp}):В существующем
Characters::remove(CharData *ch)добавитьm_active.erase(ch);рядом с чисткой прочих индексов — висячих указателей не будет by design.2. Производитель —
mobile_activity(врезка строго O(1)!)mobile_activityсам перегружен и тоже сканируетcharacter_list, поэтому туда можно добавить только одну дешёвую вставку — без новых перерасчётов/проходов. Точка — где моб признан активным в этот пульс, сразу после++processed_mobs(mobact.cpp, уже послеin_used_zoneи совпадения пульса активности):++processed_mobs; character_list.mark_active(ch.get()); // единственная добавленная стоимость, O(1)Набор наполняется из уже идущего прохода — отдельного скана ради набора не появляется.
3. Игроки — по их активности, не через affect-update
PC мобакт не обрабатывает (
!IsNpc()пропускается), вносим отдельно. У игрока активность трекается: idle-таймер сбрасывается на вводе команды (interpreter.cpp:1575), естьcheck_idling()(game_limits.cpp:1064). Игроков десятки, поэтому надёжнее всего проходить их вpoint_updateотдельно — изdescriptor_list(или лёгкого списка PC в игре). По ним идём всегда: регенерация/голод/жажда не должны зависеть от активности, иначе AFK-игрок перестанет голодать. Вариант «гейтить PC по активности» — опция с этой оговоркой.4. Потребитель —
point_updateclear_active()делает набор «активные с прошлого point_update»: между двумя вызовами мобакт срабатывает многократно и наполнит его заново.Что НЕ входит в scope
Убираем избыточный скан в
point_update(целевые 111 мс). Полный проходcharacter_listв самомmobile_activityостаётся — отдельная, более крупная задача (мобакт перегружен, трогать осторожно). Здесь в мобакт добавляется ровно одна O(1)-вставка.Поведенческое следствие (обсудить)
point_updateначнёт регенерировать только «активных» мобов, а не всех в used-зоне. Для простаивающих мобов безвредно (стоят с полным HP, на ресете восстановятся), но это сознательная смена семантики — фиксируем явно.Валидация
просмотренорезко падает,всегосходится с суммой частей.mob.activity.duration(уже есть) — врезка её не ухудшила.point_update— нет обращений к освобождённым (страхуетremove+purged()).Связанная находка
Рядом, в «Remember some spells» (
game_limits.cpp:1657), условиеGET_SPELL_MEM(i, sp) > GET_SPELL_MEM(i, sp)сравнивает значение само с собой → всегдаfalse: мобы не запоминают спеллы, аwhileвхолостую крутится ~108 раз на каждом говорящем мобе. Отдельный баг — вынести в свою issue.Связано с #3180 (уменьшение глобального состояния) — реализуется без новых глобалов, как пример DI-владельца.