feat: 实现日志与调试系统(Debug模块)

- LogLevel: 日志级别枚举 (Verbose, Debug, Info, Warning, Error, Fatal)
- LogCategory: 日志分类 (General, Rendering, Physics, Memory, Threading等)
- ILogSink: 日志输出接口
- ConsoleLogSink: 控制台输出, 支持Windows颜色
- FileLogSink: 文件日志输出
- FileWriter: 文件写入器
- Logger: 日志管理器, 支持多sink, 分类控制
- Profiler: 性能分析器
- 单元测试覆盖
This commit is contained in:
2026-03-13 20:53:57 +08:00
parent dc9b0751cb
commit 83fd517974
22 changed files with 780 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
#include "Core/FileWriter.h"
#include <cstring>
namespace XCEngine {
namespace Core {
FileWriter::FileWriter() = default;
FileWriter::FileWriter(const char* filePath, bool append) {
Open(filePath, append);
}
FileWriter::~FileWriter() {
Close();
}
bool FileWriter::Open(const char* filePath, bool append) {
Close();
const char* mode = append ? "ab" : "wb";
m_file = std::fopen(filePath, mode);
return m_file != nullptr;
}
void FileWriter::Close() {
if (m_file) {
std::fclose(m_file);
m_file = nullptr;
}
}
bool FileWriter::Write(const char* data, size_t length) {
if (!m_file || !data || length == 0) {
return false;
}
return std::fwrite(data, 1, length, m_file) == length;
}
bool FileWriter::Write(const Containers::String& str) {
return Write(str.CStr(), str.Length());
}
bool FileWriter::Flush() {
if (!m_file) {
return false;
}
return std::fflush(m_file) == 0;
}
} // namespace Core
} // namespace XCEngine

View File

@@ -0,0 +1,59 @@
#include "Debug/ConsoleLogSink.h"
#ifdef _WIN32
#include <windows.h>
#endif
namespace XCEngine {
namespace Debug {
ConsoleLogSink::ConsoleLogSink() = default;
ConsoleLogSink::~ConsoleLogSink() = default;
void ConsoleLogSink::Log(const LogEntry& entry) {
if (entry.level < m_minimumLevel) {
return;
}
#ifdef _WIN32
if (m_colorOutput) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
switch (entry.level) {
case LogLevel::Verbose: SetConsoleTextAttribute(hConsole, FOREGROUND_INTENSITY); break;
case LogLevel::Debug: SetConsoleTextAttribute(hConsole, FOREGROUND_BLUE | FOREGROUND_GREEN); break;
case LogLevel::Info: SetConsoleTextAttribute(hConsole, FOREGROUND_GREEN); break;
case LogLevel::Warning: SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_GREEN); break;
case LogLevel::Error: SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_INTENSITY); break;
case LogLevel::Fatal: SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY); break;
}
}
#endif
printf("[%s] [%s] %s\n",
LogLevelToString(entry.level),
LogCategoryToString(entry.category),
entry.message.CStr());
#ifdef _WIN32
if (m_colorOutput) {
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
}
#endif
}
void ConsoleLogSink::Flush() {
fflush(stdout);
}
void ConsoleLogSink::SetColorOutput(bool enable) {
m_colorOutput = enable;
}
void ConsoleLogSink::SetMinimumLevel(LogLevel level) {
m_minimumLevel = level;
}
} // namespace Debug
} // namespace XCEngine

View File

@@ -0,0 +1,40 @@
#include "Debug/FileLogSink.h"
#include <ctime>
namespace XCEngine {
namespace Debug {
FileLogSink::FileLogSink(const Containers::String& filePath)
: m_filePath(filePath) {
m_writer.Open(filePath.CStr(), true);
}
FileLogSink::~FileLogSink() {
m_writer.Close();
}
void FileLogSink::Log(const LogEntry& entry) {
if (!m_writer.IsOpen()) {
return;
}
char timestamp[32];
std::time_t time = static_cast<std::time_t>(entry.timestamp);
std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", std::localtime(&time));
char buffer[1024];
std::snprintf(buffer, sizeof(buffer), "[%s] [%s] [%s] %s\n",
timestamp,
LogLevelToString(entry.level),
LogCategoryToString(entry.category),
entry.message.CStr());
m_writer.Write(buffer, std::strlen(buffer));
}
void FileLogSink::Flush() {
m_writer.Flush();
}
} // namespace Debug
} // namespace XCEngine

View File

@@ -0,0 +1,23 @@
#include "Debug/LogCategory.h"
namespace XCEngine {
namespace Debug {
const char* LogCategoryToString(LogCategory category) {
switch (category) {
case LogCategory::General: return "General";
case LogCategory::Rendering: return "Rendering";
case LogCategory::Physics: return "Physics";
case LogCategory::Audio: return "Audio";
case LogCategory::Scripting: return "Scripting";
case LogCategory::Network: return "Network";
case LogCategory::Memory: return "Memory";
case LogCategory::Threading: return "Threading";
case LogCategory::FileSystem: return "FileSystem";
case LogCategory::Custom: return "Custom";
default: return "Unknown";
}
}
} // namespace Debug
} // namespace XCEngine

View File

@@ -0,0 +1,19 @@
#include "Debug/LogLevel.h"
namespace XCEngine {
namespace Debug {
const char* LogLevelToString(LogLevel level) {
switch (level) {
case LogLevel::Verbose: return "VERBOSE";
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO";
case LogLevel::Warning: return "WARNING";
case LogLevel::Error: return "ERROR";
case LogLevel::Fatal: return "FATAL";
default: return "UNKNOWN";
}
}
} // namespace Debug
} // namespace XCEngine

109
engine/src/Debug/Logger.cpp Normal file
View File

@@ -0,0 +1,109 @@
#include "Debug/Logger.h"
#include "Debug/ConsoleLogSink.h"
#include <chrono>
namespace XCEngine {
namespace Debug {
Logger& Logger::Get() {
static Logger instance;
return instance;
}
void Logger::Initialize() {
if (m_initialized) {
return;
}
m_initialized = true;
}
void Logger::Shutdown() {
for (auto& sink : m_sinks) {
sink->Flush();
}
m_sinks.clear();
m_initialized = false;
}
void Logger::AddSink(std::unique_ptr<ILogSink> sink) {
std::lock_guard<Threading::Mutex> lock(m_mutex);
m_sinks.push_back(std::move(sink));
}
void Logger::RemoveSink(ILogSink* sink) {
std::lock_guard<Threading::Mutex> lock(m_mutex);
for (auto it = m_sinks.begin(); it != m_sinks.end(); ++it) {
if (it->get() == sink) {
m_sinks.erase(it);
break;
}
}
}
void Logger::Log(LogLevel level, LogCategory category,
const Containers::String& message, const char* file,
int32_t line, const char* function) {
if (!m_initialized) {
Initialize();
}
if (level < m_minimumLevel) {
return;
}
int categoryIndex = static_cast<int>(category);
if (categoryIndex < 0 || categoryIndex > 10 || !m_categoryEnabled[categoryIndex]) {
return;
}
LogEntry entry;
entry.level = level;
entry.category = category;
entry.message = message;
entry.file = file ? file : "";
entry.line = line;
entry.function = function ? function : "";
entry.timestamp = std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
entry.threadId = static_cast<uint32_t>(std::hash<std::thread::id>{}(std::this_thread::get_id()));
std::lock_guard<Threading::Mutex> lock(m_mutex);
for (auto& sink : m_sinks) {
sink->Log(entry);
}
}
void Logger::Verbose(LogCategory category, const Containers::String& message) {
Log(LogLevel::Verbose, category, message);
}
void Logger::Debug(LogCategory category, const Containers::String& message) {
Log(LogLevel::Debug, category, message);
}
void Logger::Info(LogCategory category, const Containers::String& message) {
Log(LogLevel::Info, category, message);
}
void Logger::Warning(LogCategory category, const Containers::String& message) {
Log(LogLevel::Warning, category, message);
}
void Logger::Error(LogCategory category, const Containers::String& message) {
Log(LogLevel::Error, category, message);
}
void Logger::Fatal(LogCategory category, const Containers::String& message) {
Log(LogLevel::Fatal, category, message);
}
void Logger::SetMinimumLevel(LogLevel level) {
m_minimumLevel = level;
}
void Logger::SetCategoryEnabled(LogCategory category, bool enabled) {
m_categoryEnabled[static_cast<int>(category)] = enabled;
}
} // namespace Debug
} // namespace XCEngine

View File

@@ -0,0 +1,73 @@
#include "Debug/Profiler.h"
#include "Debug/Logger.h"
namespace XCEngine {
namespace Debug {
Profiler& Profiler::Get() {
static Profiler instance;
return instance;
}
void Profiler::Initialize() {
if (m_initialized) {
return;
}
m_initialized = true;
}
void Profiler::Shutdown() {
m_samples.clear();
m_profileStack.clear();
m_initialized = false;
}
void Profiler::BeginProfile(const char* name) {
ProfileNode node;
node.name = name;
node.startTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
node.threadId = static_cast<uint32_t>(std::hash<std::thread::id>{}(std::this_thread::get_id()));
m_profileStack.push_back(node);
}
void Profiler::EndProfile() {
if (m_profileStack.empty()) {
return;
}
uint64_t endTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
ProfileNode node = m_profileStack.back();
m_profileStack.pop_back();
node.endTime = endTime;
ProfileSample sample;
sample.name = node.name;
sample.duration = node.endTime - node.startTime;
sample.threadId = node.threadId;
m_samples.push_back(sample);
}
void Profiler::BeginFrame() {
m_frameStartTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now().time_since_epoch()).count();
}
void Profiler::EndFrame() {
m_samples.clear();
}
void Profiler::MarkEvent(const char* name, uint64_t timestamp, uint32_t threadId) {
}
void Profiler::SetMarker(const char* name, uint32_t color) {
}
void Profiler::ExportChromeTracing(const Containers::String& filePath) {
}
} // namespace Debug
} // namespace XCEngine