Skip to content

Commit 5275a05

Browse files
authored
Merge pull request #25 from devscafecommunity/23-debug-tools
feat/debug tools
2 parents 4f36538 + 63f116e commit 5275a05

7 files changed

Lines changed: 803 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ add_library(Caffeine
1616
src/core/Timer.cpp
1717
src/core/GameLoop.cpp
1818
src/input/InputManager.cpp
19+
src/debug/LogSystem.cpp
20+
src/debug/Profiler.cpp
1921
)
2022

2123
target_include_directories(Caffeine PUBLIC

src/debug/LogSystem.cpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#include "LogSystem.hpp"
2+
#include <cstdio>
3+
#include <cstring>
4+
5+
namespace Caffeine::Debug {
6+
7+
LogSystem& LogSystem::instance() {
8+
static LogSystem s;
9+
return s;
10+
}
11+
12+
LogSystem::LogSystem() {}
13+
14+
void LogSystem::log(LogLevel level, const char* category, const char* fmt, ...) {
15+
va_list args;
16+
va_start(args, fmt);
17+
vlog(level, category, fmt, args);
18+
va_end(args);
19+
}
20+
21+
void LogSystem::vlog(LogLevel level, const char* category, const char* fmt, va_list args) {
22+
std::lock_guard<std::mutex> lock(m_mutex);
23+
24+
if (level < m_minLevel) return;
25+
26+
if (category) {
27+
const CategoryEntry* entry = findCategory(category);
28+
if (entry && !entry->enabled) return;
29+
}
30+
31+
char buffer[MAX_MESSAGE_LENGTH];
32+
vsnprintf(buffer, MAX_MESSAGE_LENGTH, fmt, args);
33+
34+
for (usize i = 0; i < m_sinkCount; ++i) {
35+
if (m_sinks[i]) {
36+
m_sinks[i](level, category ? category : "", buffer);
37+
}
38+
}
39+
}
40+
41+
void LogSystem::setLevel(LogLevel minLevel) {
42+
std::lock_guard<std::mutex> lock(m_mutex);
43+
m_minLevel = minLevel;
44+
}
45+
46+
LogLevel LogSystem::getLevel() const {
47+
std::lock_guard<std::mutex> lock(m_mutex);
48+
return m_minLevel;
49+
}
50+
51+
void LogSystem::setCategoryEnabled(const char* category, bool enabled) {
52+
std::lock_guard<std::mutex> lock(m_mutex);
53+
54+
CategoryEntry* entry = findCategory(category);
55+
if (entry) {
56+
entry->enabled = enabled;
57+
return;
58+
}
59+
60+
if (m_categoryCount < MAX_CATEGORIES) {
61+
auto& e = m_categories[m_categoryCount];
62+
strncpy(e.name, category, sizeof(e.name) - 1);
63+
e.name[sizeof(e.name) - 1] = '\0';
64+
e.enabled = enabled;
65+
++m_categoryCount;
66+
}
67+
}
68+
69+
bool LogSystem::isCategoryEnabled(const char* category) const {
70+
std::lock_guard<std::mutex> lock(m_mutex);
71+
const CategoryEntry* entry = findCategory(category);
72+
if (entry) return entry->enabled;
73+
return true;
74+
}
75+
76+
void LogSystem::addSink(SinkFn sink) {
77+
std::lock_guard<std::mutex> lock(m_mutex);
78+
if (m_sinkCount < MAX_SINKS) {
79+
m_sinks[m_sinkCount] = std::move(sink);
80+
++m_sinkCount;
81+
}
82+
}
83+
84+
void LogSystem::clearSinks() {
85+
std::lock_guard<std::mutex> lock(m_mutex);
86+
for (usize i = 0; i < m_sinkCount; ++i) {
87+
m_sinks[i] = nullptr;
88+
}
89+
m_sinkCount = 0;
90+
}
91+
92+
usize LogSystem::sinkCount() const {
93+
std::lock_guard<std::mutex> lock(m_mutex);
94+
return m_sinkCount;
95+
}
96+
97+
const char* LogSystem::levelToString(LogLevel level) {
98+
switch (level) {
99+
case LogLevel::Trace: return "TRACE";
100+
case LogLevel::Info: return "INFO";
101+
case LogLevel::Warn: return "WARN";
102+
case LogLevel::Error: return "ERROR";
103+
case LogLevel::Fatal: return "FATAL";
104+
}
105+
return "UNKNOWN";
106+
}
107+
108+
LogSystem::CategoryEntry* LogSystem::findCategory(const char* category) {
109+
for (usize i = 0; i < m_categoryCount; ++i) {
110+
if (strcmp(m_categories[i].name, category) == 0) {
111+
return &m_categories[i];
112+
}
113+
}
114+
return nullptr;
115+
}
116+
117+
const LogSystem::CategoryEntry* LogSystem::findCategory(const char* category) const {
118+
for (usize i = 0; i < m_categoryCount; ++i) {
119+
if (strcmp(m_categories[i].name, category) == 0) {
120+
return &m_categories[i];
121+
}
122+
}
123+
return nullptr;
124+
}
125+
126+
} // namespace Caffeine::Debug

src/debug/LogSystem.hpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#pragma once
2+
3+
#include "../core/Types.hpp"
4+
5+
#include <cstdarg>
6+
#include <functional>
7+
#include <mutex>
8+
9+
namespace Caffeine::Debug {
10+
11+
enum class LogLevel : u8 {
12+
Trace = 0,
13+
Info,
14+
Warn,
15+
Error,
16+
Fatal
17+
};
18+
19+
class LogSystem {
20+
public:
21+
static LogSystem& instance();
22+
23+
void log(LogLevel level, const char* category, const char* fmt, ...);
24+
void vlog(LogLevel level, const char* category, const char* fmt, va_list args);
25+
26+
void setLevel(LogLevel minLevel);
27+
LogLevel getLevel() const;
28+
29+
void setCategoryEnabled(const char* category, bool enabled);
30+
bool isCategoryEnabled(const char* category) const;
31+
32+
using SinkFn = std::function<void(LogLevel, const char*, const char*)>;
33+
void addSink(SinkFn sink);
34+
void clearSinks();
35+
usize sinkCount() const;
36+
37+
static const char* levelToString(LogLevel level);
38+
39+
private:
40+
LogSystem();
41+
~LogSystem() = default;
42+
LogSystem(const LogSystem&) = delete;
43+
LogSystem& operator=(const LogSystem&) = delete;
44+
45+
struct CategoryEntry {
46+
char name[64];
47+
bool enabled;
48+
};
49+
50+
static constexpr usize MAX_CATEGORIES = 64;
51+
static constexpr usize MAX_SINKS = 16;
52+
static constexpr usize MAX_MESSAGE_LENGTH = 2048;
53+
54+
LogLevel m_minLevel = LogLevel::Trace;
55+
CategoryEntry m_categories[MAX_CATEGORIES]{};
56+
usize m_categoryCount = 0;
57+
SinkFn m_sinks[MAX_SINKS]{};
58+
usize m_sinkCount = 0;
59+
mutable std::mutex m_mutex;
60+
61+
CategoryEntry* findCategory(const char* category);
62+
const CategoryEntry* findCategory(const char* category) const;
63+
};
64+
65+
} // namespace Caffeine::Debug
66+
67+
#ifdef CF_DEBUG
68+
#define CF_TRACE(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Trace, cat, __VA_ARGS__)
69+
#else
70+
#define CF_TRACE(cat, ...) ((void)0)
71+
#endif
72+
73+
#define CF_INFO(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Info, cat, __VA_ARGS__)
74+
#define CF_WARN(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Warn, cat, __VA_ARGS__)
75+
#define CF_ERROR(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Error, cat, __VA_ARGS__)
76+
#define CF_FATAL(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Fatal, cat, __VA_ARGS__)

src/debug/Profiler.cpp

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#include "Profiler.hpp"
2+
#include <cstring>
3+
#include <algorithm>
4+
5+
namespace Caffeine::Debug {
6+
7+
Profiler& Profiler::instance() {
8+
static Profiler s;
9+
return s;
10+
}
11+
12+
void Profiler::beginScope(const char* name) {
13+
if (!m_enabled) return;
14+
15+
InternalScopeData* scope = findOrCreateScope(name);
16+
if (!scope) return;
17+
18+
scope->activeTimer.reset();
19+
scope->activeTimer.start();
20+
}
21+
22+
void Profiler::endScope(const char* name) {
23+
if (!m_enabled) return;
24+
25+
InternalScopeData* scope = findScope(name);
26+
if (!scope) return;
27+
28+
scope->activeTimer.stop();
29+
f64 ms = scope->activeTimer.elapsed().millis();
30+
31+
scope->callCount++;
32+
scope->totalMs += ms;
33+
if (ms < scope->minMs) scope->minMs = ms;
34+
if (ms > scope->maxMs) scope->maxMs = ms;
35+
}
36+
37+
void Profiler::report(Vector<ScopeStats>& out) const {
38+
out.clear();
39+
for (usize i = 0; i < m_scopeCount; ++i) {
40+
const auto& s = m_scopes[i];
41+
ScopeStats stats;
42+
stats.name = s.name;
43+
stats.callCount = s.callCount;
44+
stats.totalMs = s.totalMs;
45+
stats.avgMs = (s.callCount > 0) ? s.totalMs / static_cast<f64>(s.callCount) : 0.0;
46+
stats.minMs = s.minMs;
47+
stats.maxMs = s.maxMs;
48+
out.pushBack(stats);
49+
}
50+
}
51+
52+
void Profiler::reset() {
53+
m_scopeCount = 0;
54+
for (usize i = 0; i < MAX_SCOPES; ++i) {
55+
m_scopes[i] = InternalScopeData{};
56+
}
57+
}
58+
59+
usize Profiler::scopeCount() const {
60+
return m_scopeCount;
61+
}
62+
63+
Profiler::InternalScopeData* Profiler::findScope(const char* name) {
64+
for (usize i = 0; i < m_scopeCount; ++i) {
65+
if (strcmp(m_scopes[i].name, name) == 0) {
66+
return &m_scopes[i];
67+
}
68+
}
69+
return nullptr;
70+
}
71+
72+
const Profiler::InternalScopeData* Profiler::findScope(const char* name) const {
73+
for (usize i = 0; i < m_scopeCount; ++i) {
74+
if (strcmp(m_scopes[i].name, name) == 0) {
75+
return &m_scopes[i];
76+
}
77+
}
78+
return nullptr;
79+
}
80+
81+
Profiler::InternalScopeData* Profiler::findOrCreateScope(const char* name) {
82+
InternalScopeData* existing = findScope(name);
83+
if (existing) return existing;
84+
85+
if (m_scopeCount >= MAX_SCOPES) return nullptr;
86+
87+
auto& scope = m_scopes[m_scopeCount];
88+
scope.name = name;
89+
scope.callCount = 0;
90+
scope.totalMs = 0.0;
91+
scope.minMs = 1e18;
92+
scope.maxMs = 0.0;
93+
++m_scopeCount;
94+
return &scope;
95+
}
96+
97+
ProfileScope::ProfileScope(const char* name) : m_name(name) {
98+
Profiler::instance().beginScope(m_name);
99+
}
100+
101+
ProfileScope::~ProfileScope() {
102+
Profiler::instance().endScope(m_name);
103+
}
104+
105+
} // namespace Caffeine::Debug

src/debug/Profiler.hpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#pragma once
2+
3+
#include "../core/Types.hpp"
4+
#include "../core/Timer.hpp"
5+
#include "../containers/HashMap.hpp"
6+
#include "../containers/Vector.hpp"
7+
8+
namespace Caffeine::Debug {
9+
10+
class Profiler {
11+
public:
12+
static Profiler& instance();
13+
14+
void beginScope(const char* name);
15+
void endScope(const char* name);
16+
17+
struct ScopeStats {
18+
const char* name = nullptr;
19+
u64 callCount = 0;
20+
f64 totalMs = 0.0;
21+
f64 avgMs = 0.0;
22+
f64 minMs = 1e18;
23+
f64 maxMs = 0.0;
24+
};
25+
26+
void report(Vector<ScopeStats>& out) const;
27+
void reset();
28+
29+
void setEnabled(bool enabled) { m_enabled = enabled; }
30+
bool isEnabled() const { return m_enabled; }
31+
32+
usize scopeCount() const;
33+
34+
private:
35+
Profiler() = default;
36+
~Profiler() = default;
37+
Profiler(const Profiler&) = delete;
38+
Profiler& operator=(const Profiler&) = delete;
39+
40+
struct InternalScopeData {
41+
const char* name = nullptr;
42+
u64 callCount = 0;
43+
f64 totalMs = 0.0;
44+
f64 minMs = 1e18;
45+
f64 maxMs = 0.0;
46+
Core::Timer activeTimer;
47+
};
48+
49+
bool m_enabled = true;
50+
51+
static constexpr usize MAX_SCOPES = 256;
52+
InternalScopeData m_scopes[MAX_SCOPES]{};
53+
usize m_scopeCount = 0;
54+
55+
InternalScopeData* findScope(const char* name);
56+
const InternalScopeData* findScope(const char* name) const;
57+
InternalScopeData* findOrCreateScope(const char* name);
58+
};
59+
60+
class ProfileScope {
61+
public:
62+
explicit ProfileScope(const char* name);
63+
~ProfileScope();
64+
65+
private:
66+
const char* m_name;
67+
};
68+
69+
} // namespace Caffeine::Debug
70+
71+
#define CF_PROFILE_SCOPE(name) \
72+
Caffeine::Debug::ProfileScope _cfProfileScope_##__LINE__(name)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ add_executable(CaffeineTest
88
test_timer.cpp
99
test_gameloop.cpp
1010
test_input.cpp
11+
test_debug.cpp
1112
)
1213

1314
target_link_libraries(CaffeineTest PRIVATE Caffeine)

0 commit comments

Comments
 (0)