From 63f116ed37613bcb7495d4ed68dfdec2325a6e3f Mon Sep 17 00:00:00 2001 From: LyeZinho Date: Tue, 14 Apr 2026 14:16:12 +0100 Subject: [PATCH] feat/debug tools --- CMakeLists.txt | 2 + src/debug/LogSystem.cpp | 126 ++++++++++++ src/debug/LogSystem.hpp | 76 ++++++++ src/debug/Profiler.cpp | 105 ++++++++++ src/debug/Profiler.hpp | 72 +++++++ tests/CMakeLists.txt | 1 + tests/test_debug.cpp | 421 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 803 insertions(+) create mode 100644 src/debug/LogSystem.cpp create mode 100644 src/debug/LogSystem.hpp create mode 100644 src/debug/Profiler.cpp create mode 100644 src/debug/Profiler.hpp create mode 100644 tests/test_debug.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cae31be..de79078 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,8 @@ add_library(Caffeine src/core/Timer.cpp src/core/GameLoop.cpp src/input/InputManager.cpp + src/debug/LogSystem.cpp + src/debug/Profiler.cpp ) target_include_directories(Caffeine PUBLIC diff --git a/src/debug/LogSystem.cpp b/src/debug/LogSystem.cpp new file mode 100644 index 0000000..b9136e5 --- /dev/null +++ b/src/debug/LogSystem.cpp @@ -0,0 +1,126 @@ +#include "LogSystem.hpp" +#include +#include + +namespace Caffeine::Debug { + +LogSystem& LogSystem::instance() { + static LogSystem s; + return s; +} + +LogSystem::LogSystem() {} + +void LogSystem::log(LogLevel level, const char* category, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vlog(level, category, fmt, args); + va_end(args); +} + +void LogSystem::vlog(LogLevel level, const char* category, const char* fmt, va_list args) { + std::lock_guard lock(m_mutex); + + if (level < m_minLevel) return; + + if (category) { + const CategoryEntry* entry = findCategory(category); + if (entry && !entry->enabled) return; + } + + char buffer[MAX_MESSAGE_LENGTH]; + vsnprintf(buffer, MAX_MESSAGE_LENGTH, fmt, args); + + for (usize i = 0; i < m_sinkCount; ++i) { + if (m_sinks[i]) { + m_sinks[i](level, category ? category : "", buffer); + } + } +} + +void LogSystem::setLevel(LogLevel minLevel) { + std::lock_guard lock(m_mutex); + m_minLevel = minLevel; +} + +LogLevel LogSystem::getLevel() const { + std::lock_guard lock(m_mutex); + return m_minLevel; +} + +void LogSystem::setCategoryEnabled(const char* category, bool enabled) { + std::lock_guard lock(m_mutex); + + CategoryEntry* entry = findCategory(category); + if (entry) { + entry->enabled = enabled; + return; + } + + if (m_categoryCount < MAX_CATEGORIES) { + auto& e = m_categories[m_categoryCount]; + strncpy(e.name, category, sizeof(e.name) - 1); + e.name[sizeof(e.name) - 1] = '\0'; + e.enabled = enabled; + ++m_categoryCount; + } +} + +bool LogSystem::isCategoryEnabled(const char* category) const { + std::lock_guard lock(m_mutex); + const CategoryEntry* entry = findCategory(category); + if (entry) return entry->enabled; + return true; +} + +void LogSystem::addSink(SinkFn sink) { + std::lock_guard lock(m_mutex); + if (m_sinkCount < MAX_SINKS) { + m_sinks[m_sinkCount] = std::move(sink); + ++m_sinkCount; + } +} + +void LogSystem::clearSinks() { + std::lock_guard lock(m_mutex); + for (usize i = 0; i < m_sinkCount; ++i) { + m_sinks[i] = nullptr; + } + m_sinkCount = 0; +} + +usize LogSystem::sinkCount() const { + std::lock_guard lock(m_mutex); + return m_sinkCount; +} + +const char* LogSystem::levelToString(LogLevel level) { + switch (level) { + case LogLevel::Trace: return "TRACE"; + case LogLevel::Info: return "INFO"; + case LogLevel::Warn: return "WARN"; + case LogLevel::Error: return "ERROR"; + case LogLevel::Fatal: return "FATAL"; + } + return "UNKNOWN"; +} + +LogSystem::CategoryEntry* LogSystem::findCategory(const char* category) { + for (usize i = 0; i < m_categoryCount; ++i) { + if (strcmp(m_categories[i].name, category) == 0) { + return &m_categories[i]; + } + } + return nullptr; +} + +const LogSystem::CategoryEntry* LogSystem::findCategory(const char* category) const { + for (usize i = 0; i < m_categoryCount; ++i) { + if (strcmp(m_categories[i].name, category) == 0) { + return &m_categories[i]; + } + } + return nullptr; +} + +} // namespace Caffeine::Debug diff --git a/src/debug/LogSystem.hpp b/src/debug/LogSystem.hpp new file mode 100644 index 0000000..ea3bdd5 --- /dev/null +++ b/src/debug/LogSystem.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "../core/Types.hpp" + +#include +#include +#include + +namespace Caffeine::Debug { + +enum class LogLevel : u8 { + Trace = 0, + Info, + Warn, + Error, + Fatal +}; + +class LogSystem { +public: + static LogSystem& instance(); + + void log(LogLevel level, const char* category, const char* fmt, ...); + void vlog(LogLevel level, const char* category, const char* fmt, va_list args); + + void setLevel(LogLevel minLevel); + LogLevel getLevel() const; + + void setCategoryEnabled(const char* category, bool enabled); + bool isCategoryEnabled(const char* category) const; + + using SinkFn = std::function; + void addSink(SinkFn sink); + void clearSinks(); + usize sinkCount() const; + + static const char* levelToString(LogLevel level); + +private: + LogSystem(); + ~LogSystem() = default; + LogSystem(const LogSystem&) = delete; + LogSystem& operator=(const LogSystem&) = delete; + + struct CategoryEntry { + char name[64]; + bool enabled; + }; + + static constexpr usize MAX_CATEGORIES = 64; + static constexpr usize MAX_SINKS = 16; + static constexpr usize MAX_MESSAGE_LENGTH = 2048; + + LogLevel m_minLevel = LogLevel::Trace; + CategoryEntry m_categories[MAX_CATEGORIES]{}; + usize m_categoryCount = 0; + SinkFn m_sinks[MAX_SINKS]{}; + usize m_sinkCount = 0; + mutable std::mutex m_mutex; + + CategoryEntry* findCategory(const char* category); + const CategoryEntry* findCategory(const char* category) const; +}; + +} // namespace Caffeine::Debug + +#ifdef CF_DEBUG + #define CF_TRACE(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Trace, cat, __VA_ARGS__) +#else + #define CF_TRACE(cat, ...) ((void)0) +#endif + +#define CF_INFO(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Info, cat, __VA_ARGS__) +#define CF_WARN(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Warn, cat, __VA_ARGS__) +#define CF_ERROR(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Error, cat, __VA_ARGS__) +#define CF_FATAL(cat, ...) Caffeine::Debug::LogSystem::instance().log(Caffeine::Debug::LogLevel::Fatal, cat, __VA_ARGS__) diff --git a/src/debug/Profiler.cpp b/src/debug/Profiler.cpp new file mode 100644 index 0000000..8062ccf --- /dev/null +++ b/src/debug/Profiler.cpp @@ -0,0 +1,105 @@ +#include "Profiler.hpp" +#include +#include + +namespace Caffeine::Debug { + +Profiler& Profiler::instance() { + static Profiler s; + return s; +} + +void Profiler::beginScope(const char* name) { + if (!m_enabled) return; + + InternalScopeData* scope = findOrCreateScope(name); + if (!scope) return; + + scope->activeTimer.reset(); + scope->activeTimer.start(); +} + +void Profiler::endScope(const char* name) { + if (!m_enabled) return; + + InternalScopeData* scope = findScope(name); + if (!scope) return; + + scope->activeTimer.stop(); + f64 ms = scope->activeTimer.elapsed().millis(); + + scope->callCount++; + scope->totalMs += ms; + if (ms < scope->minMs) scope->minMs = ms; + if (ms > scope->maxMs) scope->maxMs = ms; +} + +void Profiler::report(Vector& out) const { + out.clear(); + for (usize i = 0; i < m_scopeCount; ++i) { + const auto& s = m_scopes[i]; + ScopeStats stats; + stats.name = s.name; + stats.callCount = s.callCount; + stats.totalMs = s.totalMs; + stats.avgMs = (s.callCount > 0) ? s.totalMs / static_cast(s.callCount) : 0.0; + stats.minMs = s.minMs; + stats.maxMs = s.maxMs; + out.pushBack(stats); + } +} + +void Profiler::reset() { + m_scopeCount = 0; + for (usize i = 0; i < MAX_SCOPES; ++i) { + m_scopes[i] = InternalScopeData{}; + } +} + +usize Profiler::scopeCount() const { + return m_scopeCount; +} + +Profiler::InternalScopeData* Profiler::findScope(const char* name) { + for (usize i = 0; i < m_scopeCount; ++i) { + if (strcmp(m_scopes[i].name, name) == 0) { + return &m_scopes[i]; + } + } + return nullptr; +} + +const Profiler::InternalScopeData* Profiler::findScope(const char* name) const { + for (usize i = 0; i < m_scopeCount; ++i) { + if (strcmp(m_scopes[i].name, name) == 0) { + return &m_scopes[i]; + } + } + return nullptr; +} + +Profiler::InternalScopeData* Profiler::findOrCreateScope(const char* name) { + InternalScopeData* existing = findScope(name); + if (existing) return existing; + + if (m_scopeCount >= MAX_SCOPES) return nullptr; + + auto& scope = m_scopes[m_scopeCount]; + scope.name = name; + scope.callCount = 0; + scope.totalMs = 0.0; + scope.minMs = 1e18; + scope.maxMs = 0.0; + ++m_scopeCount; + return &scope; +} + +ProfileScope::ProfileScope(const char* name) : m_name(name) { + Profiler::instance().beginScope(m_name); +} + +ProfileScope::~ProfileScope() { + Profiler::instance().endScope(m_name); +} + +} // namespace Caffeine::Debug diff --git a/src/debug/Profiler.hpp b/src/debug/Profiler.hpp new file mode 100644 index 0000000..59e3a4f --- /dev/null +++ b/src/debug/Profiler.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include "../core/Types.hpp" +#include "../core/Timer.hpp" +#include "../containers/HashMap.hpp" +#include "../containers/Vector.hpp" + +namespace Caffeine::Debug { + +class Profiler { +public: + static Profiler& instance(); + + void beginScope(const char* name); + void endScope(const char* name); + + struct ScopeStats { + const char* name = nullptr; + u64 callCount = 0; + f64 totalMs = 0.0; + f64 avgMs = 0.0; + f64 minMs = 1e18; + f64 maxMs = 0.0; + }; + + void report(Vector& out) const; + void reset(); + + void setEnabled(bool enabled) { m_enabled = enabled; } + bool isEnabled() const { return m_enabled; } + + usize scopeCount() const; + +private: + Profiler() = default; + ~Profiler() = default; + Profiler(const Profiler&) = delete; + Profiler& operator=(const Profiler&) = delete; + + struct InternalScopeData { + const char* name = nullptr; + u64 callCount = 0; + f64 totalMs = 0.0; + f64 minMs = 1e18; + f64 maxMs = 0.0; + Core::Timer activeTimer; + }; + + bool m_enabled = true; + + static constexpr usize MAX_SCOPES = 256; + InternalScopeData m_scopes[MAX_SCOPES]{}; + usize m_scopeCount = 0; + + InternalScopeData* findScope(const char* name); + const InternalScopeData* findScope(const char* name) const; + InternalScopeData* findOrCreateScope(const char* name); +}; + +class ProfileScope { +public: + explicit ProfileScope(const char* name); + ~ProfileScope(); + +private: + const char* m_name; +}; + +} // namespace Caffeine::Debug + +#define CF_PROFILE_SCOPE(name) \ + Caffeine::Debug::ProfileScope _cfProfileScope_##__LINE__(name) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 47c1920..e9e5eff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(CaffeineTest test_timer.cpp test_gameloop.cpp test_input.cpp + test_debug.cpp ) target_link_libraries(CaffeineTest PRIVATE Caffeine) diff --git a/tests/test_debug.cpp b/tests/test_debug.cpp new file mode 100644 index 0000000..15f957d --- /dev/null +++ b/tests/test_debug.cpp @@ -0,0 +1,421 @@ +#include "catch.hpp" +#include +#include +#include +#include +#include "../src/debug/LogSystem.hpp" +#include "../src/debug/Profiler.hpp" + +using namespace Caffeine; +using namespace Caffeine::Debug; + +// ============================================================================ +// LogSystem — LogLevel enum +// ============================================================================ + +TEST_CASE("LogLevel - Trace is lowest", "[debug][log]") { + REQUIRE(static_cast(LogLevel::Trace) < static_cast(LogLevel::Info)); + REQUIRE(static_cast(LogLevel::Info) < static_cast(LogLevel::Warn)); + REQUIRE(static_cast(LogLevel::Warn) < static_cast(LogLevel::Error)); + REQUIRE(static_cast(LogLevel::Error) < static_cast(LogLevel::Fatal)); +} + +TEST_CASE("LogLevel - levelToString returns correct names", "[debug][log]") { + REQUIRE(std::string(LogSystem::levelToString(LogLevel::Trace)) == "TRACE"); + REQUIRE(std::string(LogSystem::levelToString(LogLevel::Info)) == "INFO"); + REQUIRE(std::string(LogSystem::levelToString(LogLevel::Warn)) == "WARN"); + REQUIRE(std::string(LogSystem::levelToString(LogLevel::Error)) == "ERROR"); + REQUIRE(std::string(LogSystem::levelToString(LogLevel::Fatal)) == "FATAL"); +} + +// ============================================================================ +// LogSystem — Singleton +// ============================================================================ + +TEST_CASE("LogSystem - instance returns same object", "[debug][log]") { + auto& a = LogSystem::instance(); + auto& b = LogSystem::instance(); + REQUIRE(&a == &b); +} + +// ============================================================================ +// LogSystem — Level filtering +// ============================================================================ + +TEST_CASE("LogSystem - setLevel filters lower levels", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + int callCount = 0; + log.addSink([&](LogLevel, const char*, const char*) { ++callCount; }); + + log.setLevel(LogLevel::Warn); + log.log(LogLevel::Trace, "Test", "should be filtered"); + log.log(LogLevel::Info, "Test", "should be filtered"); + log.log(LogLevel::Warn, "Test", "should pass"); + log.log(LogLevel::Error, "Test", "should pass"); + + REQUIRE(callCount == 2); + + log.clearSinks(); + log.setLevel(LogLevel::Trace); +} + +TEST_CASE("LogSystem - getLevel returns current minimum level", "[debug][log]") { + auto& log = LogSystem::instance(); + log.setLevel(LogLevel::Error); + REQUIRE(log.getLevel() == LogLevel::Error); + log.setLevel(LogLevel::Trace); +} + +// ============================================================================ +// LogSystem — Sinks +// ============================================================================ + +TEST_CASE("LogSystem - addSink receives formatted message", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + LogLevel receivedLevel = LogLevel::Trace; + std::string receivedCategory; + std::string receivedMessage; + + log.addSink([&](LogLevel level, const char* cat, const char* msg) { + receivedLevel = level; + receivedCategory = cat; + receivedMessage = msg; + }); + + log.log(LogLevel::Info, "Physics", "velocity = %d", 42); + + REQUIRE(receivedLevel == LogLevel::Info); + REQUIRE(receivedCategory == "Physics"); + REQUIRE(receivedMessage == "velocity = 42"); + + log.clearSinks(); +} + +TEST_CASE("LogSystem - multiple sinks all receive messages", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + int sink1Count = 0; + int sink2Count = 0; + log.addSink([&](LogLevel, const char*, const char*) { ++sink1Count; }); + log.addSink([&](LogLevel, const char*, const char*) { ++sink2Count; }); + + log.log(LogLevel::Info, "Test", "hello"); + + REQUIRE(sink1Count == 1); + REQUIRE(sink2Count == 1); + + log.clearSinks(); +} + +TEST_CASE("LogSystem - sinkCount tracks added sinks", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + REQUIRE(log.sinkCount() == 0); + + log.addSink([](LogLevel, const char*, const char*) {}); + REQUIRE(log.sinkCount() == 1); + + log.addSink([](LogLevel, const char*, const char*) {}); + REQUIRE(log.sinkCount() == 2); + + log.clearSinks(); + REQUIRE(log.sinkCount() == 0); +} + +// ============================================================================ +// LogSystem — Category filtering +// ============================================================================ + +TEST_CASE("LogSystem - category enabled by default", "[debug][log]") { + auto& log = LogSystem::instance(); + REQUIRE(log.isCategoryEnabled("NewCategory") == true); +} + +TEST_CASE("LogSystem - setCategoryEnabled disables category", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + int callCount = 0; + log.addSink([&](LogLevel, const char*, const char*) { ++callCount; }); + + log.setCategoryEnabled("Physics", false); + log.log(LogLevel::Info, "Physics", "should be filtered"); + log.log(LogLevel::Info, "Audio", "should pass"); + + REQUIRE(callCount == 1); + + log.setCategoryEnabled("Physics", true); + log.clearSinks(); +} + +TEST_CASE("LogSystem - re-enable category works", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + int callCount = 0; + log.addSink([&](LogLevel, const char*, const char*) { ++callCount; }); + + log.setCategoryEnabled("ECS", false); + log.log(LogLevel::Info, "ECS", "filtered"); + REQUIRE(callCount == 0); + + log.setCategoryEnabled("ECS", true); + log.log(LogLevel::Info, "ECS", "passes now"); + REQUIRE(callCount == 1); + + log.clearSinks(); +} + +// ============================================================================ +// LogSystem — Format strings +// ============================================================================ + +TEST_CASE("LogSystem - format string with multiple args", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + std::string msg; + log.addSink([&](LogLevel, const char*, const char* m) { msg = m; }); + + log.log(LogLevel::Info, "Test", "x=%d y=%.1f name=%s", 10, 3.14, "hello"); + + REQUIRE(msg == "x=10 y=3.1 name=hello"); + + log.clearSinks(); +} + +// ============================================================================ +// LogSystem — Thread safety +// ============================================================================ + +TEST_CASE("LogSystem - concurrent logging does not crash", "[debug][log]") { + auto& log = LogSystem::instance(); + log.clearSinks(); + log.setLevel(LogLevel::Trace); + + int callCount = 0; + std::mutex countMutex; + log.addSink([&](LogLevel, const char*, const char*) { + std::lock_guard lock(countMutex); + ++callCount; + }); + + constexpr int THREADS = 4; + constexpr int MESSAGES_PER_THREAD = 100; + + std::vector threads; + for (int t = 0; t < THREADS; ++t) { + threads.emplace_back([&, t]() { + for (int i = 0; i < MESSAGES_PER_THREAD; ++i) { + log.log(LogLevel::Info, "Thread", "msg %d from thread %d", i, t); + } + }); + } + + for (auto& th : threads) th.join(); + + REQUIRE(callCount == THREADS * MESSAGES_PER_THREAD); + + log.clearSinks(); +} + +// ============================================================================ +// Profiler — Singleton +// ============================================================================ + +TEST_CASE("Profiler - instance returns same object", "[debug][profiler]") { + auto& a = Profiler::instance(); + auto& b = Profiler::instance(); + REQUIRE(&a == &b); +} + +// ============================================================================ +// Profiler — Scope measurement +// ============================================================================ + +TEST_CASE("Profiler - beginScope/endScope records a call", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + prof.beginScope("TestScope"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + prof.endScope("TestScope"); + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 1); + REQUIRE(std::string(stats[0].name) == "TestScope"); + REQUIRE(stats[0].callCount == 1); + REQUIRE(stats[0].totalMs > 0.0); +} + +TEST_CASE("Profiler - multiple calls accumulate stats", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + for (int i = 0; i < 3; ++i) { + prof.beginScope("Loop"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + prof.endScope("Loop"); + } + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 1); + REQUIRE(stats[0].callCount == 3); + REQUIRE(stats[0].avgMs > 0.0); + REQUIRE(stats[0].totalMs >= stats[0].avgMs); +} + +TEST_CASE("Profiler - min/max tracked correctly", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + prof.beginScope("MinMax"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + prof.endScope("MinMax"); + + prof.beginScope("MinMax"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + prof.endScope("MinMax"); + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 1); + REQUIRE(stats[0].minMs < stats[0].maxMs); + REQUIRE(stats[0].minMs > 0.0); +} + +TEST_CASE("Profiler - multiple named scopes are independent", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + prof.beginScope("ScopeA"); + prof.endScope("ScopeA"); + + prof.beginScope("ScopeB"); + prof.endScope("ScopeB"); + + prof.beginScope("ScopeA"); + prof.endScope("ScopeA"); + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 2); + + bool foundA = false, foundB = false; + for (usize i = 0; i < stats.size(); ++i) { + if (std::string(stats[i].name) == "ScopeA") { + REQUIRE(stats[i].callCount == 2); + foundA = true; + } + if (std::string(stats[i].name) == "ScopeB") { + REQUIRE(stats[i].callCount == 1); + foundB = true; + } + } + REQUIRE(foundA); + REQUIRE(foundB); +} + +// ============================================================================ +// Profiler — Enable/disable +// ============================================================================ + +TEST_CASE("Profiler - disabled profiler does not record", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(false); + + prof.beginScope("Disabled"); + prof.endScope("Disabled"); + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 0); + + prof.setEnabled(true); +} + +TEST_CASE("Profiler - reset clears all stats", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.setEnabled(true); + + prof.beginScope("WillBeCleared"); + prof.endScope("WillBeCleared"); + REQUIRE(prof.scopeCount() > 0); + + prof.reset(); + REQUIRE(prof.scopeCount() == 0); + + Vector stats; + prof.report(stats); + REQUIRE(stats.size() == 0); +} + +// ============================================================================ +// ProfileScope — RAII +// ============================================================================ + +TEST_CASE("ProfileScope - RAII records scope", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + { + ProfileScope scope("RAIIScope"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + Vector stats; + prof.report(stats); + + REQUIRE(stats.size() == 1); + REQUIRE(std::string(stats[0].name) == "RAIIScope"); + REQUIRE(stats[0].callCount == 1); + REQUIRE(stats[0].totalMs > 0.0); +} + +// ============================================================================ +// Profiler — scopeCount +// ============================================================================ + +TEST_CASE("Profiler - scopeCount tracks unique scopes", "[debug][profiler]") { + auto& prof = Profiler::instance(); + prof.reset(); + prof.setEnabled(true); + + REQUIRE(prof.scopeCount() == 0); + + prof.beginScope("One"); + prof.endScope("One"); + REQUIRE(prof.scopeCount() == 1); + + prof.beginScope("Two"); + prof.endScope("Two"); + REQUIRE(prof.scopeCount() == 2); + + prof.beginScope("One"); + prof.endScope("One"); + REQUIRE(prof.scopeCount() == 2); +}