Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in a new TimeTravel mesage to allow the system to change the NUClear clock and ensure that chrono events continue to work #102

Merged
merged 46 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f1a5e27
Add in a new TimeTravel mesage to allow the system to change the NUCl…
TrentHouliston Feb 23, 2024
29927be
Delete NUClearNetwork.cpp
Tom0Brien Mar 6, 2024
0786ca0
Fix linking
Tom0Brien Mar 6, 2024
621ea39
Add time travel types
Tom0Brien Mar 20, 2024
b28856f
Add time travel actions
Tom0Brien Mar 27, 2024
c73ad64
Update TimeTravel.hpp
Tom0Brien Mar 27, 2024
36d022c
Change to adjustment
Tom0Brien Mar 27, 2024
26f5605
Add first test
Tom0Brien Mar 27, 2024
d71d315
Use a pointer
Tom0Brien Mar 27, 2024
cf73079
chrono_task_delay_ -> task_delay
Tom0Brien Mar 27, 2024
86f93ee
Move global vars to reactor
Tom0Brien Mar 27, 2024
1efec1a
Const
Tom0Brien Apr 2, 2024
6291c5d
Update test to use milliseconds
Tom0Brien Apr 2, 2024
fab29bf
Add tolerance
Tom0Brien Apr 3, 2024
b746d53
Add comments
Tom0Brien Apr 3, 2024
80136af
Clang tidy things
Tom0Brien Apr 3, 2024
6e71474
Clang tidy fixes
Tom0Brien Apr 3, 2024
f339b0f
Clang tidy
Tom0Brien Apr 3, 2024
51011be
Clang tidy
Tom0Brien Apr 3, 2024
8ef05ac
abs -> std::abs
Tom0Brien Apr 9, 2024
78c0572
Init fields
Tom0Brien Apr 9, 2024
192f533
Tidy up
Tom0Brien Apr 9, 2024
2f62c17
Move implementation to Cpp and add comments
Tom0Brien Apr 9, 2024
70a15f7
Rename JUMP to ABSOLUTE
Tom0Brien Apr 9, 2024
96827e5
Clean up test
Tom0Brien Apr 10, 2024
118db70
Consider RTF in Chrono sleep
Tom0Brien Apr 10, 2024
64158d7
Clean up test
Tom0Brien Apr 10, 2024
27940ee
Account for rtf = 0.0 in chrono controller
Tom0Brien Apr 10, 2024
d86a6af
Add test for TimeTravel when rtf = 0.0
Tom0Brien Apr 10, 2024
c66361a
Init events field
Tom0Brien Apr 10, 2024
e92af34
Delete CustomClock.cpp
Tom0Brien Apr 10, 2024
09d71e7
Change time travel from adjustment to target
Tom0Brien Apr 10, 2024
dfae922
const things
Tom0Brien Apr 10, 2024
ceb92f6
Linting
Tom0Brien Apr 10, 2024
355e96d
Lint
Tom0Brien Apr 10, 2024
1ab2a87
Move clock reset
Tom0Brien Apr 16, 2024
452a1fa
Apply suggestions
Tom0Brien Apr 16, 2024
1d15fdd
Apply suggestions
Tom0Brien Apr 16, 2024
f5440b6
Add diff string
Tom0Brien Apr 16, 2024
6ad8b29
Cast type to int64_t
Tom0Brien Apr 17, 2024
9203c6e
Test change enum for windows
Tom0Brien Apr 17, 2024
a168f86
Revert "Test change enum for windows"
Tom0Brien Apr 17, 2024
a0ad58b
undef RELATIVE and ABSOLUTE
Tom0Brien Apr 17, 2024
eb73f3f
Apply suggestions
Tom0Brien Apr 17, 2024
ab6cdf7
Change TestUnits to help CI
Tom0Brien Apr 17, 2024
0659fec
Merge branch 'main' into houliston/timetravel
TrentHouliston Apr 17, 2024
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
133 changes: 114 additions & 19 deletions src/clock.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,131 @@
#ifndef NUCLEAR_CLOCK_HPP
#define NUCLEAR_CLOCK_HPP

// Default to using the system clock but allow it to be overridden by the user
#ifndef NUCLEAR_CLOCK_TYPE
#define NUCLEAR_CLOCK_TYPE std::chrono::system_clock
#endif // NUCLEAR_CLOCK_TYPE

#include <array>
#include <atomic>
#include <chrono>
#include <mutex>

namespace NUClear {

#ifdef NUCLEAR_CLOCK_TYPE
/// @brief The custom base clock that is used when defining the NUClear clock
using base_clock = NUCLEAR_CLOCK_TYPE;
#else
/// @brief The default base clock that is used when defining the NUClear clock
using base_clock = std::chrono::steady_clock;
#endif // NUCLEAR_CLOCK_TYPE
/**
* @brief A clock class that extends a base clock type and allows for clock adjustment and setting.
*/
template <typename = void>
struct nuclear_clock : public NUCLEAR_CLOCK_TYPE {
using base_clock = NUCLEAR_CLOCK_TYPE;

/**
* @brief Get the current time of the clock.
* @return The current time of the clock.
*/
static time_point now() {
const ClockData current = data[active.load()]; // Take a copy in case it changes
return current.epoch + dc((base_clock::now() - current.base_from) * current.rtf);
}

/**
* @brief Adjust the clock by a specified duration and real-time factor.
* @param adjustment The duration by which to adjust the clock.
* @param rtf The real-time factor to apply to the clock.
*/
static void adjust_clock(const duration& adjustment, const double& rtf = 1.0) {
const std::lock_guard<std::mutex> lock(mutex);
// Load the current state
const auto& current = data[active.load()];
const int n = static_cast<int>((active.load() + 1) % data.size());
auto& next = data[n];

// Perform the update
auto base = base_clock::now();
next.epoch = current.epoch + adjustment + dc((base - current.base_from) * current.rtf);
next.base_from = base;
next.rtf = rtf;
active = n;
}

#ifndef NUCLEAR_CUSTOM_CLOCK
/**
* @brief Set the clock to a specified time and real-time factor.
* @param time The time to set the clock to.
* @param rtf The real-time factor to apply to the clock.
*/
static void set_clock(const time_point& time, const double& rtf = 1.0) {
const std::lock_guard<std::mutex> lock(mutex);
// Load the current state
const int n = static_cast<int>((active.load() + 1) % data.size());
auto& next = data[n];

/// @brief The clock that is used throughout the entire nuclear system
using clock = base_clock;
// Perform the update
auto base = base_clock::now();
next.epoch = time;
next.base_from = base;
next.rtf = rtf;
active = n;
}

#else

struct clock {
using rep = base_clock::rep;
using period = base_clock::period;
using duration = base_clock::duration;
using time_point = base_clock::time_point;
static constexpr bool is_steady = false;
/**
* @brief Get the real-time factor of the clock.
* @return The real-time factor of the clock.
*/
static double rtf() {
return data[active.load()].rtf;
}

static time_point now();
private:
/**
* @brief Convert a duration to the clock's duration type.
* @tparam T The type of the duration.
* @param t The duration to convert.
* @return The converted duration.
*/
template <typename T>
duration static dc(const T& t) {
return std::chrono::duration_cast<duration>(t);
}

/**
* @brief Data structure to hold clock information.
*/
struct ClockData {
/// When the clock was last updated under the true time
time_point base_from = base_clock::now();
/// Our calculated time when the clock was last updated in simulated time
time_point epoch = base_from;
/// The real time factor of the simulated clock
double rtf = 1.0;

ClockData() = default;
};

/// @brief The mutex to protect the clock data.
static std::mutex mutex; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

/// @brief The clock data for the system.
static std::array<ClockData, 3> data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

/// @brief The active clock data index.
static std::atomic<int> active; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
};

#endif
template <typename T>
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::mutex nuclear_clock<T>::mutex;
template <typename T>
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::array<typename nuclear_clock<T>::ClockData, 3> nuclear_clock<T>::data =
std::array<typename nuclear_clock<T>::ClockData, 3>{};
template <typename T>
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<int> nuclear_clock<T>::active{0};

using clock = nuclear_clock<>;


} // namespace NUClear

Expand Down
95 changes: 68 additions & 27 deletions src/extension/ChronoController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

#include "../PowerPlant.hpp"
#include "../Reactor.hpp"
#include "../message/TimeTravel.hpp"
#include "../util/precise_sleep.hpp"

namespace NUClear {
Expand Down Expand Up @@ -100,6 +101,33 @@ namespace extension {
wait.notify_all();
});

on<Trigger<message::TimeTravel>>().then("Time Travel", [this](const message::TimeTravel& travel) {
const std::lock_guard<std::mutex> lock(mutex);

// Adjust clock to target time and leave chrono tasks where they are
switch (travel.type) {
case message::TimeTravel::Action::ABSOLUTE: clock::set_clock(travel.target, travel.rtf); break;
case message::TimeTravel::Action::RELATIVE: {
auto adjustment = travel.target - NUClear::clock::now();
clock::set_clock(travel.target, travel.rtf);
for (auto& task : tasks) {
task.time += adjustment;
}

} break;
case message::TimeTravel::Action::NEAREST: {
auto next_task =
std::min_element(tasks.begin(), tasks.end(), [](const ChronoTask& a, const ChronoTask& b) {
return a.time < b.time;
});
clock::set_clock(std::min(next_task->time, travel.target), travel.rtf);
} break;
}

// Poke the system
wait.notify_all();
});

on<Always, Priority::REALTIME>().then("Chrono Controller", [this] {
// Run until we are told to stop
while (running.load()) {
Expand All @@ -115,33 +143,7 @@ namespace extension {
auto start = NUClear::clock::now();
auto target = tasks.front().time;

if (target - start > cv_accuracy) {
// Wait on the cv
wait.wait_until(lock, target - cv_accuracy);

// Update the accuracy of our cv wait
const auto end = NUClear::clock::now();
const auto error = end - (target - cv_accuracy); // when ended - when wanted to end
if (error.count() > 0) { // only if we were late
cv_accuracy = error > cv_accuracy ? error : ((cv_accuracy * 99 + error) / 100);
}
}
else if (target - start > ns_accuracy) {
// Wait on nanosleep
util::precise_sleep(target - start - ns_accuracy);

// Update the accuracy of our precise sleep
const auto end = NUClear::clock::now();
const auto error = end - (target - ns_accuracy); // when ended - when wanted to end
if (error.count() > 0) { // only if we were late
ns_accuracy = error > ns_accuracy ? error : ((ns_accuracy * 99 + error) / 100);
}
}
else {
while (NUClear::clock::now() < tasks.front().time) {
// Spinlock until we get to the time
}

if (target <= start) {
// Run our task and if it returns false remove it
const bool renew = tasks.front()();

Expand All @@ -157,6 +159,45 @@ namespace extension {
tasks.pop_back();
}
}
else {
const NUClear::clock::duration time_until_task =
std::chrono::duration_cast<NUClear::clock::duration>((target - start) / clock::rtf());

if (clock::rtf() == 0.0) {
// If we are paused then just wait until we are unpaused
wait.wait(lock, [&] {
return !running.load() || clock::rtf() != 0.0 || NUClear::clock::now() != start;
});
}
else if (time_until_task > cv_accuracy) { // A long time in the future
// Wait on the cv
wait.wait_for(lock, time_until_task - cv_accuracy);

// Update the accuracy of our cv wait
const auto end = NUClear::clock::now();
const auto error = end - (target - cv_accuracy); // when ended - when wanted to end
if (error.count() > 0) { // only if we were late
cv_accuracy = error > cv_accuracy ? error : ((cv_accuracy * 99 + error) / 100);
}
}
else if (time_until_task > ns_accuracy) { // Somewhat close in time
// Wait on nanosleep
const NUClear::clock::duration sleep_time = time_until_task - ns_accuracy;
util::precise_sleep(sleep_time);

// Update the accuracy of our precise sleep
const auto end = NUClear::clock::now();
const auto error = end - (target - ns_accuracy); // when ended - when wanted to end
if (error.count() > 0) { // only if we were late
ns_accuracy = error > ns_accuracy ? error : ((ns_accuracy * 99 + error) / 100);
}
}
else {
while (NUClear::clock::now() < tasks.front().time) {
// Spinlock until we get to the time
}
}
}
}
}
});
Expand Down
63 changes: 63 additions & 0 deletions src/message/TimeTravel.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2024 NUClear Contributors
*
* This file is part of the NUClear codebase.
* See https://github.com/Fastcode/NUClear for further info.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

#ifndef NUCLEAR_MESSAGE_TIME_TRAVEL_HPP
#define NUCLEAR_MESSAGE_TIME_TRAVEL_HPP

#include "../clock.hpp"

namespace NUClear {
namespace message {
/**
* @brief This message is used to adjust the time of the system clock and the rate at which time passes.
*
* Using this message allows the NUClear system to adapt to the change by adjusting any time based operations
* to the new time and rate.
*/
struct TimeTravel {
enum class Action {
/// @brief Adjust clock and move all chrono tasks with it
RELATIVE,

/// @brief Adjust clock to target time and leave chrono tasks where they are
ABSOLUTE,

/// @brief Adjust clock to as close to target as possible without skipping any chrono tasks
NEAREST,
};

/// @brief The target time to set the clock to
clock::time_point target = clock::now();
/// @brief The rate at which time should pass
double rtf = 1.0;
/// @brief The type of time travel to perform
Action type = Action::RELATIVE;

TimeTravel() = default;
TimeTravel(const clock::time_point& target, double rtf = 1.0, Action type = Action::RELATIVE)
: target(target), rtf(rtf), type(type) {}
};

} // namespace message
} // namespace NUClear

#endif // NUCLEAR_MESSAGE_TIME_TRAVEL_HPP
17 changes: 10 additions & 7 deletions tests/individual/BaseClock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
#include <catch.hpp>
#include <ctime> // for localtime_r/s

// This define declares that we are using system_clock as the base clock for NUClear
#define NUCLEAR_CLOCK_TYPE std::chrono::system_clock
// This define declares that we are using steady_clock as the base clock for NUClear
#define NUCLEAR_CLOCK_TYPE std::chrono::steady_clock

#include <chrono>
#include <nuclear>
Expand All @@ -38,7 +38,7 @@
namespace {

// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::vector<std::pair<NUClear::clock::time_point, std::chrono::system_clock::time_point>> times;
std::vector<std::pair<NUClear::clock::time_point, std::chrono::steady_clock::time_point>> times;
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::mutex times_mutex;
constexpr int n_time = 100;
Expand All @@ -56,7 +56,7 @@ class TestReactor : public NUClear::Reactor {
on<Trigger<NUClear::message::ReactionStatistics>>().then(
[this](const NUClear::message::ReactionStatistics& stats) {
const std::lock_guard<std::mutex> lock(times_mutex);
times.push_back(std::make_pair(stats.finished, std::chrono::system_clock::now()));
times.push_back(std::make_pair(stats.finished, std::chrono::steady_clock::now()));
if (times.size() > n_time) {
powerplant.shutdown();
}
Expand All @@ -68,7 +68,7 @@ class TestReactor : public NUClear::Reactor {
TEST_CASE("Testing base clock works correctly", "[api][base_clock]") {

INFO("Ensure NUClear base_clock is the correct type");
STATIC_REQUIRE(std::is_same<NUClear::clock, std::chrono::system_clock>::value);
STATIC_REQUIRE(std::is_base_of<std::chrono::steady_clock, NUClear::clock>::value);

// Construct the powerplant
NUClear::Configuration config;
Expand Down Expand Up @@ -105,8 +105,11 @@ TEST_CASE("Testing base clock works correctly", "[api][base_clock]") {
// Compute the differences between the time pairs
int match_count = 0;
for (const auto& time_pairs : times) {
const std::time_t ntt = NUClear::clock::to_time_t(time_pairs.first);
const std::time_t stt = NUClear::clock::to_time_t(time_pairs.second);
using namespace std::chrono; // NOLINT(google-build-using-namespace)
const std::time_t ntt = system_clock::to_time_t(
system_clock::time_point(duration_cast<system_clock::duration>(time_pairs.first.time_since_epoch())));
const std::time_t stt = system_clock::to_time_t(
system_clock::time_point(duration_cast<system_clock::duration>(time_pairs.second.time_since_epoch())));

std::tm result{};
#ifdef WIN32
Expand Down
Loading
Loading