From 0e6cf101a4c99603de8c63ae1dfe987f9d4f2cd9 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 13:04:38 +0200 Subject: [PATCH 1/8] Refactor logger to run asynchronously to prevent event loop blocking Previously, the logger opened, wrote to, and closed `Launcher.log` synchronously on every single log call. Under heavy network traffic, this file I/O was stalling the main networking threads, contributing to dropped connections when too many events were received at once. This commit moves the logger to a dedicated background thread. - Added a `std::queue` and a `std::condition_variable` to handle log dispatching. - Log calls (`info`, `debug`, etc.) now instantly push their messages to the queue and return immediately, freeing up the network threads. - Added a lightweight `Utils::ToString` helper in `Utils.h` (mirroring the existing `ToWString`) to convert wide strings into standard UTF-8 strings before pushing them into the new logger queue. This significantly improves launcher performance under heavy load without changing the visual log output. --- include/Utils.h | 14 ++++++++++++ src/Logger.cpp | 59 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/include/Utils.h b/include/Utils.h index 36f35704..a91c3fd1 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -189,6 +189,20 @@ namespace Utils { MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed); + return result; + } + inline std::string ToString(const std::wstring& s) { + if (s.empty()) return std::string(); + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) { + return ""; + } + + std::string result(size_needed, 0); + + WideCharToMultiByte(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed, nullptr, nullptr); + return result; } #else diff --git a/src/Logger.cpp b/src/Logger.cpp index 771d07d4..6dffaa42 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -13,6 +13,38 @@ #include #include #include "Options.h" +#include +#include +#include + +std::mutex logMutex; +std::condition_variable logCV; +std::queue logQueue; +bool logThreadRunning = false; +std::thread logThread; + +void logThreadFunc() { + std::ofstream LFS; + LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::out); + if (!LFS.is_open()) return; + + while (logThreadRunning || !logQueue.empty()) { + std::unique_lock lock(logMutex); + logCV.wait(lock, [] { return !logQueue.empty() || !logThreadRunning; }); + + while (!logQueue.empty()) { + std::string line = logQueue.front(); + logQueue.pop(); + lock.unlock(); + + LFS << line; + LFS.flush(); + + lock.lock(); + } + } + LFS.close(); +} std::string getDate() { time_t tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); @@ -36,24 +68,23 @@ std::string getDate() { return date.str(); } void InitLog() { - std::ofstream LFS; - LFS.open(GetEP() + beammp_wide("Launcher.log")); - if (!LFS.is_open()) { - error("logger file init failed!"); - } else - LFS.close(); + logThreadRunning = true; + logThread = std::thread(logThreadFunc); + logThread.detach(); } void addToLog(const std::string& Line) { - std::ofstream LFS; - LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app); - LFS << Line.c_str(); - LFS.close(); + { + std::lock_guard lock(logMutex); + logQueue.push(Line); + } + logCV.notify_one(); } void addToLog(const std::wstring& Line) { - std::wofstream LFS; - LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::app); - LFS << Line.c_str(); - LFS.close(); +#ifdef _WIN32 + addToLog(Utils::ToString(Line)); +#else + addToLog(std::string(Line.begin(), Line.end())); +#endif } void info(const std::string& toPrint) { std::string Print = getDate() + "[INFO] " + toPrint + "\n"; From 83a24a0cb7f5135a2db0ac6919088d403c408119 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 13:10:55 +0200 Subject: [PATCH 2/8] Add debug logging for Lua custom events ('E' packets) Added targeted debug logging in `GlobalHandler.cpp` for both incoming and outgoing custom events (`C == 'E'`). Previously, it was difficult to tell when custom events were causing network saturation. This change logs the total size and name of all custom events passing through the launcher. To prevent console spam, the logic intelligently strips out the JSON payload body (truncating at the first `{` or `[`) so only the clean event name and byte size are printed. Note: Because the logger was moved to an asynchronous background queue in the previous commit, adding this detailed logging is completely safe. It will not block the main network loop or cause connection drops, even if a server mod aggressively spams thousands of events per second. This should significantly help server owners and mod developers quickly identify network bottlenecks. --- src/Network/GlobalHandler.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Network/GlobalHandler.cpp b/src/Network/GlobalHandler.cpp index 94086dae..32ce65bc 100644 --- a/src/Network/GlobalHandler.cpp +++ b/src/Network/GlobalHandler.cpp @@ -61,6 +61,15 @@ void GameSend(std::string_view Data) { auto Result = send(CSocket, ToSend.data(), ToSend.size(), 0); if (Result < 0) { error("(Game) send failed with error: " + std::to_string(WSAGetLastError())); + } else { + char C = Data.empty() ? 0 : Data.at(0); + if (C == 'E') { + std::string header = std::string(Data.substr(0, std::min(Data.length(), 120))); + auto payloadStart = header.find_first_of("{["); + if (payloadStart != std::string::npos) header = header.substr(0, payloadStart); + if (!header.empty() && header.back() == ':') header.pop_back(); + debug("(Server->Launcher) Custom Event: Size: " + std::to_string(Data.length()) + " bytes, Event: " + header); + } } } @@ -89,8 +98,14 @@ void ServerSend(std::string Data, bool Rel) { } else UDPSend(Data); - if (DLen > 1000) { - debug("(Launcher->Server) Bytes sent: " + std::to_string(Data.length()) + " : " + if (C == 'E') { + std::string header = std::string(Data.substr(0, std::min(Data.length(), 120))); + auto payloadStart = header.find_first_of("{["); + if (payloadStart != std::string::npos) header = header.substr(0, payloadStart); + if (!header.empty() && header.back() == ':') header.pop_back(); + debug("(Launcher->Server) Custom Event: Size: " + std::to_string(Data.length()) + " bytes, Event: " + header); + } else if (DLen > 1000) { + debug("(Launcher->Server) Large packet sent: " + std::to_string(Data.length()) + " : " + Data.substr(0, 10) + Data.substr(Data.length() - 10)); } else if (C == 'Z') { From 8376576c13eae5058b1520932ccb6812aae04414 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 18:00:19 +0200 Subject: [PATCH 3/8] Add error log back when Launcher.log fails to open --- src/Logger.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Logger.cpp b/src/Logger.cpp index 6dffaa42..8d92d3c2 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "Options.h" #include #include @@ -26,7 +28,10 @@ std::thread logThread; void logThreadFunc() { std::ofstream LFS; LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::out); - if (!LFS.is_open()) return; + if (!LFS.is_open()) { + std::cerr << "Failed to open Launcher.log: " << std::strerror(errno) << std::endl; + return; + } while (logThreadRunning || !logQueue.empty()) { std::unique_lock lock(logMutex); From 4aeab900598c2c3de0eab4751029562bd801f3e9 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 18:05:59 +0200 Subject: [PATCH 4/8] Revert "Add debug logging for Lua custom events ('E' packets)" This reverts commit 83a24a0cb7f5135a2db0ac6919088d403c408119. --- src/Network/GlobalHandler.cpp | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/Network/GlobalHandler.cpp b/src/Network/GlobalHandler.cpp index 32ce65bc..94086dae 100644 --- a/src/Network/GlobalHandler.cpp +++ b/src/Network/GlobalHandler.cpp @@ -61,15 +61,6 @@ void GameSend(std::string_view Data) { auto Result = send(CSocket, ToSend.data(), ToSend.size(), 0); if (Result < 0) { error("(Game) send failed with error: " + std::to_string(WSAGetLastError())); - } else { - char C = Data.empty() ? 0 : Data.at(0); - if (C == 'E') { - std::string header = std::string(Data.substr(0, std::min(Data.length(), 120))); - auto payloadStart = header.find_first_of("{["); - if (payloadStart != std::string::npos) header = header.substr(0, payloadStart); - if (!header.empty() && header.back() == ':') header.pop_back(); - debug("(Server->Launcher) Custom Event: Size: " + std::to_string(Data.length()) + " bytes, Event: " + header); - } } } @@ -98,14 +89,8 @@ void ServerSend(std::string Data, bool Rel) { } else UDPSend(Data); - if (C == 'E') { - std::string header = std::string(Data.substr(0, std::min(Data.length(), 120))); - auto payloadStart = header.find_first_of("{["); - if (payloadStart != std::string::npos) header = header.substr(0, payloadStart); - if (!header.empty() && header.back() == ':') header.pop_back(); - debug("(Launcher->Server) Custom Event: Size: " + std::to_string(Data.length()) + " bytes, Event: " + header); - } else if (DLen > 1000) { - debug("(Launcher->Server) Large packet sent: " + std::to_string(Data.length()) + " : " + if (DLen > 1000) { + debug("(Launcher->Server) Bytes sent: " + std::to_string(Data.length()) + " : " + Data.substr(0, 10) + Data.substr(Data.length() - 10)); } else if (C == 'Z') { From f251207fc0eaf7016402008aea82a24c523ae988 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 18:11:35 +0200 Subject: [PATCH 5/8] Implement CloseLog to gracefully join logger thread on exit --- include/Logger.h | 1 + src/Logger.cpp | 10 +++++++++- src/main.cpp | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/include/Logger.h b/include/Logger.h index ddea34b7..c8de6ade 100644 --- a/include/Logger.h +++ b/include/Logger.h @@ -8,6 +8,7 @@ #include #include void InitLog(); +void CloseLog(); void except(const std::string& toPrint); void fatal(const std::string& toPrint); void debug(const std::string& toPrint); diff --git a/src/Logger.cpp b/src/Logger.cpp index 8d92d3c2..89200676 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -75,7 +75,15 @@ std::string getDate() { void InitLog() { logThreadRunning = true; logThread = std::thread(logThreadFunc); - logThread.detach(); +} +void CloseLog() { + if (logThreadRunning) { + logThreadRunning = false; + logCV.notify_one(); + if (logThread.joinable()) { + logThread.join(); + } + } } void addToLog(const std::string& Line) { { diff --git a/src/main.cpp b/src/main.cpp index b2f90991..eaf3f53c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -62,9 +62,11 @@ int main(int argc, const char** argv) try { PreGame(GetGameDir()); InitGame(GetGameDir()); CoreNetwork(); + CloseLog(); } catch (const std::exception& e) { error(std::string("Exception in main(): ") + e.what()); info("Closing in 5 seconds"); info("If this keeps happening, contact us on either: Forum: https://forum.beammp.com, Discord: https://discord.gg/beammp"); std::this_thread::sleep_for(std::chrono::seconds(5)); + CloseLog(); } From bf235c334a3a860960cfa3c126d1fc2f4fb8e583 Mon Sep 17 00:00:00 2001 From: Unshown Date: Mon, 24 Aug 2026 18:17:53 +0200 Subject: [PATCH 6/8] Remove accidental pasted import --- src/Logger.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Logger.cpp b/src/Logger.cpp index 89200676..56d0388d 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include "Options.h" #include #include From 9efb72ef259f64da3db3ff827207a31fb7c8b58e Mon Sep 17 00:00:00 2001 From: Unshown Date: Tue, 25 Aug 2026 10:18:40 +0200 Subject: [PATCH 7/8] Switch logger queue to beammp_fs_string and use wofstream on Windows --- src/Logger.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Logger.cpp b/src/Logger.cpp index 56d0388d..ac62b996 100644 --- a/src/Logger.cpp +++ b/src/Logger.cpp @@ -20,12 +20,16 @@ std::mutex logMutex; std::condition_variable logCV; -std::queue logQueue; +std::queue logQueue; bool logThreadRunning = false; std::thread logThread; void logThreadFunc() { +#ifdef _WIN32 + std::wofstream LFS; +#else std::ofstream LFS; +#endif LFS.open(GetEP() + beammp_wide("Launcher.log"), std::ios_base::out); if (!LFS.is_open()) { std::cerr << "Failed to open Launcher.log: " << std::strerror(errno) << std::endl; @@ -37,7 +41,7 @@ void logThreadFunc() { logCV.wait(lock, [] { return !logQueue.empty() || !logThreadRunning; }); while (!logQueue.empty()) { - std::string line = logQueue.front(); + beammp_fs_string line = logQueue.front(); logQueue.pop(); lock.unlock(); @@ -87,16 +91,24 @@ void CloseLog() { void addToLog(const std::string& Line) { { std::lock_guard lock(logMutex); +#ifdef _WIN32 + logQueue.push(Utils::ToWString(Line)); +#else logQueue.push(Line); +#endif } logCV.notify_one(); } void addToLog(const std::wstring& Line) { + { + std::lock_guard lock(logMutex); #ifdef _WIN32 - addToLog(Utils::ToString(Line)); + logQueue.push(Line); #else - addToLog(std::string(Line.begin(), Line.end())); + logQueue.push(std::string(Line.begin(), Line.end())); #endif + } + logCV.notify_one(); } void info(const std::string& toPrint) { std::string Print = getDate() + "[INFO] " + toPrint + "\n"; From 1c9fe1197824f976b7f9fcdb7596462484c08933 Mon Sep 17 00:00:00 2001 From: Unshown Date: Tue, 25 Aug 2026 10:24:40 +0200 Subject: [PATCH 8/8] Remove unused Utils::ToString function --- include/Utils.h | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/include/Utils.h b/include/Utils.h index a91c3fd1..36f35704 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -189,20 +189,6 @@ namespace Utils { MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed); - return result; - } - inline std::string ToString(const std::wstring& s) { - if (s.empty()) return std::string(); - - int size_needed = WideCharToMultiByte(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0, nullptr, nullptr); - if (size_needed <= 0) { - return ""; - } - - std::string result(size_needed, 0); - - WideCharToMultiByte(CP_UTF8, 0, s.c_str(), (int)s.size(), &result[0], size_needed, nullptr, nullptr); - return result; } #else