diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cd2fe9..6f0fcd1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,12 +12,17 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") add_compile_definitions(CF_DEBUG) endif() -add_library(Caffeine INTERFACE) -target_include_directories(Caffeine INTERFACE +add_library(Caffeine + src/core/Timer.cpp +) + +target_include_directories(Caffeine PUBLIC $ $ ) +target_compile_features(Caffeine PUBLIC cxx_std_20) + add_library(Caffeine::Core ALIAS Caffeine) add_subdirectory(tests) \ No newline at end of file diff --git a/src/core/Timer.cpp b/src/core/Timer.cpp new file mode 100644 index 0000000..1eb8609 --- /dev/null +++ b/src/core/Timer.cpp @@ -0,0 +1,76 @@ +#include "Timer.hpp" +#include + +namespace Caffeine::Core { + +static TimePoint getCurrentTimePoint() { + auto now = std::chrono::high_resolution_clock::now(); + auto elapsed = now.time_since_epoch(); + u64 nanos = std::chrono::duration_cast(elapsed).count(); + return TimePoint(nanos); +} + +Timer::Timer() + : m_is_running(false), m_accumulated_ticks(0) { +} + +Timer::~Timer() { +} + +void Timer::start() { + if (!m_is_running) { + m_start_time = getCurrentTimePoint(); + m_is_running = true; + } +} + +void Timer::stop() { + if (m_is_running) { + m_stop_time = getCurrentTimePoint(); + u64 ticks = m_stop_time.ticks - m_start_time.ticks; + m_accumulated_ticks += ticks; + m_is_running = false; + } +} + +void Timer::reset() { + m_accumulated_ticks = 0; + m_start_time.ticks = 0; + m_stop_time.ticks = 0; + m_is_running = false; +} + +Duration Timer::elapsed() const { + if (m_is_running) { + TimePoint current = getCurrentTimePoint(); + u64 ticks = current.ticks - m_start_time.ticks + m_accumulated_ticks; + return Duration::fromNanos(static_cast(ticks)); + } else { + return Duration::fromNanos(static_cast(m_accumulated_ticks)); + } +} + +Duration Timer::tick() { + if (m_is_running) { + TimePoint current = getCurrentTimePoint(); + u64 ticks = current.ticks - m_start_time.ticks; + m_start_time = current; + return Duration::fromNanos(static_cast(ticks)); + } + return Duration::fromSeconds(0.0); +} + +bool Timer::isRunning() const { + return m_is_running; +} + +ScopeTimer::ScopeTimer(const char* name) + : m_name(name) { + m_timer.start(); +} + +ScopeTimer::~ScopeTimer() { + m_timer.stop(); +} + +} diff --git a/src/core/Timer.hpp b/src/core/Timer.hpp new file mode 100644 index 0000000..0225a56 --- /dev/null +++ b/src/core/Timer.hpp @@ -0,0 +1,154 @@ +#pragma once + +#include "../core/Types.hpp" + +namespace Caffeine::Core { + +struct TimePoint { + u64 ticks = 0; + + TimePoint() = default; + explicit TimePoint(u64 t) : ticks(t) {} + + bool operator<(const TimePoint& other) const { + return ticks < other.ticks; + } + + bool operator<=(const TimePoint& other) const { + return ticks <= other.ticks; + } + + bool operator>(const TimePoint& other) const { + return ticks > other.ticks; + } + + bool operator>=(const TimePoint& other) const { + return ticks >= other.ticks; + } + + bool operator==(const TimePoint& other) const { + return ticks == other.ticks; + } + + bool operator!=(const TimePoint& other) const { + return ticks != other.ticks; + } +}; + +struct Duration { + f64 seconds = 0.0; + + Duration() = default; + explicit Duration(f64 s) : seconds(s) {} + + static Duration fromSeconds(f64 s) { + return Duration(s); + } + + static Duration fromMillis(f64 ms) { + return Duration(ms / 1000.0); + } + + static Duration fromMicros(f64 us) { + return Duration(us / 1000000.0); + } + + static Duration fromNanos(f64 ns) { + return Duration(ns / 1000000000.0); + } + + f64 millis() const { + return seconds * 1000.0; + } + + f64 micros() const { + return seconds * 1000000.0; + } + + f64 nanos() const { + return seconds * 1000000000.0; + } + + Duration operator+(const Duration& other) const { + return Duration(seconds + other.seconds); + } + + Duration operator-(const Duration& other) const { + return Duration(seconds - other.seconds); + } + + Duration operator*(f64 scalar) const { + return Duration(seconds * scalar); + } + + friend Duration operator*(f64 scalar, const Duration& d) { + return Duration(scalar * d.seconds); + } + + Duration operator/(f64 scalar) const { + return Duration(seconds / scalar); + } + + bool operator<(const Duration& other) const { + return seconds < other.seconds; + } + + bool operator<=(const Duration& other) const { + return seconds <= other.seconds; + } + + bool operator>(const Duration& other) const { + return seconds > other.seconds; + } + + bool operator>=(const Duration& other) const { + return seconds >= other.seconds; + } + + bool operator==(const Duration& other) const { + const f64 epsilon = 1e-15; + return (seconds - other.seconds) < epsilon && (seconds - other.seconds) > -epsilon; + } + + bool operator!=(const Duration& other) const { + return !(*this == other); + } +}; + +inline Duration operator-(const TimePoint& a, const TimePoint& b) { + u64 tick_diff = (a.ticks > b.ticks) ? (a.ticks - b.ticks) : (b.ticks - a.ticks); + return Duration::fromSeconds(static_cast(tick_diff) / 1000000.0); +} + +class Timer { +public: + Timer(); + ~Timer(); + + void start(); + void stop(); + void reset(); + + Duration elapsed() const; + Duration tick(); + + bool isRunning() const; + +private: + TimePoint m_start_time; + TimePoint m_stop_time; + u64 m_accumulated_ticks; + bool m_is_running; +}; + +class ScopeTimer { +public: + explicit ScopeTimer(const char* name); + ~ScopeTimer(); + +private: + const char* m_name; + Timer m_timer; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7f6c6c8..601a9bb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(CaffeineTest test_containers.cpp test_math.cpp test_core.cpp + test_timer.cpp ) target_link_libraries(CaffeineTest PRIVATE Caffeine) diff --git a/tests/test_timer.cpp b/tests/test_timer.cpp new file mode 100644 index 0000000..3e0f8eb --- /dev/null +++ b/tests/test_timer.cpp @@ -0,0 +1,287 @@ +#include "catch.hpp" +#include +#include +#include "../src/core/Types.hpp" +#include "../src/core/Timer.hpp" + +using namespace Caffeine::Core; + +TEST_CASE("TimePoint - Constructor initializes to zero", "[timer][timepoint]") { + TimePoint tp; + REQUIRE(tp.ticks == 0); +} + +TEST_CASE("TimePoint - Can copy construct", "[timer][timepoint]") { + TimePoint tp1; + tp1.ticks = 1000; + TimePoint tp2(tp1); + REQUIRE(tp2.ticks == 1000); +} + +TEST_CASE("TimePoint - Can assign", "[timer][timepoint]") { + TimePoint tp1; + tp1.ticks = 5000; + TimePoint tp2 = tp1; + REQUIRE(tp2.ticks == 5000); +} + +TEST_CASE("TimePoint - Comparison operators", "[timer][timepoint]") { + TimePoint tp1; + TimePoint tp2; + + tp1.ticks = 100; + tp2.ticks = 200; + + REQUIRE(tp1 < tp2); + REQUIRE_FALSE(tp2 < tp1); + REQUIRE(tp2 > tp1); + REQUIRE_FALSE(tp1 > tp2); + + TimePoint tp3; + tp3.ticks = 100; + REQUIRE(tp1 == tp3); + REQUIRE_FALSE(tp1 != tp3); + REQUIRE(tp1 <= tp3); + REQUIRE(tp1 >= tp3); +} + +TEST_CASE("TimePoint - Subtraction returns Duration", "[timer][timepoint]") { + TimePoint tp1; + TimePoint tp2; + + tp1.ticks = 1000; + tp2.ticks = 3000; + + Duration dur = tp2 - tp1; + REQUIRE(dur.seconds > 0.0); +} + +TEST_CASE("Duration - Constructor initializes to zero", "[timer][duration]") { + Duration d; + REQUIRE(d.seconds == 0.0); +} + +TEST_CASE("Duration - Can construct from seconds", "[timer][duration]") { + Duration d = Duration::fromSeconds(1.5); + REQUIRE_THAT(d.seconds, Catch::Matchers::WithinAbs(1.5, 1e-9)); +} + +TEST_CASE("Duration - Convert to milliseconds", "[timer][duration]") { + Duration d = Duration::fromSeconds(1.0); + REQUIRE_THAT(d.millis(), Catch::Matchers::WithinAbs(1000.0, 1e-6)); +} + +TEST_CASE("Duration - Convert to microseconds", "[timer][duration]") { + Duration d = Duration::fromSeconds(1.0); + REQUIRE_THAT(d.micros(), Catch::Matchers::WithinAbs(1000000.0, 1e-3)); +} + +TEST_CASE("Duration - Convert to nanoseconds", "[timer][duration]") { + Duration d = Duration::fromSeconds(1.0); + REQUIRE_THAT(d.nanos(), Catch::Matchers::WithinAbs(1000000000.0, 1e0)); +} + +TEST_CASE("Duration - Add durations", "[timer][duration]") { + Duration d1 = Duration::fromSeconds(1.0); + Duration d2 = Duration::fromSeconds(2.0); + Duration result = d1 + d2; + REQUIRE_THAT(result.seconds, Catch::Matchers::WithinAbs(3.0, 1e-9)); +} + +TEST_CASE("Duration - Subtract durations", "[timer][duration]") { + Duration d1 = Duration::fromSeconds(5.0); + Duration d2 = Duration::fromSeconds(2.0); + Duration result = d1 - d2; + REQUIRE_THAT(result.seconds, Catch::Matchers::WithinAbs(3.0, 1e-9)); +} + +TEST_CASE("Duration - Multiply by scalar", "[timer][duration]") { + Duration d = Duration::fromSeconds(2.0); + Duration result = d * 3.0; + REQUIRE_THAT(result.seconds, Catch::Matchers::WithinAbs(6.0, 1e-9)); +} + +TEST_CASE("Duration - Divide by scalar", "[timer][duration]") { + Duration d = Duration::fromSeconds(6.0); + Duration result = d / 2.0; + REQUIRE_THAT(result.seconds, Catch::Matchers::WithinAbs(3.0, 1e-9)); +} + +TEST_CASE("Duration - Comparison operators", "[timer][duration]") { + Duration d1 = Duration::fromSeconds(1.0); + Duration d2 = Duration::fromSeconds(2.0); + Duration d3 = Duration::fromSeconds(1.0); + + REQUIRE(d1 < d2); + REQUIRE(d2 > d1); + REQUIRE(d1 == d3); + REQUIRE_FALSE(d1 != d3); + REQUIRE(d1 <= d3); + REQUIRE(d1 >= d3); +} + +TEST_CASE("Duration - Precision is at least microseconds", "[timer][duration]") { + Duration d_micro = Duration::fromSeconds(0.000001); + double recovered_micros = d_micro.micros(); + REQUIRE_THAT(recovered_micros, Catch::Matchers::WithinAbs(1.0, 1e-2)); +} + +TEST_CASE("Timer - Constructor creates stopped timer", "[timer][timer]") { + Timer timer; + REQUIRE_FALSE(timer.isRunning()); +} + +TEST_CASE("Timer - Can start", "[timer][timer]") { + Timer timer; + timer.start(); + REQUIRE(timer.isRunning()); +} + +TEST_CASE("Timer - Can stop", "[timer][timer]") { + Timer timer; + timer.start(); + timer.stop(); + REQUIRE_FALSE(timer.isRunning()); +} + +TEST_CASE("Timer - Can reset when stopped", "[timer][timer]") { + Timer timer; + timer.start(); + timer.stop(); + timer.reset(); + REQUIRE(timer.elapsed().seconds == 0.0); +} + +TEST_CASE("Timer - Elapsed time increases", "[timer][timer]") { + Timer timer; + timer.start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Duration d1 = timer.elapsed(); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Duration d2 = timer.elapsed(); + + REQUIRE(d2.seconds > d1.seconds); +} + +TEST_CASE("Timer - Peek does not reset timer", "[timer][timer]") { + Timer timer; + timer.start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + Duration d1 = timer.elapsed(); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + Duration d2 = timer.elapsed(); + + REQUIRE(d2.seconds > d1.seconds); +} + +TEST_CASE("Timer - Tick resets internal clock", "[timer][timer]") { + Timer timer; + timer.start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Duration ticked = timer.tick(); + REQUIRE(ticked.seconds > 0.0); + + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Duration ticked2 = timer.tick(); + + REQUIRE(ticked2.seconds > 0.0); +} + +TEST_CASE("Timer - Accurate to microseconds", "[timer][timer]") { + Timer timer; + timer.start(); + + std::this_thread::sleep_for(std::chrono::microseconds(1000)); + Duration elapsed = timer.elapsed(); + + REQUIRE(elapsed.millis() > 0.5); +} + +TEST_CASE("Timer - Multiple start-stop cycles", "[timer][timer]") { + Timer timer; + timer.start(); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + timer.stop(); + Duration d1 = timer.elapsed(); + + timer.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + timer.stop(); + Duration d2 = timer.elapsed(); + + REQUIRE(d2.seconds > d1.seconds); +} + +TEST_CASE("ScopeTimer - Can construct", "[timer][scope-timer]") { + { + ScopeTimer scoped("test_scope"); + } + REQUIRE(true); +} + +TEST_CASE("ScopeTimer - Measures block execution time", "[timer][scope-timer]") { + ScopeTimer scoped("sleep_test"); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + REQUIRE(true); +} + +TEST_CASE("ScopeTimer - Multiple nested scopes", "[timer][scope-timer]") { + { + ScopeTimer outer("outer"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + { + ScopeTimer inner("inner"); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + } + REQUIRE(true); +} + +TEST_CASE("Timer integration - Measure complex workload", "[timer][integration]") { + Timer total_timer; + total_timer.start(); + + for (int i = 0; i < 1000; ++i) { + volatile int x = i * i; + (void)x; + } + + Duration elapsed = total_timer.elapsed(); + REQUIRE(elapsed.micros() > 0.0); +} + +TEST_CASE("Timer integration - Multiple concurrent timers", "[timer][integration]") { + Timer timer1; + Timer timer2; + + timer1.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + timer2.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + Duration d1 = timer1.elapsed(); + Duration d2 = timer2.elapsed(); + + REQUIRE(d1.seconds > d2.seconds); +} + +TEST_CASE("Timer integration - Frame timing simulation", "[timer][integration]") { + Timer frame_timer; + double cumulative_ms = 0.0; + + for (int frame = 0; frame < 10; ++frame) { + frame_timer.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Duration frame_time = frame_timer.tick(); + cumulative_ms += frame_time.millis(); + } + + REQUIRE(cumulative_ms > 5.0); +}