Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/shambase/include/shambase/Timer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// -------------------------------------------------------//
//
// SHAMROCK code for hydrodynamics
// Copyright (c) 2021-2026 Timothée David--Cléris <tim.shamrock@proton.me>
// SPDX-License-Identifier: CeCILL Free Software License Agreement v2.1
// Shamrock is licensed under the CeCILL 2.1 License, see LICENSE for more information
//
// -------------------------------------------------------//

#pragma once

/**
* @file Timer.hpp
* @author Timothée David--Cléris (tim.shamrock@proton.me)
* @brief High-resolution timer with plf_nanotimer (non-macOS) or std::chrono (macOS) backend
*
*/

#include "shambase/aliases_float.hpp"
#include "shambase/format_time.hpp"

#ifndef __MACH__
#define USE_PLF_TIMER
#endif

#if defined(USE_PLF_TIMER)
#include <plf_nanotimer.h>
#else
#include <chrono>
#endif

namespace shambase {
/**
* @brief Class Timer measures the time elapsed since the timer was started.
*/
class Timer {
public:
#if defined(USE_PLF_TIMER)
plf::nanotimer timer; ///< Internal timer
#else
std::chrono::steady_clock::time_point t_start; ///< Internal timer
#endif
f64 nanosec; ///< Time in nanoseconds
Comment on lines +37 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The internal state of the Timer class (the backend timer/time_point and the nanosec accumulator) is currently exposed as public members. To improve encapsulation and prevent accidental modification of the timer's state from outside the class, these should be made private.

Suggested change
public:
#if defined(USE_PLF_TIMER)
plf::nanotimer timer; ///< Internal timer
#else
std::chrono::steady_clock::time_point t_start; ///< Internal timer
#endif
f64 nanosec; ///< Time in nanoseconds
private:
#if defined(USE_PLF_TIMER)
plf::nanotimer timer; ///< Internal timer
#else
std::chrono::steady_clock::time_point t_start; ///< Internal timer
#endif
f64 nanosec; ///< Time in nanoseconds
public:


/// Constructor, init nanosec to 0
Timer() : nanosec(0.0) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When using the std::chrono backend, t_start is not initialized in the constructor. If stop() is called before start(), the calculated duration will be relative to the clock's epoch, resulting in a very large and incorrect value. It is safer to initialize t_start to the current time or ensure nanosec remains 0 if the timer hasn't been started.

Suggested change
Timer() : nanosec(0.0) {}
Timer() : nanosec(0.0) {
#if !defined(USE_PLF_TIMER)
t_start = std::chrono::steady_clock::now();
#endif
}


/**
* @brief Starts the timer.
*/
inline void start() {
#if defined(USE_PLF_TIMER)
timer.start();
#else
t_start = std::chrono::steady_clock::now();
#endif
}

/**
* @brief Stops the timer and stores the elapsed time in nanoseconds.
*
* If the timer has already been stopped, calling this again updates `nanosec` to
* the new delta since `start()`.
*/
inline void stop() {
#if defined(USE_PLF_TIMER)
nanosec = f64(timer.get_elapsed_ns());
#else
auto t_end = std::chrono::steady_clock::now();
nanosec = f64(
std::chrono::duration_cast<std::chrono::nanoseconds>(t_end - t_start).count());
#endif
}

/**
* @brief Converts the stored nanosecond time to a string representation.
* @return std::string A string representation of the elapsed time.
*/
inline std::string get_time_str() const { return nanosec_to_time_str(nanosec); }

/**
* @brief Converts the stored nanosecond time to a floating point representation in seconds.
* @return f64 The elapsed time in seconds.
*/
[[nodiscard]] inline f64 elapsed_sec() const { return nanosec * 1e-9; }
};
} // namespace shambase
66 changes: 66 additions & 0 deletions src/shambase/include/shambase/format_time.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// -------------------------------------------------------//
//
// SHAMROCK code for hydrodynamics
// Copyright (c) 2021-2026 Timothée David--Cléris <tim.shamrock@proton.me>
// SPDX-License-Identifier: CeCILL Free Software License Agreement v2.1
// Shamrock is licensed under the CeCILL 2.1 License, see LICENSE for more information
//
// -------------------------------------------------------//

#pragma once

/**
* @file format_time.hpp
* @author Timothée David--Cléris (tim.shamrock@proton.me)
* @brief Human-readable nanosecond duration formatting
*
*/

#include "shambase/string.hpp"

namespace shambase {

/**
* @brief Convert nanoseconds to a human-readable string representation.
*
* @param nanosec The duration in nanoseconds.
* @return std::string The duration in a human-readable format.
*/
inline std::string nanosec_to_time_str(double nanosec) {
double sec_int = nanosec;

std::string unit = "ns";

if (sec_int > 2000) {
unit = "us";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "ms";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "s";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "ks";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "Ms";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "Gs";
sec_int /= 1000;
}

return shambase::format("{:.2f} {}", sec_int, unit);
}
} // namespace shambase
108 changes: 1 addition & 107 deletions src/shambase/include/shambase/time.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,118 +16,12 @@
*
*/

#include "shambase/Timer.hpp"
#include "shambase/aliases_float.hpp"
#include "shambase/string.hpp"
#include <functional>
#include <iostream>

#ifndef __MACH__
#include <plf_nanotimer.h>
#else
#include <chrono>
#endif

namespace shambase {

/**
* @brief Convert nanoseconds to a human-readable string representation.
*
* @param nanosec The duration in nanoseconds.
* @return std::string The duration in a human-readable format.
*/
inline std::string nanosec_to_time_str(double nanosec) {
double sec_int = nanosec;

std::string unit = "ns";

if (sec_int > 2000) {
unit = "us";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "ms";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "s";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "ks";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "Ms";
sec_int /= 1000;
}

if (sec_int > 2000) {
unit = "Gs";
sec_int /= 1000;
}

return shambase::format_printf("%4.2f", sec_int) + " " + unit;
}

/**
* @brief Class Timer measures the time elapsed since the timer was started.
*/
class Timer {
public:
#if defined(__MACH__)
std::chrono::steady_clock::time_point t_start; ///< Internal timer
#else
plf::nanotimer timer; ///< Internal timer
#endif
f64 nanosec; ///< Time in nanoseconds

/// Constructor, init nanosec to 0
Timer() : nanosec(0.0) {}

/**
* @brief Starts the timer.
*/
inline void start() {
#if defined(__MACH__)
t_start = std::chrono::steady_clock::now();
#else
timer.start();
#endif
}

/**
* @brief Stops the timer and stores the elapsed time in nanoseconds.
*
* If the timer has already been stopped, calling this again updates `nanosec` to
* the new delta since `start()`.
*/
inline void stop() {
#if defined(__MACH__)
auto t_end = std::chrono::steady_clock::now();
nanosec = f64(
std::chrono::duration_cast<std::chrono::nanoseconds>(t_end - t_start).count());
#else
nanosec = f64(timer.get_elapsed_ns());
#endif
}

/**
* @brief Converts the stored nanosecond time to a string representation.
* @return std::string A string representation of the elapsed time.
*/
inline std::string get_time_str() const { return nanosec_to_time_str(nanosec); }

/**
* @brief Converts the stored nanosecond time to a floating point representation in seconds.
* @return f64 The elapsed time in seconds.
*/
[[nodiscard]] inline f64 elapsed_sec() const { return nanosec * 1e-9; }
};

/**
* @brief Class FunctionTimer measures the time it takes to execute a function.
*
Expand Down
100 changes: 100 additions & 0 deletions src/tests/shambase/timeTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// -------------------------------------------------------//
//
// SHAMROCK code for hydrodynamics
// Copyright (c) 2021-2026 Timothée David--Cléris <tim.shamrock@proton.me>
// SPDX-License-Identifier: CeCILL Free Software License Agreement v2.1
// Shamrock is licensed under the CeCILL 2.1 License, see LICENSE for more information
//
// -------------------------------------------------------//

#include "shambase/time.hpp"
#include "shamtest/shamtest.hpp"
#include <thread>

TestStart(Unittest, "shambase/time/start_stop_elapsed_gt_zero", unitt_timer_start_stop_elapsed, 1) {

shambase::Timer timer;

timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
timer.stop();

REQUIRE(timer.elapsed_sec() > 0);

std::string time_str = timer.get_time_str();
REQUIRE(!time_str.empty());
}

TestStart(Unittest, "shambase/time/sleep_200ms_precision", unitt_timer_sleep_200ms, 1) {

shambase::Timer timer;
timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(400));
timer.stop();

// sadly i must be verrrrrrry loose on the tolerances because of Github runners ...
REQUIRE_FLOAT_EQUAL(timer.elapsed_sec(), 0.4, 0.2);
}

TestStart(Unittest, "shambase/time/stop_overwrites_nanosec", unitt_timer_stop_overwrites, 1) {

shambase::Timer timer;

timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(300));
timer.stop();
f64 elapsed1 = timer.elapsed_sec();

std::this_thread::sleep_for(std::chrono::milliseconds(50));
timer.stop();
f64 elapsed2 = timer.elapsed_sec();

REQUIRE(elapsed1 < elapsed2);
}

TestStart(Unittest, "shambase/time/reusability", unitt_timer_reusability, 1) {

shambase::Timer timer;

timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
timer.stop();
f64 elapsed1 = timer.elapsed_sec();

timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
timer.stop();
f64 elapsed2 = timer.elapsed_sec();

REQUIRE(elapsed1 > elapsed2);
}

TestStart(Unittest, "shambase/time/get_time_str_has_unit", unitt_timer_get_time_str_format, 1) {

shambase::Timer timer;
timer.start();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
timer.stop();

std::string s = timer.get_time_str();

REQUIRE(!s.empty());
REQUIRE(s.find("ms") != std::string::npos || s.find("us") != std::string::npos);
}

TestStart(
Unittest, "shambase/time/nanosec_to_time_str_all_units", unitt_nanosec_to_time_str_various, 1) {

using namespace shambase;

REQUIRE(nanosec_to_time_str(0) == "0.00 ns");
REQUIRE(nanosec_to_time_str(500) == "500.00 ns");
REQUIRE(nanosec_to_time_str(2500) == "2.50 us");
REQUIRE(nanosec_to_time_str(5000000) == "5.00 ms");
REQUIRE(nanosec_to_time_str(2500000) == "2.50 ms");
REQUIRE(nanosec_to_time_str(5000000000) == "5.00 s");
REQUIRE(nanosec_to_time_str(2500000000) == "2.50 s");
REQUIRE(nanosec_to_time_str(2500000000000) == "2.50 ks");
REQUIRE(nanosec_to_time_str(2500000000000000) == "2.50 Ms");
REQUIRE(nanosec_to_time_str(2500000000000000000) == "2.50 Gs");
}
Loading