Skip to content

Commit ab40dda

Browse files
authored
Make log level checks lock-free via permanent loggers. (#5349)
# Description Make log level checks lock-free via permanent loggers. Every CLOG_* invocation fetches its respective logger instance in order to check if we should log this line, and the getter used to be guarded by a recursive mutex lock. So we kept serializing every disabled log statement, which hindered parallelization for the code paths with frequent trace/debug logs. Now each partition has a single permanent logger, created on first use and never destroyed or replaced (logging rotations/reconfigurations now are done at the sink level, instead of the logger level). Additionally, we avoid a shared ptr copy on every logger access, as we now we can just safely return a non-owning pointer to it. The logging level checks per logger still have to be thread-safe, as the level may be changed in the middle of the app lifetime. We remove SPDLOG_NO_ATOMIC_LEVELS to achieve that, which simply uses very fast relaxed atomic checks. This doesn't have too large impact right now (~-3ms on the local benchmarks), but it will become more meaningful as we land more apply path parallelization. # Checklist - [ ] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [ ] Rebased on top of master (no merge commits) - [ ] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [ ] Compiles - [ ] Ran all tests - [ ] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents e61776d + 0ad4d19 commit ab40dda

3 files changed

Lines changed: 128 additions & 50 deletions

File tree

src/util/Logging.cpp

Lines changed: 109 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <fmt/chrono.h>
1212
#include <fstream>
1313
#include <spdlog/sinks/basic_file_sink.h>
14+
#include <spdlog/sinks/dist_sink.h>
1415
#include <spdlog/sinks/stdout_color_sinks.h>
1516
#include <spdlog/sinks/stdout_sinks.h>
1617
#include <spdlog/spdlog.h>
@@ -36,7 +37,7 @@ bool Logging::mColor = false;
3637
std::string Logging::mLastPattern;
3738
std::string Logging::mLastFilenamePattern;
3839
bool Logging::mLogToConsole = true;
39-
#endif
40+
#endif // USE_SPDLOG
4041

4142
// Right now this is hard-coded to log messages at least as important as INFO
4243
CoutLogger::CoutLogger(LogLevel l) : mShouldLog(l <= Logging::getLogLevel(""))
@@ -81,7 +82,68 @@ convert_loglevel(LogLevel level)
8182
}
8283
return slev;
8384
}
84-
#endif
85+
86+
namespace
87+
{
88+
// Permanent logger to use per each logging partition.
89+
// This is intended to be created on the first use and never destroyed.
90+
struct PermanentLogger
91+
{
92+
std::shared_ptr<spdlog::sinks::dist_sink_mt> mSink;
93+
LogPtr mLogger;
94+
};
95+
96+
PermanentLogger
97+
makePermanentLogger(std::string const& name, bool isDefault)
98+
{
99+
auto sink = std::make_shared<spdlog::sinks::dist_sink_mt>();
100+
auto logger = std::make_shared<spdlog::logger>(name, sink);
101+
if (isDefault)
102+
{
103+
// Logging through DEFAULT_LOG before Logging::init() goes to the
104+
// console, matching spdlog's own auto-created default logger.
105+
sink->add_sink(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
106+
// This also registers the logger in the spdlog registry.
107+
spdlog::set_default_logger(logger);
108+
}
109+
else
110+
{
111+
spdlog::register_logger(logger);
112+
}
113+
return PermanentLogger{std::move(sink), std::move(logger)};
114+
}
115+
116+
PermanentLogger&
117+
defaultPermanentLogger()
118+
{
119+
static PermanentLogger pl = makePermanentLogger("default", true);
120+
return pl;
121+
}
122+
123+
#define LOG_PARTITION(name) \
124+
PermanentLogger& name##PermanentLogger() \
125+
{ \
126+
static PermanentLogger pl = makePermanentLogger(#name, false); \
127+
return pl; \
128+
}
129+
#include "util/LogPartitions.def"
130+
#undef LOG_PARTITION
131+
132+
// NB: forces creation (and spdlog-registry registration) of every permanent
133+
// logger, so registry-wide operations like spdlog::set_pattern and
134+
// spdlog::set_level cover all of them deterministically.
135+
std::vector<PermanentLogger*>
136+
allPermanentLoggers()
137+
{
138+
std::vector<PermanentLogger*> loggers;
139+
loggers.push_back(&defaultPermanentLogger());
140+
#define LOG_PARTITION(name) loggers.push_back(&name##PermanentLogger());
141+
#include "util/LogPartitions.def"
142+
#undef LOG_PARTITION
143+
return loggers;
144+
}
145+
} // namespace
146+
#endif // USE_SPDLOG
85147

86148
void
87149
Logging::init(bool truncate)
@@ -166,25 +228,24 @@ Logging::init(bool truncate)
166228
make_shared<basic_file_sink_mt>(filename, /*truncate=*/false));
167229
}
168230

169-
auto makeLogger =
170-
[&](std::string const& name) -> shared_ptr<spdlog::logger> {
171-
auto logger =
172-
make_shared<spdlog::logger>(name, sinks.begin(), sinks.end());
173-
spdlog::register_logger(logger);
174-
return logger;
175-
};
176-
177-
spdlog::set_default_logger(makeLogger("default"));
178-
for (auto const& partition : stellar::Logging::kPartitionNames)
231+
// Attach the configured sinks to all the permanent loggers.
232+
for (auto* permanentLogger : allPermanentLoggers())
179233
{
180-
makeLogger(partition);
234+
permanentLogger->mSink->set_sinks(sinks);
181235
}
182236
if (mLastPattern.empty())
183237
{
184238
mLastPattern = "%Y-%m-%dT%H:%M:%S.%e [%^%n %l%$] %v";
185239
}
186240
auto maxLevel = mGlobalLogLevel;
187241
spdlog::set_pattern(mLastPattern);
242+
// NB: these level writes are read lock-free by isLogLevelAtLeast() on
243+
// other threads. set_level() first resets every logger to the global
244+
// level, then the loop applies per-partition overrides, so a thread
245+
// logging concurrently with (re)configuration can momentarily observe a
246+
// partition at the global level (or an override that is about to be
247+
// re-applied). This is benign: reconfiguration is rare, and the worst
248+
// case is a single log line emitted or suppressed at the prior level.
188249
spdlog::set_level(convert_loglevel(mGlobalLogLevel));
189250
for (auto const& pair : mPartitionLogLevels)
190251
{
@@ -206,10 +267,15 @@ Logging::deinit()
206267
std::lock_guard<std::recursive_mutex> guard(mLogMutex);
207268
if (mInitialized)
208269
{
209-
#define LOG_PARTITION(name) Logging::name##LogPtr = nullptr;
210-
#include "util/LogPartitions.def"
211-
#undef LOG_PARTITION
212-
spdlog::drop_all();
270+
// Detach all the sinks from the permanent loggers (which
271+
// closes the log file once the last reference drops).
272+
// The loggers themselves are never destroyed and just stop writing
273+
// anywhere until the next init().
274+
for (auto* permanentLogger : allPermanentLoggers())
275+
{
276+
permanentLogger->mSink->flush();
277+
permanentLogger->mSink->set_sinks({});
278+
}
213279
mInitialized = false;
214280
}
215281
#endif
@@ -291,17 +357,9 @@ Logging::setLogLevel(LogLevel level, char const* partition)
291357
mPartitionLogLevels.clear();
292358
}
293359
#if defined(USE_SPDLOG)
360+
// Re-initialize the loggers, which also picks up the new levels.
294361
deinit();
295362
init();
296-
auto slev = convert_loglevel(level);
297-
if (partition)
298-
{
299-
spdlog::get(partition)->set_level(slev);
300-
}
301-
else
302-
{
303-
spdlog::set_level(slev);
304-
}
305363
#endif
306364
}
307365

@@ -384,13 +442,29 @@ Logging::logTrace(std::string const& partition)
384442
bool
385443
Logging::isLogLevelAtLeast(std::string const& partition, LogLevel level)
386444
{
445+
#if defined(USE_SPDLOG)
446+
// Read the (atomic) level of the permanent partition logger instead of
447+
// consulting the level maps under the global mutex: this function is
448+
// called from hot paths on concurrently-running threads. The logger
449+
// levels are kept in sync with the maps by setLogLevel()/init().
450+
auto slev = convert_loglevel(level);
451+
#define LOG_PARTITION(name) \
452+
if (partition == #name) \
453+
{ \
454+
return name##PermanentLogger().mLogger->should_log(slev); \
455+
}
456+
#include "util/LogPartitions.def"
457+
#undef LOG_PARTITION
458+
return defaultPermanentLogger().mLogger->should_log(slev);
459+
#else
387460
std::lock_guard<std::recursive_mutex> guard(mLogMutex);
388461
auto it = mPartitionLogLevels.find(partition);
389462
if (it != mPartitionLogLevels.end())
390463
{
391464
return it->second >= level;
392465
}
393466
return mGlobalLogLevel >= level;
467+
#endif
394468
}
395469

396470
void
@@ -418,27 +492,19 @@ Logging::normalizePartition(std::string const& partition)
418492
std::recursive_mutex Logging::mLogMutex;
419493

420494
#if defined(USE_SPDLOG)
421-
LogPtr Logging::defaultLogPtr = nullptr;
422-
LogPtr
495+
// These are called by every CLOG_* macro invocation (including ones for
496+
// disabled levels) and must remain lock-free and write-free: they return a
497+
// raw pointer into a permanent logger that is guaranteed to never be destroyed,
498+
// and thus is safe to be stored if necessary.
499+
spdlog::logger*
423500
Logging::getDefaultLogPtr()
424501
{
425-
std::lock_guard<std::recursive_mutex> guard(mLogMutex);
426-
if (!defaultLogPtr)
427-
{
428-
defaultLogPtr = spdlog::default_logger();
429-
}
430-
return defaultLogPtr;
502+
return defaultPermanentLogger().mLogger.get();
431503
}
432504
#define LOG_PARTITION(name) \
433-
LogPtr Logging::name##LogPtr = nullptr; \
434-
LogPtr Logging::get##name##LogPtr() \
505+
spdlog::logger* Logging::get##name##LogPtr() \
435506
{ \
436-
std::lock_guard<std::recursive_mutex> guard(mLogMutex); \
437-
if (!name##LogPtr) \
438-
{ \
439-
name##LogPtr = spdlog::get(#name); \
440-
} \
441-
return name##LogPtr; \
507+
return name##PermanentLogger().mLogger.get(); \
442508
}
443509
#include "util/LogPartitions.def"
444510
#undef LOG_PARTITION
@@ -458,7 +524,7 @@ Logging::logAtPartitionAndLevel(std::string const& partition, LogLevel level,
458524
}
459525
#include "util/LogPartitions.def"
460526
#undef LOG_PARTITION
461-
LOG_CHECK(spdlog::default_logger(), lev, lg->log(lev, msg));
527+
LOG_CHECK(Logging::getDefaultLogPtr(), lev, lg->log(lev, msg));
462528
#else
463529
CoutLogger logger(level) << msg;
464530
#endif

src/util/Logging.h

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ class CoutLogger
157157
}
158158
};
159159

160+
// Logging uses one *permanent* spdlog logger per partition (plus "default"):
161+
// the logger objects are created on first use and are never destroyed or
162+
// replaced for the lifetime of the process. Each logger's single sink is a
163+
// thread-safe dist_sink_mt whose child sinks (console/file) are swapped in
164+
// place, under that sink's own mutex, whenever logging is (re)configured.
165+
// Log levels are atomics inside spdlog.
166+
//
167+
// This makes the per-partition getters below (and thus every CLOG_* call
168+
// site) lock-free and write-free: checking whether a disabled level is
169+
// enabled costs two loads and touches no shared mutable state, so it can be
170+
// done freely from concurrently-running threads. It is also safe to cache the
171+
// returned pointer: reconfiguration changes what the permanent loggers write
172+
// to, not the logger identities.
160173
class Logging
161174
{
162175
static LogLevel mGlobalLogLevel;
@@ -168,10 +181,6 @@ class Logging
168181
static std::string mLastPattern;
169182
static std::string mLastFilenamePattern;
170183
static bool mLogToConsole;
171-
static LogPtr defaultLogPtr;
172-
#define LOG_PARTITION(name) static LogPtr name##LogPtr;
173-
#include "util/LogPartitions.def"
174-
#undef LOG_PARTITION
175184
#endif
176185

177186
public:
@@ -196,8 +205,8 @@ class Logging
196205
static std::array<std::string const, 15> const kPartitionNames;
197206

198207
#if defined(USE_SPDLOG)
199-
static LogPtr getDefaultLogPtr();
200-
#define LOG_PARTITION(name) static LogPtr get##name##LogPtr();
208+
static spdlog::logger* getDefaultLogPtr();
209+
#define LOG_PARTITION(name) static spdlog::logger* get##name##LogPtr();
201210
#include "util/LogPartitions.def"
202211
#undef LOG_PARTITION
203212
#endif

src/util/SpdlogTweaks.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
#define SPDLOG_FMT_EXTERNAL
1313
#define SPDLOG_NO_THREAD_ID
1414
#define SPDLOG_NO_TLS
15-
#define SPDLOG_NO_ATOMIC_LEVELS
15+
// NB: SPDLOG_NO_ATOMIC_LEVELS must remain undefined: stellar logging keeps
16+
// permanent logger objects whose levels can be changed in place while other
17+
// threads are logging through them, so the level reads/writes must be
18+
// atomic. A relaxed atomic load costs the same as a plain load on x86/arm64.
1619
#define SPDLOG_PREVENT_CHILD_FD
1720
#define SPDLOG_LEVEL_NAMES \
1821
{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL", "OFF"}

0 commit comments

Comments
 (0)