diff options
| author | 2021-08-13 18:39:45 +0000 | |
|---|---|---|
| committer | 2021-08-13 18:39:45 +0000 | |
| commit | 001675dced1b7b751d1db4f0d6490776c613df2f (patch) | |
| tree | 151923c90c42bdcede2581ddf6b56768bfcafc01 /src/common/logging/backend.cpp | |
| parent | Merge pull request #6862 from german77/badsdl (diff) | |
| download | yuzu-001675dced1b7b751d1db4f0d6490776c613df2f.tar.gz yuzu-001675dced1b7b751d1db4f0d6490776c613df2f.tar.xz yuzu-001675dced1b7b751d1db4f0d6490776c613df2f.zip | |
logging: Simplify and make thread-safe
This simplifies the logging system.
This also fixes some lost messages on startup.
The simplification is simple. I removed unused functions and moved most things in the .h to the .cpp. I replaced the unnecessary linked list with its contents laid out as three member variables. Anything that went through the linked list now directly accesses the backends. Generic functions are replaced with those for each specific use case and there aren't many. This change increases coupling but we gain back more KISS and encapsulation.
With those changes it was easy to make it thread-safe. I just removed the mutex and turned a boolean atomic. I was planning to use this thread-safety in my next PR about stacktraces. It was actually async-signal-safety at first but I ended up using a different approach. Anyway getting rid of the linked list is important for that because have the list of backends constantly changing complicates things.
Diffstat (limited to 'src/common/logging/backend.cpp')
| -rw-r--r-- | src/common/logging/backend.cpp | 350 |
1 files changed, 211 insertions, 139 deletions
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 61dddab3f..13edda9c9 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp | |||
| @@ -2,13 +2,9 @@ | |||
| 2 | // Licensed under GPLv2 or any later version | 2 | // Licensed under GPLv2 or any later version |
| 3 | // Refer to the license.txt file included. | 3 | // Refer to the license.txt file included. |
| 4 | 4 | ||
| 5 | #include <algorithm> | ||
| 6 | #include <atomic> | 5 | #include <atomic> |
| 7 | #include <chrono> | 6 | #include <chrono> |
| 8 | #include <climits> | 7 | #include <climits> |
| 9 | #include <condition_variable> | ||
| 10 | #include <memory> | ||
| 11 | #include <mutex> | ||
| 12 | #include <thread> | 8 | #include <thread> |
| 13 | #include <vector> | 9 | #include <vector> |
| 14 | 10 | ||
| @@ -16,104 +12,229 @@ | |||
| 16 | #include <windows.h> // For OutputDebugStringW | 12 | #include <windows.h> // For OutputDebugStringW |
| 17 | #endif | 13 | #endif |
| 18 | 14 | ||
| 19 | #include "common/assert.h" | ||
| 20 | #include "common/fs/file.h" | 15 | #include "common/fs/file.h" |
| 21 | #include "common/fs/fs.h" | 16 | #include "common/fs/fs.h" |
| 17 | #include "common/fs/fs_paths.h" | ||
| 18 | #include "common/fs/path_util.h" | ||
| 22 | #include "common/literals.h" | 19 | #include "common/literals.h" |
| 23 | 20 | ||
| 24 | #include "common/logging/backend.h" | 21 | #include "common/logging/backend.h" |
| 25 | #include "common/logging/log.h" | 22 | #include "common/logging/log.h" |
| 26 | #include "common/logging/text_formatter.h" | 23 | #include "common/logging/text_formatter.h" |
| 27 | #include "common/settings.h" | 24 | #include "common/settings.h" |
| 25 | #ifdef _WIN32 | ||
| 28 | #include "common/string_util.h" | 26 | #include "common/string_util.h" |
| 27 | #endif | ||
| 29 | #include "common/threadsafe_queue.h" | 28 | #include "common/threadsafe_queue.h" |
| 30 | 29 | ||
| 31 | namespace Common::Log { | 30 | namespace Common::Log { |
| 32 | 31 | ||
| 32 | namespace { | ||
| 33 | |||
| 33 | /** | 34 | /** |
| 34 | * Static state as a singleton. | 35 | * Interface for logging backends. |
| 35 | */ | 36 | */ |
| 36 | class Impl { | 37 | class Backend { |
| 37 | public: | 38 | public: |
| 38 | static Impl& Instance() { | 39 | virtual ~Backend() = default; |
| 39 | static Impl backend; | 40 | |
| 40 | return backend; | 41 | virtual void Write(const Entry& entry) = 0; |
| 42 | |||
| 43 | virtual void EnableForStacktrace() = 0; | ||
| 44 | |||
| 45 | virtual void Flush() = 0; | ||
| 46 | }; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Backend that writes to stderr and with color | ||
| 50 | */ | ||
| 51 | class ColorConsoleBackend final : public Backend { | ||
| 52 | public: | ||
| 53 | explicit ColorConsoleBackend() = default; | ||
| 54 | |||
| 55 | ~ColorConsoleBackend() override = default; | ||
| 56 | |||
| 57 | void Write(const Entry& entry) override { | ||
| 58 | if (enabled.load(std::memory_order_relaxed)) { | ||
| 59 | PrintColoredMessage(entry); | ||
| 60 | } | ||
| 41 | } | 61 | } |
| 42 | 62 | ||
| 43 | Impl(const Impl&) = delete; | 63 | void Flush() override { |
| 44 | Impl& operator=(const Impl&) = delete; | 64 | // stderr shouldn't be buffered |
| 65 | } | ||
| 45 | 66 | ||
| 46 | Impl(Impl&&) = delete; | 67 | void EnableForStacktrace() override { |
| 47 | Impl& operator=(Impl&&) = delete; | 68 | enabled = true; |
| 69 | } | ||
| 48 | 70 | ||
| 49 | void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, | 71 | void SetEnabled(bool enabled_) { |
| 50 | const char* function, std::string message) { | 72 | enabled = enabled_; |
| 51 | message_queue.Push( | 73 | } |
| 52 | CreateEntry(log_class, log_level, filename, line_num, function, std::move(message))); | 74 | |
| 75 | private: | ||
| 76 | std::atomic_bool enabled{false}; | ||
| 77 | }; | ||
| 78 | |||
| 79 | /** | ||
| 80 | * Backend that writes to a file passed into the constructor | ||
| 81 | */ | ||
| 82 | class FileBackend final : public Backend { | ||
| 83 | public: | ||
| 84 | explicit FileBackend(const std::filesystem::path& filename) { | ||
| 85 | auto old_filename = filename; | ||
| 86 | old_filename += ".old.txt"; | ||
| 87 | |||
| 88 | // Existence checks are done within the functions themselves. | ||
| 89 | // We don't particularly care if these succeed or not. | ||
| 90 | static_cast<void>(FS::RemoveFile(old_filename)); | ||
| 91 | static_cast<void>(FS::RenameFile(filename, old_filename)); | ||
| 92 | |||
| 93 | file = std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, | ||
| 94 | FS::FileType::TextFile); | ||
| 95 | } | ||
| 96 | |||
| 97 | ~FileBackend() override = default; | ||
| 98 | |||
| 99 | void Write(const Entry& entry) override { | ||
| 100 | if (!enabled) { | ||
| 101 | return; | ||
| 102 | } | ||
| 103 | |||
| 104 | bytes_written += file->WriteString(FormatLogMessage(entry).append(1, '\n')); | ||
| 105 | |||
| 106 | using namespace Common::Literals; | ||
| 107 | // Prevent logs from exceeding a set maximum size in the event that log entries are spammed. | ||
| 108 | const auto write_limit = Settings::values.extended_logging ? 1_GiB : 100_MiB; | ||
| 109 | const bool write_limit_exceeded = bytes_written > write_limit; | ||
| 110 | if (entry.log_level >= Level::Error || write_limit_exceeded) { | ||
| 111 | if (write_limit_exceeded) { | ||
| 112 | // Stop writing after the write limit is exceeded. | ||
| 113 | // Don't close the file so we can print a stacktrace if necessary | ||
| 114 | enabled = false; | ||
| 115 | } | ||
| 116 | file->Flush(); | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | void Flush() override { | ||
| 121 | file->Flush(); | ||
| 122 | } | ||
| 123 | |||
| 124 | void EnableForStacktrace() override { | ||
| 125 | enabled = true; | ||
| 126 | bytes_written = 0; | ||
| 53 | } | 127 | } |
| 54 | 128 | ||
| 55 | void AddBackend(std::unique_ptr<Backend> backend) { | 129 | private: |
| 56 | std::lock_guard lock{writing_mutex}; | 130 | std::unique_ptr<FS::IOFile> file; |
| 57 | backends.push_back(std::move(backend)); | 131 | bool enabled = true; |
| 132 | std::size_t bytes_written = 0; | ||
| 133 | }; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * Backend that writes to Visual Studio's output window | ||
| 137 | */ | ||
| 138 | class DebuggerBackend final : public Backend { | ||
| 139 | public: | ||
| 140 | explicit DebuggerBackend() = default; | ||
| 141 | |||
| 142 | ~DebuggerBackend() override = default; | ||
| 143 | |||
| 144 | void Write(const Entry& entry) override { | ||
| 145 | #ifdef _WIN32 | ||
| 146 | ::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str()); | ||
| 147 | #endif | ||
| 58 | } | 148 | } |
| 59 | 149 | ||
| 60 | void RemoveBackend(std::string_view backend_name) { | 150 | void Flush() override {} |
| 61 | std::lock_guard lock{writing_mutex}; | 151 | |
| 152 | void EnableForStacktrace() override {} | ||
| 153 | }; | ||
| 154 | |||
| 155 | bool initialization_in_progress_suppress_logging = false; | ||
| 62 | 156 | ||
| 63 | std::erase_if(backends, [&backend_name](const auto& backend) { | 157 | /** |
| 64 | return backend_name == backend->GetName(); | 158 | * Static state as a singleton. |
| 65 | }); | 159 | */ |
| 160 | class Impl { | ||
| 161 | public: | ||
| 162 | static Impl& Instance() { | ||
| 163 | if (!instance) { | ||
| 164 | abort(); | ||
| 165 | } | ||
| 166 | return *instance; | ||
| 66 | } | 167 | } |
| 67 | 168 | ||
| 68 | const Filter& GetGlobalFilter() const { | 169 | static void Initialize() { |
| 69 | return filter; | 170 | if (instance) { |
| 171 | abort(); | ||
| 172 | } | ||
| 173 | using namespace Common::FS; | ||
| 174 | initialization_in_progress_suppress_logging = true; | ||
| 175 | const auto& log_dir = GetYuzuPath(YuzuPath::LogDir); | ||
| 176 | void(CreateDir(log_dir)); | ||
| 177 | Filter filter; | ||
| 178 | filter.ParseFilterString(Settings::values.log_filter.GetValue()); | ||
| 179 | instance = std::unique_ptr<Impl, decltype(&Deleter)>(new Impl(log_dir / LOG_FILE, filter), | ||
| 180 | Deleter); | ||
| 181 | initialization_in_progress_suppress_logging = false; | ||
| 70 | } | 182 | } |
| 71 | 183 | ||
| 184 | Impl(const Impl&) = delete; | ||
| 185 | Impl& operator=(const Impl&) = delete; | ||
| 186 | |||
| 187 | Impl(Impl&&) = delete; | ||
| 188 | Impl& operator=(Impl&&) = delete; | ||
| 189 | |||
| 72 | void SetGlobalFilter(const Filter& f) { | 190 | void SetGlobalFilter(const Filter& f) { |
| 73 | filter = f; | 191 | filter = f; |
| 74 | } | 192 | } |
| 75 | 193 | ||
| 76 | Backend* GetBackend(std::string_view backend_name) { | 194 | void SetColorConsoleBackendEnabled(bool enabled) { |
| 77 | const auto it = | 195 | color_console_backend.SetEnabled(enabled); |
| 78 | std::find_if(backends.begin(), backends.end(), | 196 | } |
| 79 | [&backend_name](const auto& i) { return backend_name == i->GetName(); }); | 197 | |
| 80 | if (it == backends.end()) | 198 | void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, |
| 81 | return nullptr; | 199 | const char* function, std::string message) { |
| 82 | return it->get(); | 200 | if (!filter.CheckMessage(log_class, log_level)) |
| 201 | return; | ||
| 202 | const Entry& entry = | ||
| 203 | CreateEntry(log_class, log_level, filename, line_num, function, std::move(message)); | ||
| 204 | message_queue.Push(entry); | ||
| 83 | } | 205 | } |
| 84 | 206 | ||
| 85 | private: | 207 | private: |
| 86 | Impl() { | 208 | Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_) |
| 87 | backend_thread = std::thread([&] { | 209 | : filter{filter_}, file_backend{file_backend_filename}, backend_thread{std::thread([this] { |
| 88 | Entry entry; | 210 | Common::SetCurrentThreadName("yuzu:Log"); |
| 89 | auto write_logs = [&](Entry& e) { | 211 | Entry entry; |
| 90 | std::lock_guard lock{writing_mutex}; | 212 | const auto write_logs = [this, &entry]() { |
| 91 | for (const auto& backend : backends) { | 213 | ForEachBackend([&entry](Backend& backend) { backend.Write(entry); }); |
| 92 | backend->Write(e); | 214 | }; |
| 93 | } | 215 | while (true) { |
| 94 | }; | 216 | entry = message_queue.PopWait(); |
| 95 | while (true) { | 217 | if (entry.final_entry) { |
| 96 | entry = message_queue.PopWait(); | 218 | break; |
| 97 | if (entry.final_entry) { | 219 | } |
| 98 | break; | 220 | write_logs(); |
| 99 | } | 221 | } |
| 100 | write_logs(entry); | 222 | // Drain the logging queue. Only writes out up to MAX_LOGS_TO_WRITE to prevent a |
| 101 | } | 223 | // case where a system is repeatedly spamming logs even on close. |
| 224 | int max_logs_to_write = filter.IsDebug() ? INT_MAX : 100; | ||
| 225 | while (max_logs_to_write-- && message_queue.Pop(entry)) { | ||
| 226 | write_logs(); | ||
| 227 | } | ||
| 228 | })} {} | ||
| 102 | 229 | ||
| 103 | // Drain the logging queue. Only writes out up to MAX_LOGS_TO_WRITE to prevent a | 230 | ~Impl() { |
| 104 | // case where a system is repeatedly spamming logs even on close. | 231 | StopBackendThread(); |
| 105 | const int MAX_LOGS_TO_WRITE = filter.IsDebug() ? INT_MAX : 100; | ||
| 106 | int logs_written = 0; | ||
| 107 | while (logs_written++ < MAX_LOGS_TO_WRITE && message_queue.Pop(entry)) { | ||
| 108 | write_logs(entry); | ||
| 109 | } | ||
| 110 | }); | ||
| 111 | } | 232 | } |
| 112 | 233 | ||
| 113 | ~Impl() { | 234 | void StopBackendThread() { |
| 114 | Entry entry; | 235 | Entry stop_entry{}; |
| 115 | entry.final_entry = true; | 236 | stop_entry.final_entry = true; |
| 116 | message_queue.Push(entry); | 237 | message_queue.Push(stop_entry); |
| 117 | backend_thread.join(); | 238 | backend_thread.join(); |
| 118 | } | 239 | } |
| 119 | 240 | ||
| @@ -135,100 +256,51 @@ private: | |||
| 135 | }; | 256 | }; |
| 136 | } | 257 | } |
| 137 | 258 | ||
| 138 | std::mutex writing_mutex; | 259 | void ForEachBackend(auto lambda) { |
| 139 | std::thread backend_thread; | 260 | lambda(static_cast<Backend&>(debugger_backend)); |
| 140 | std::vector<std::unique_ptr<Backend>> backends; | 261 | lambda(static_cast<Backend&>(color_console_backend)); |
| 141 | MPSCQueue<Entry> message_queue; | 262 | lambda(static_cast<Backend&>(file_backend)); |
| 142 | Filter filter; | 263 | } |
| 143 | std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; | ||
| 144 | }; | ||
| 145 | |||
| 146 | ConsoleBackend::~ConsoleBackend() = default; | ||
| 147 | |||
| 148 | void ConsoleBackend::Write(const Entry& entry) { | ||
| 149 | PrintMessage(entry); | ||
| 150 | } | ||
| 151 | |||
| 152 | ColorConsoleBackend::~ColorConsoleBackend() = default; | ||
| 153 | |||
| 154 | void ColorConsoleBackend::Write(const Entry& entry) { | ||
| 155 | PrintColoredMessage(entry); | ||
| 156 | } | ||
| 157 | |||
| 158 | FileBackend::FileBackend(const std::filesystem::path& filename) { | ||
| 159 | auto old_filename = filename; | ||
| 160 | old_filename += ".old.txt"; | ||
| 161 | |||
| 162 | // Existence checks are done within the functions themselves. | ||
| 163 | // We don't particularly care if these succeed or not. | ||
| 164 | FS::RemoveFile(old_filename); | ||
| 165 | void(FS::RenameFile(filename, old_filename)); | ||
| 166 | |||
| 167 | file = | ||
| 168 | std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, FS::FileType::TextFile); | ||
| 169 | } | ||
| 170 | |||
| 171 | FileBackend::~FileBackend() = default; | ||
| 172 | 264 | ||
| 173 | void FileBackend::Write(const Entry& entry) { | 265 | static void Deleter(Impl* ptr) { |
| 174 | if (!file->IsOpen()) { | 266 | delete ptr; |
| 175 | return; | ||
| 176 | } | 267 | } |
| 177 | 268 | ||
| 178 | using namespace Common::Literals; | 269 | static inline std::unique_ptr<Impl, decltype(&Deleter)> instance{nullptr, Deleter}; |
| 179 | // Prevent logs from exceeding a set maximum size in the event that log entries are spammed. | ||
| 180 | constexpr std::size_t MAX_BYTES_WRITTEN = 100_MiB; | ||
| 181 | constexpr std::size_t MAX_BYTES_WRITTEN_EXTENDED = 1_GiB; | ||
| 182 | 270 | ||
| 183 | const bool write_limit_exceeded = | 271 | Filter filter; |
| 184 | bytes_written > MAX_BYTES_WRITTEN_EXTENDED || | 272 | DebuggerBackend debugger_backend{}; |
| 185 | (bytes_written > MAX_BYTES_WRITTEN && !Settings::values.extended_logging); | 273 | ColorConsoleBackend color_console_backend{}; |
| 274 | FileBackend file_backend; | ||
| 186 | 275 | ||
| 187 | // Close the file after the write limit is exceeded. | 276 | std::thread backend_thread; |
| 188 | if (write_limit_exceeded) { | 277 | MPSCQueue<Entry> message_queue{}; |
| 189 | file->Close(); | 278 | std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; |
| 190 | return; | 279 | }; |
| 191 | } | 280 | } // namespace |
| 192 | 281 | ||
| 193 | bytes_written += file->WriteString(FormatLogMessage(entry).append(1, '\n')); | 282 | void Initialize() { |
| 194 | if (entry.log_level >= Level::Error) { | 283 | Impl::Initialize(); |
| 195 | file->Flush(); | ||
| 196 | } | ||
| 197 | } | 284 | } |
| 198 | 285 | ||
| 199 | DebuggerBackend::~DebuggerBackend() = default; | 286 | void DisableLoggingInTests() { |
| 200 | 287 | initialization_in_progress_suppress_logging = true; | |
| 201 | void DebuggerBackend::Write(const Entry& entry) { | ||
| 202 | #ifdef _WIN32 | ||
| 203 | ::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str()); | ||
| 204 | #endif | ||
| 205 | } | 288 | } |
| 206 | 289 | ||
| 207 | void SetGlobalFilter(const Filter& filter) { | 290 | void SetGlobalFilter(const Filter& filter) { |
| 208 | Impl::Instance().SetGlobalFilter(filter); | 291 | Impl::Instance().SetGlobalFilter(filter); |
| 209 | } | 292 | } |
| 210 | 293 | ||
| 211 | void AddBackend(std::unique_ptr<Backend> backend) { | 294 | void SetColorConsoleBackendEnabled(bool enabled) { |
| 212 | Impl::Instance().AddBackend(std::move(backend)); | 295 | Impl::Instance().SetColorConsoleBackendEnabled(enabled); |
| 213 | } | ||
| 214 | |||
| 215 | void RemoveBackend(std::string_view backend_name) { | ||
| 216 | Impl::Instance().RemoveBackend(backend_name); | ||
| 217 | } | ||
| 218 | |||
| 219 | Backend* GetBackend(std::string_view backend_name) { | ||
| 220 | return Impl::Instance().GetBackend(backend_name); | ||
| 221 | } | 296 | } |
| 222 | 297 | ||
| 223 | void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, | 298 | void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, |
| 224 | unsigned int line_num, const char* function, const char* format, | 299 | unsigned int line_num, const char* function, const char* format, |
| 225 | const fmt::format_args& args) { | 300 | const fmt::format_args& args) { |
| 226 | auto& instance = Impl::Instance(); | 301 | if (!initialization_in_progress_suppress_logging) { |
| 227 | const auto& filter = instance.GetGlobalFilter(); | 302 | Impl::Instance().PushEntry(log_class, log_level, filename, line_num, function, |
| 228 | if (!filter.CheckMessage(log_class, log_level)) | 303 | fmt::vformat(format, args)); |
| 229 | return; | 304 | } |
| 230 | |||
| 231 | instance.PushEntry(log_class, log_level, filename, line_num, function, | ||
| 232 | fmt::vformat(format, args)); | ||
| 233 | } | 305 | } |
| 234 | } // namespace Common::Log | 306 | } // namespace Common::Log |