From 27c12e7904eaf2df93f36ff4a5f5679690b7b848 Mon Sep 17 00:00:00 2001 From: stribog Date: Sun, 14 Jun 2026 20:07:10 +0200 Subject: [PATCH] =?UTF-8?q?perf(save):=20=D0=BF=D1=80=D0=BE=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=D1=82=D1=8C=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0=20=D0=BE=D0=B1=D1=8A?= =?UTF-8?q?=D0=B5=D0=BA=D1=82=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=D0=BC=20=D0=B8?= =?UTF-8?q?=D0=BD=D0=B2=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crash_frac_save_all (размазанный crash-сейв) каждый цикл безусловно переписывал файл объектов всех онлайн-игроков по очереди через save_char_objects, даже если инвентарь не менялся. Синхронная дисковая запись неизменившихся инвентарей -- основной источник спайков "Crash frac save" в хартбите (до 100+ мс). Теперь для периодического crash-сейва (RENT_CRASH) считаем хеш содержимого файла объектов и пропускаем дисковую запись + пересчёт CRC, если оно не изменилось с прошлого сейва. Файл объектов -- это чистый payload предметов (рент-таймеры пишутся отдельно в .time), поэтому одинаковый payload даёт байт-в-байт идентичный файл, и запись не нужна -- данные не теряются. Остальные типы сейва (рент/ребут/idle-rent) пишут всегда. Хеш в кэше обновляется при каждой реальной записи; на пути "нет предметов" сбрасывается. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/engine/db/obj_save.cpp | 58 ++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/engine/db/obj_save.cpp b/src/engine/db/obj_save.cpp index e94844d95..f1f15eaca 100644 --- a/src/engine/db/obj_save.cpp +++ b/src/engine/db/obj_save.cpp @@ -32,6 +32,13 @@ #include "engine/observability/metrics.h" #include "utils/tracing/trace_manager.h" +#include + +// #3440: кэш хешей содержимого файла объектов по uid игрока. Для периодического +// crash-сейва (RENT_CRASH) позволяет пропустить дисковую запись, если набор +// предметов не изменился. Заполняется только при реальной записи файла. +static std::unordered_map g_obj_crash_save_hash; + const int LOC_INVENTORY = 0; //const int MAX_BAG_ROWS = 5; //const int ITEM_DESTROYED = 100; @@ -1877,6 +1884,7 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) { ObjSaveSync::check(ch->get_uid(), ObjSaveSync::CHAR_SAVE); if (!num) { + g_obj_crash_save_hash.erase(ch->get_uid()); Crash_delete_files(iplayer); return false; } @@ -1970,13 +1978,6 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) { double crc_sec = 0.0; utils::CExecutionTimer obj_io_timer; if (get_filename(GET_NAME(ch), fname, kTextCrashFile)) { - std::ofstream file(fname, std::ios::binary); - if (!file.is_open()) { - snprintf(buf, kMaxStringLength, "[SYSERR] Store objects file '%s'- MAY BE LOCKED.", fname); - mudlog(buf, BRF, kLvlImmortal, SYSLOG, true); - Crash_delete_files(iplayer); - return false; - } write_buffer << "\n$\n$\n"; // Пишем в бинарном режиме и из этого же буфера считаем CRC -- без // перечитывания только что записанного файла. Бинарный режим @@ -1984,19 +1985,40 @@ int save_char_objects(CharData *ch, int savetype, int rentcost) { // (в текстовом режиме Windows транслировал бы \n -> \r\n и CRC из // буфера разошёлся бы с CRC файла). const std::string obj_content = write_buffer.str(); - file.write(obj_content.data(), static_cast(obj_content.size())); - file.close(); + // #3440: для периодического crash-сейва (RENT_CRASH) пропускаем запись + // файла объектов, если набор предметов не изменился с прошлого сейва. + // Файл объектов -- чистый payload предметов (рент-таймеры пишутся + // отдельно в .time), поэтому одинаковый payload => идентичный файл => + // дисковая запись и пересчёт CRC не нужны. Это основной источник спайков + // "Crash frac save" (синхронный write неизменившихся инвентарей). + const std::size_t content_hash = std::hash{}(obj_content); + const auto cached = g_obj_crash_save_hash.find(ch->get_uid()); + const bool unchanged = (cached != g_obj_crash_save_hash.end() && cached->second == content_hash); + if (savetype == RENT_CRASH && unchanged) { + // предметы не менялись -- файл уже актуален, диск не трогаем + } else { + std::ofstream file(fname, std::ios::binary); + if (!file.is_open()) { + snprintf(buf, kMaxStringLength, "[SYSERR] Store objects file '%s'- MAY BE LOCKED.", fname); + mudlog(buf, BRF, kLvlImmortal, SYSLOG, true); + Crash_delete_files(iplayer); + return false; + } + file.write(obj_content.data(), static_cast(obj_content.size())); + file.close(); #ifndef _WIN32 - if (chmod(fname, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) < 0) { - std::stringstream ss; - ss << "Error chmod file: " << fname << " (" << __FILE__ << " "<< __func__ << " "<< __LINE__ << ")"; - mudlog(ss.str(), BRF, kLvlGod, SYSLOG, true); - } + if (chmod(fname, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) < 0) { + std::stringstream ss; + ss << "Error chmod file: " << fname << " (" << __FILE__ << " "<< __func__ << " "<< __LINE__ << ")"; + mudlog(ss.str(), BRF, kLvlGod, SYSLOG, true); + } #endif - utils::CExecutionTimer crc_timer; - FileCRC::update_from_content(ch->get_uid(), FileCRC::kTextObjs, - obj_content.data(), obj_content.size()); - crc_sec = crc_timer.delta().count(); + utils::CExecutionTimer crc_timer; + FileCRC::update_from_content(ch->get_uid(), FileCRC::kTextObjs, + obj_content.data(), obj_content.size()); + crc_sec = crc_timer.delta().count(); + g_obj_crash_save_hash[ch->get_uid()] = content_hash; + } } else { Crash_delete_files(iplayer); return false;