Skip to content

Commit c718ef1

Browse files
MadMaxMangosclaude
andauthored
Logging performance: off-thread console writer, buffered file sinks, and quieter defaults (#402)
* logging: quiet console defaults (LogLevel 3->1) + suppress AI/spell debug filters Stage 1 / Change A increment 1 (config only, no recompile needed). - LogLevel 3 -> 1: the dist shipped full DEBUG to the console; on Windows the console is flushed per stdio call, so at level 3 every per-spawn DEBUG/DETAIL line is a synchronous console write on the world/map-update threads -> the LivingWorld / playerbot spawn-burst hang. - LogFilter_AIAndMovegens 0 -> 1 and LogFilter_SpellCast 0 -> 1: these two filters shipped enabled; in a LivingWorld bitmask-7 baseline they are ~50% of a LogFileLevel=3 dump (~11k "used AI is" + ~2.7k spell/aura + waypoint). Each stays re-enablable for targeted debugging. Recommended runtime mode is now LogLevel=1 + LogFileLevel=3 (quiet console, buffered DEBUG to file). Spec: docs/logging-perf/SPEC.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * logging: buffer file sinks + atomic, flush-free hot loggers (Stage 1 / Change A) Increment 2 - make LogFileLevel=3 debug fast/buffered/un-torn so devs can run full DEBUG without the world thread stalling on per-line file I/O. - setvbuf(_IOFBF, 64KB) on all file sinks (openLogFile). The four hot loggers (outString/outBasic/outDetail/outDebug) drop their per-line fflush and write the timestamp+body+newline under a new m_fileMtx so concurrent map-update worker threads cannot tear lines. - Log::Flush() drains the buffered main + packet logs; called ~1/sec from World::Update and once at shutdown (after "Bye!"). Errors still flush at emit time and are deliberately NOT under m_fileMtx, so the AntiFreeze watchdog can never be blocked by it and error lines stay crash-durable (accepted cost: a rare error/debug line interleave, no corruption). - Initialize() is now idempotent via CloseLogFiles(), fixing the realmd double-init handle leak; ~Log() shares the same CloseLogFiles(). - outWorldPacketDump rewritten from one fprintf PER BYTE to a single buffered fwrite (byte-identical output). - PacketLoggingEnabled (default off) is the single gate for packet logging: worldLogfile is opened only when set, IsPacketLoggingEnabled() keys off it, and both WorldSocket call sites gate on it -> packet logging is off by default even on legacy configs with WorldLogFile set + LogLevel=3. Recommended dev mode: LogLevel=1 + LogFileLevel=3 (quiet console, buffered DEBUG file). Reviewed by a 3-lens adversarial pass (compile / concurrency / byte-fidelity): SHIP, no compile blockers. Spec: docs/logging-perf/SPEC.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * logging: off-thread console writer (Stage 1 / increment 3) Move the console emit off the world + map-update threads so live LogLevel=3 console debug no longer stalls them. On Windows the CRT flushes the console per stdio call, so console writes can't be batched inline - a dedicated thread does them. - ConsoleLogWriter (ACE_Based::Runnable, modeled on SqlDelayThread): producers format the line and Enqueue a by-value ConsoleLogRecord into a LockedQueue; one writer thread drains it and does Log::SetColor (now static) + OEM transform + fwrite + ResetColor, per line, off the hot threads. Bounded with drop+count on overflow (the file sink is durable, so dropped/crash-lost console lines are harmless). The writer never calls sLog (no reentrancy). - Every logger's CONSOLE block now routes through Log::ConsoleEmit / ConsoleEmitBlank (async when the thread runs, synchronous inline fallback otherwise - startup + shutdown tail). The increment-2 m_fileMtx FILE blocks are untouched and the public sLog API is unchanged. - Util::vutf8format returns the formatted OEM/UTF-8 bytes as std::string (same vsnprintf + Utf8toWStr + CharToOemBuffW chain as vutf8printf; byte-identical). - mangosd starts the writer after config/InitColors and stops+drains+joins it at shutdown before the final Flush. realmd (a submodule) is intentionally NOT wired - it uses the synchronous fallback; its console-thread wiring is a deferred submodule follow-up. Built clean (RelWithDebInfo: mangosd + realmd, 0 errors/warnings) and reviewed by a 3-lens adversarial pass (concurrency/lifecycle, console-fidelity, style), SHIP_WITH_FIXES: the no-arg error variants now route through the writer, the drop-notice fwrite is clamped, and the StopConsoleThread quiescence invariant is documented. Spec: docs/logging-perf/SPEC.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * logging: address Codex final review of fix/logging-perf-speedfix Adversarial pre-PR review (Codex) flagged correctness/portability issues in the off-thread console + buffered-file rework. Fixes (no public sLog API change, core-side only, no submodule edits): BLOCKING - SOAP UAF on shutdown: the SOAP listener is a std::thread, not an ACE task, so ACE_Thread_Manager::wait() never joined it; its shared_ptr deleter joined only at main() scope exit, AFTER StopConsoleThread() freed the writer -- a late SOAP log could enqueue into a freed ConsoleLogWriter. Now soapThread is reset() (joined) before StopConsoleThread(), upholding the quiescence invariant for SOAP too. (mangosd.cpp) - ConsoleLogWriter::m_running is now std::atomic<bool>, not volatile bool, so Stop()/run() synchronise correctly (volatile is not a memory-model fence; matters on weakly-ordered archs). (ConsoleLogWriter.h) MAJOR - Console writer started too early: it was launched before the OpenSSL / PID / start_db init, whose early-return error paths left the writer thread running into stdio teardown. Moved StartConsoleThread() to just after start_db() succeeds -- still before SetInitialWorldSettings() (the spawn-burst hot path) -- so no fatal early-return leaks a running writer. (mangosd.cpp) - POSIX truncation regression: vutf8format() clamped to 32 KiB whereas the legacy POSIX path used unbounded vfprintf. Oversize messages are now rendered in full via a sized second pass (va_copy). (Util.cpp) - Console byte-fidelity: the trailing newline was being written INSIDE the SetColor/ResetColor span; restored the legacy ordering (body -> ResetColor -> "\n") by keeping the newline off rec.text and appending it after reset in the writer and the sync fallback. (Log.cpp, ConsoleLogWriter.cpp, Log.h) MINOR / hygiene - Writer now owns the stdout flush (one fflush per drain batch, off the world thread) so redirected/piped consoles stay prompt. (ConsoleLogWriter.cpp) - Added <utility> for std::move (was relying on a transitive include that GCC/ Clang may not provide). (Log.cpp) - Refreshed stale config comments (LogLevel now defaults to 1; AIAndMovegens / SpellCast now ship suppressed) and documented PacketLoggingEnabled. (mangosd.conf.dist.in) Note (intentional, not restored): all error console output (timestamp + message + newline) now goes to stderr consistently. Previously the optional LogTime timestamp leaked to stdout via outTime() while the message went to stderr; with the default LogTime=0 the console output is byte-identical to master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * logging: bump world ConfVersion for the new logging config keys The logging-performance change adds PacketLoggingEnabled and flips the default LogLevel / LogFilter_AIAndMovegens / LogFilter_SpellCast values in mangosd.conf.dist, so bump MANGOS_WORLD_VER (drives both the generated ConfVersion and the compiled MANGOSD_CONFIG_VERSION) to today's date. Servers running an older mangosd.conf now get the "conf out of date" warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e6b8563 commit c718ef1

12 files changed

Lines changed: 726 additions & 346 deletions

File tree

cmake/MangosParams.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
set(MANGOS_EXP "CLASSIC")
22
set(MANGOS_PKG "Mangos Zero")
3-
set(MANGOS_WORLD_VER 2026062401)
3+
set(MANGOS_WORLD_VER 2026062801)
44
set(MANGOS_REALM_VER 2026060300)
55
set(MANGOS_AHBOT_VER 2021010100)

src/game/Server/WorldSocket.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,11 @@ int WorldSocket::ProcessIncoming(WorldPacket* new_pct)
657657
return -1;
658658
}
659659

660-
// Dump received packet.
661-
sLog.outWorldPacketDump(uint32(get_handle()), new_pct->GetOpcode(), new_pct->GetOpcodeName(), new_pct, true);
660+
// Dump received packet (opt-in via PacketLoggingEnabled; off by default).
661+
if (sLog.IsPacketLoggingEnabled())
662+
{
663+
sLog.outWorldPacketDump(uint32(get_handle()), new_pct->GetOpcode(), new_pct->GetOpcodeName(), new_pct, true);
664+
}
662665

663666
try
664667
{
@@ -1051,7 +1054,7 @@ int WorldSocket::iSendPacket(const WorldPacket& pct)
10511054
return -1;
10521055
}
10531056

1054-
if (sLog.HasLogLevelOrHigher(LOG_LVL_DEBUG)) // allow server packet logging
1057+
if (sLog.IsPacketLoggingEnabled()) // server packet logging (opt-in via PacketLoggingEnabled)
10551058
{
10561059
sLog.outWorldPacketDump(uint32(get_handle()), pct.GetOpcode(), pct.GetOpcodeName(), &pct, false);
10571060
}

src/game/WorldHandlers/World.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1839,6 +1839,18 @@ void World::Update(uint32 diff)
18391839
GameTime::UpdateGameTimers();
18401840
sWorldUpdateTime.UpdateWithDiff(diff);
18411841

1842+
///- Flush buffered log files about once per second. The file sinks are
1843+
/// fully buffered (setvbuf), so this bounds how long a buffered line waits
1844+
/// to reach disk; error paths still flush immediately at emit time. Safe as
1845+
/// a function-local static: World::Update runs only on the world thread.
1846+
static uint32 logFlushTimer = 0;
1847+
logFlushTimer += diff;
1848+
if (logFlushTimer >= 1000)
1849+
{
1850+
logFlushTimer = 0;
1851+
sLog.Flush();
1852+
}
1853+
18421854
///-Update mass mailer tasks if any
18431855
sMassMailMgr.Update();
18441856

src/mangosd/mangosd.conf.dist.in

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,10 @@ MaxWhoListReturns = 49
260260
# LogLevel
261261
# Server console level of logging
262262
# 0 = Minimum; 1 = Basic&Error; 2 = Detail; 3 = Full/Debug
263-
# Default: 3
263+
# Default: 1 - Basic&Error (keeps the live console light; level 3 in
264+
# particular is very chatty and can stall startup under
265+
# large spawn loads -- prefer raising LogFileLevel instead
266+
# to capture detail to disk without the console cost)
264267
#
265268
# LogTime
266269
# Include time in server console output [hh:mm:ss]
@@ -303,9 +306,13 @@ MaxWhoListReturns = 49
303306
# LogFilter_Damage
304307
# LogFilter_Combat
305308
# LogFilter_SpellCast
306-
# Log filters (disabled by default, mostly debug only output affected cases)
309+
# Log filters (mostly debug-only output for the affected cases)
307310
# Default: 0 - include in log if log level permit
308311
# 1 - not include with any log level
312+
# NOTE: LogFilter_AIAndMovegens and LogFilter_SpellCast now ship as 1
313+
# (suppressed). Both are extremely high-volume on busy or debug
314+
# servers (movement generators; every spell cast) and otherwise
315+
# dominate the logs. Set either to 0 to restore its verbose output.
309316
#
310317
# LogWhispers
311318
# Log whispers between players to database, character.character_whispers
@@ -318,6 +325,15 @@ MaxWhoListReturns = 49
318325
# Default: "" - no log file created
319326
# "world.log" - recommended name to create a log file
320327
#
328+
# PacketLoggingEnabled
329+
# Master switch for world packet logging (the WorldLogFile dump). Packet
330+
# logging is high-volume and was historically turned on implicitly
331+
# whenever WorldLogFile was set; it is now OFF unless this is explicitly
332+
# set to 1, so an existing config that sets WorldLogFile (even at
333+
# LogLevel 3) no longer pays the packet-dump cost by accident.
334+
# Default: 0 - do not write packet dumps (recommended)
335+
# 1 - write packet dumps to WorldLogFile
336+
#
321337
# WorldLogTimestamp
322338
# Logfile with timestamp of server start in name
323339
# Default: 0 - no timestamp in name
@@ -389,7 +405,7 @@ MaxWhoListReturns = 49
389405

390406
LogSQL = 1
391407
PidFile = ""
392-
LogLevel = 3
408+
LogLevel = 1
393409
LogTime = 0
394410
LogFile = "world-server.log"
395411
LogTimestamp = 0
@@ -406,13 +422,14 @@ LogFilter_CellEnvelope = 1
406422
LogFilter_PeriodicAffects = 0
407423
LogFilter_PlayerMoves = 1
408424
LogFilter_SQLText = 1
409-
LogFilter_AIAndMovegens = 0
425+
LogFilter_AIAndMovegens = 1
410426
LogFilter_PlayerStats = 0
411427
LogFilter_Damage = 0
412428
LogFilter_Combat = 0
413-
LogFilter_SpellCast = 0
429+
LogFilter_SpellCast = 1
414430
LogWhispers = 1
415431
WorldLogFile = "world-packets.log"
432+
PacketLoggingEnabled = 0
416433
WorldLogTimestamp = 0
417434
DBErrorLogFile = "world-database.log"
418435
ElunaErrorLogFile = "ElunaErrors.log"

src/mangosd/mangosd.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,14 @@ int main(int argc, char** argv)
485485
return 1;
486486
}
487487

488+
// Move the console emit off the world/map-update threads. Started only after
489+
// the fallible init above (OpenSSL / PID file / DB) so the early-return error
490+
// paths never leave a writer thread running into stdio teardown; still placed
491+
// before SetInitialWorldSettings() -- the LivingWorld spawn burst -- so the
492+
// hot console path is covered. Config/InitColors already applied, so colors
493+
// are set; from here the per-call console flush runs on the writer thread.
494+
sLog.StartConsoleThread();
495+
488496
///- Set Realm to Offline, if crash happens. Only used once.
489497
LoginDatabase.DirectPExecute("UPDATE `realmlist` SET `realmflags` = `realmflags` | %u WHERE `id` = '%u'", REALM_FLAG_OFFLINE, realmID);
490498

@@ -677,7 +685,33 @@ int main(int argc, char** argv)
677685
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
678686
#endif
679687

688+
#ifdef ENABLE_SOAP
689+
// Join the SOAP listener before stopping the console writer. SOAP runs on a
690+
// std::thread (NOT an ACE task), so ACE_Thread_Manager::wait() above did NOT
691+
// join it; its shared_ptr deleter would otherwise join only at main() scope
692+
// exit -- AFTER StopConsoleThread() deletes the writer -- leaving a window
693+
// where a late SOAP log could enqueue into a freed ConsoleLogWriter (UAF).
694+
// Resetting here runs the deleter (join + delete) now, so the writer's
695+
// quiescence invariant holds for SOAP too.
696+
soapThread.reset();
697+
#endif
698+
699+
// Stop and join the off-thread console writer before the final shutdown lines:
700+
// later lines ("Bye!") then take the synchronous fallback. Placed after EVERY
701+
// console-producing thread is gone -- world/map ACE workers (joined by
702+
// ACE_Thread_Manager::wait above), the CLI thread, and the SOAP std::thread
703+
// (joined just above) -- so no concurrent producer can race the writer delete.
704+
// The remaining main-thread shutdown lines are drained by the still-running
705+
// writer before it joins. Precedes the final Flush.
706+
sLog.StopConsoleThread();
707+
680708
sLog.outString("Bye!");
709+
710+
// Final flush of the buffered file logs before exit. ~Log/CloseLogFiles also
711+
// flush via fclose, but this guarantees "Bye!" and any late shutdown lines
712+
// reach disk first.
713+
sLog.Flush();
714+
681715
return code;
682716
}
683717
/// @}

src/shared/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ source_group("LockedQueue" FILES ${SRC_GRP_LOCKQ})
109109
set(SRC_GRP_LOG
110110
Log/Log.cpp
111111
Log/Log.h
112+
Log/ConsoleLogWriter.cpp
113+
Log/ConsoleLogWriter.h
112114
)
113115
source_group("Log" FILES ${SRC_GRP_LOG})
114116

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* MaNGOS is a full featured server for World of Warcraft, supporting
3+
* the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8
4+
*
5+
* Copyright (C) 2005-2025 MaNGOS <https://www.getmangos.eu>
6+
*
7+
* This program is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation; either version 2 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program; if not, write to the Free Software
19+
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20+
*
21+
* World of Warcraft, and all World of Warcraft or Warcraft art, images,
22+
* and lore are copyrighted by Blizzard Entertainment, Inc.
23+
*/
24+
25+
/**
26+
* @file ConsoleLogWriter.cpp
27+
* @brief Off-thread console log writer implementation
28+
*
29+
* Implements the dedicated thread that renders queued console log records,
30+
* moving the per-call console flush (and the SetConsoleTextAttribute / OEM
31+
* transform) off the world and map-update threads.
32+
*/
33+
34+
#include "ConsoleLogWriter.h"
35+
36+
#include <cstdio>
37+
38+
ConsoleLogWriter::ConsoleLogWriter()
39+
: m_depth(0), m_dropped(0), m_running(true)
40+
{
41+
}
42+
43+
void ConsoleLogWriter::Enqueue(ConsoleLogRecord& rec)
44+
{
45+
if (m_depth.value() >= MAX_CONSOLE_QUEUE)
46+
{
47+
++m_dropped;
48+
return;
49+
}
50+
++m_depth;
51+
m_queue.add(rec);
52+
}
53+
54+
void ConsoleLogWriter::run()
55+
{
56+
while (m_running)
57+
{
58+
if (!DrainOnce())
59+
{
60+
ACE_Based::Thread::Sleep(5);
61+
}
62+
}
63+
// Authoritative final drain: runs on the writer thread while wait()
64+
// blocks the caller, so any straggler enqueued before Stop() is rendered.
65+
DrainOnce();
66+
}
67+
68+
bool ConsoleLogWriter::DrainOnce()
69+
{
70+
bool didWork = false;
71+
72+
long dropped = m_dropped.value();
73+
if (dropped > 0)
74+
{
75+
m_dropped -= dropped;
76+
char note[96];
77+
int n = snprintf(note, sizeof(note),
78+
"[Log] %ld console line(s) dropped (queue full)\n", dropped);
79+
if (n >= (int)sizeof(note))
80+
{
81+
n = (int)sizeof(note) - 1; // clamp: never fwrite past note[]
82+
}
83+
if (n > 0)
84+
{
85+
Log::SetColor(true, RED);
86+
fwrite(note, 1, (size_t)n, stdout);
87+
Log::ResetColor(true);
88+
}
89+
didWork = true;
90+
}
91+
92+
ConsoleLogRecord rec;
93+
while (m_queue.next(rec))
94+
{
95+
--m_depth;
96+
Emit(rec);
97+
didWork = true;
98+
}
99+
100+
// The writer owns the console stream, so it owns the flush: a redirected /
101+
// piped stdout is fully buffered, and flushing here (off the world/map
102+
// threads) makes output prompt without any producer paying for it. A real
103+
// console is effectively unbuffered, so this is a cheap no-op there.
104+
if (didWork)
105+
{
106+
fflush(stdout);
107+
}
108+
109+
return didWork;
110+
}
111+
112+
void ConsoleLogWriter::Emit(const ConsoleLogRecord& rec)
113+
{
114+
FILE* out = rec.toStdout ? stdout : stderr;
115+
if (rec.applyColor)
116+
{
117+
Log::SetColor(rec.toStdout, rec.color);
118+
}
119+
if (!rec.text.empty())
120+
{
121+
fwrite(rec.text.data(), 1, rec.text.size(), out);
122+
}
123+
if (rec.applyColor)
124+
{
125+
Log::ResetColor(rec.toStdout);
126+
}
127+
// Newline is written AFTER ResetColor so the line terminator stays outside
128+
// the color span, byte-matching the legacy console ordering. The producer
129+
// (Log::ConsoleEmit) deliberately leaves it off rec.text.
130+
fputc('\n', out);
131+
}

0 commit comments

Comments
 (0)