Improve ability to reschedule phased loops in AOS
This makes it so that users can reliably reschedule phased loops in a
well-defined way, for situations, e.g., where there may be a need to
update the offset of the loop dynamically.
This slightly changes the default semantics around dynamically changing
the interval/offset of a phased loop, but I don't actually see anyone
doing that currently, and there were some poorly-defined corner cases
anyways.
References: PRO-18058
Change-Id: I679c905da1582d250deb7f9607c8517ed34e5a25
Signed-off-by: James Kuszmaul <james.kuszmaul@bluerivertech.com>
diff --git a/aos/events/event_loop_param_test.cc b/aos/events/event_loop_param_test.cc
index 0c8e41e..07bd6d4 100644
--- a/aos/events/event_loop_param_test.cc
+++ b/aos/events/event_loop_param_test.cc
@@ -1922,6 +1922,298 @@
EXPECT_GT(expected_times[expected_times.size() / 2], average_time - kEpsilon);
}
+// Tests that a phased loop responds correctly to a changing offset; sweep
+// across a variety of potential offset changes, to ensure that we are
+// exercising a variety of potential cases.
+TEST_P(AbstractEventLoopTest, PhasedLoopChangingOffsetSweep) {
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+ const int kCount = 5;
+
+ auto loop1 = MakePrimary();
+
+ std::vector<aos::monotonic_clock::duration> offset_options;
+ for (int ii = 0; ii < kCount; ++ii) {
+ offset_options.push_back(ii * kInterval / kCount);
+ }
+ std::vector<aos::monotonic_clock::duration> offset_sweep;
+ // Run over all the pair-wise combinations of offsets.
+ for (int ii = 0; ii < kCount; ++ii) {
+ for (int jj = 0; jj < kCount; ++jj) {
+ offset_sweep.push_back(offset_options.at(ii));
+ offset_sweep.push_back(offset_options.at(jj));
+ }
+ }
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ // Run kCount iterations.
+ size_t counter = 0;
+ phased_loop = loop1->AddPhasedLoop(
+ [&phased_loop, &expected_times, &loop1, this, kInterval, &counter,
+ offset_sweep](int count) {
+ EXPECT_EQ(count, 1);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ counter++;
+
+ if (counter == offset_sweep.size()) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+
+ phased_loop->set_interval_and_offset(kInterval,
+ offset_sweep.at(counter));
+ },
+ kInterval, offset_sweep.at(0));
+
+ Run();
+ ASSERT_EQ(expected_times.size(), offset_sweep.size());
+ for (size_t ii = 1; ii < expected_times.size(); ++ii) {
+ EXPECT_LE(expected_times.at(ii) - expected_times.at(ii - 1), kInterval);
+ }
+}
+
+// Tests that a phased loop responds correctly to being rescheduled with now
+// equal to a time in the past.
+TEST_P(AbstractEventLoopTest, PhasedLoopRescheduleInPast) {
+ const chrono::milliseconds kOffset = chrono::milliseconds(400);
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+
+ auto loop1 = MakePrimary();
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ int expected_count = 1;
+
+ // Set up a timer that will get run immediately after the phased loop and
+ // which will attempt to reschedule the phased loop to just before now. This
+ // should succeed, but will result in 0 cycles elapsing.
+ TimerHandler *manager_timer =
+ loop1->AddTimer([&phased_loop, &loop1, &expected_count, this]() {
+ if (expected_count == 0) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+ phased_loop->Reschedule(loop1->context().monotonic_event_time -
+ std::chrono::nanoseconds(1));
+ expected_count = 0;
+ });
+
+ phased_loop = loop1->AddPhasedLoop(
+ [&expected_count, &expected_times, &loop1, manager_timer](int count) {
+ EXPECT_EQ(count, expected_count);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ manager_timer->Setup(loop1->context().monotonic_event_time);
+ },
+ kInterval, kOffset);
+ phased_loop->set_name("Test loop");
+ manager_timer->set_name("Manager timer");
+
+ Run();
+
+ ASSERT_EQ(2u, expected_times.size());
+ ASSERT_EQ(expected_times[0], expected_times[1]);
+}
+
+// Tests that a phased loop responds correctly to being rescheduled at the time
+// when it should be triggering (it should kick the trigger to the next cycle).
+TEST_P(AbstractEventLoopTest, PhasedLoopRescheduleNow) {
+ const chrono::milliseconds kOffset = chrono::milliseconds(400);
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+
+ auto loop1 = MakePrimary();
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ bool should_exit = false;
+ // Set up a timer that will get run immediately after the phased loop and
+ // which will attempt to reschedule the phased loop to now. This should
+ // succeed, but will result in no change to the expected behavior (since this
+ // is the same thing that is actually done internally).
+ TimerHandler *manager_timer =
+ loop1->AddTimer([&phased_loop, &loop1, &should_exit, this]() {
+ if (should_exit) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+ phased_loop->Reschedule(loop1->context().monotonic_event_time);
+ should_exit = true;
+ });
+
+ phased_loop = loop1->AddPhasedLoop(
+ [&expected_times, &loop1, manager_timer](int count) {
+ EXPECT_EQ(count, 1);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ manager_timer->Setup(loop1->context().monotonic_event_time);
+ },
+ kInterval, kOffset);
+ phased_loop->set_name("Test loop");
+ manager_timer->set_name("Manager timer");
+
+ Run();
+
+ ASSERT_EQ(2u, expected_times.size());
+ ASSERT_EQ(expected_times[0] + kInterval, expected_times[1]);
+}
+
+// Tests that a phased loop responds correctly to being rescheduled at a time in
+// the distant future.
+TEST_P(AbstractEventLoopTest, PhasedLoopRescheduleFuture) {
+ const chrono::milliseconds kOffset = chrono::milliseconds(400);
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+
+ auto loop1 = MakePrimary();
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ bool should_exit = false;
+ int expected_count = 1;
+ TimerHandler *manager_timer = loop1->AddTimer(
+ [&expected_count, &phased_loop, &loop1, &should_exit, this, kInterval]() {
+ if (should_exit) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+ expected_count = 10;
+ // Knock off 1 ns, since the scheduler rounds up when it is
+ // scheduled to exactly a loop time.
+ phased_loop->Reschedule(loop1->context().monotonic_event_time +
+ kInterval * expected_count -
+ std::chrono::nanoseconds(1));
+ should_exit = true;
+ });
+
+ phased_loop = loop1->AddPhasedLoop(
+ [&expected_times, &loop1, manager_timer, &expected_count](int count) {
+ EXPECT_EQ(count, expected_count);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ manager_timer->Setup(loop1->context().monotonic_event_time);
+ },
+ kInterval, kOffset);
+ phased_loop->set_name("Test loop");
+ manager_timer->set_name("Manager timer");
+
+ Run();
+
+ ASSERT_EQ(2u, expected_times.size());
+ ASSERT_EQ(expected_times[0] + expected_count * kInterval, expected_times[1]);
+}
+
+// Tests that a phased loop responds correctly to having its phase offset
+// incremented and then being scheduled after a set time, exercising a pattern
+// where a phased loop's offset is changed while trying to maintain the trigger
+// at a consistent period.
+TEST_P(AbstractEventLoopTest, PhasedLoopRescheduleWithLaterOffset) {
+ const chrono::milliseconds kOffset = chrono::milliseconds(400);
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+
+ auto loop1 = MakePrimary();
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ bool should_exit = false;
+ TimerHandler *manager_timer = loop1->AddTimer(
+ [&phased_loop, &loop1, &should_exit, this, kInterval, kOffset]() {
+ if (should_exit) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+ // Schedule the next callback to be strictly later than the current time
+ // + interval / 2, to ensure a consistent frequency.
+ monotonic_clock::time_point half_time =
+ loop1->context().monotonic_event_time + kInterval / 2;
+ phased_loop->set_interval_and_offset(
+ kInterval, kOffset + std::chrono::nanoseconds(1), half_time);
+ phased_loop->Reschedule(half_time);
+ should_exit = true;
+ });
+
+ phased_loop = loop1->AddPhasedLoop(
+ [&expected_times, &loop1, manager_timer](int count) {
+ EXPECT_EQ(1, count);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ manager_timer->Setup(loop1->context().monotonic_event_time);
+ },
+ kInterval, kOffset);
+ phased_loop->set_name("Test loop");
+ manager_timer->set_name("Manager timer");
+
+ Run();
+
+ ASSERT_EQ(2u, expected_times.size());
+ ASSERT_EQ(expected_times[0] + kInterval + std::chrono::nanoseconds(1),
+ expected_times[1]);
+}
+
+// Tests that a phased loop responds correctly to having its phase offset
+// decremented and then being scheduled after a set time, exercising a pattern
+// where a phased loop's offset is changed while trying to maintain the trigger
+// at a consistent period.
+TEST_P(AbstractEventLoopTest, PhasedLoopRescheduleWithEarlierOffset) {
+ const chrono::milliseconds kOffset = chrono::milliseconds(400);
+ const chrono::milliseconds kInterval = chrono::milliseconds(1000);
+
+ auto loop1 = MakePrimary();
+
+ std::vector<::aos::monotonic_clock::time_point> expected_times;
+
+ PhasedLoopHandler *phased_loop;
+
+ bool should_exit = false;
+ TimerHandler *manager_timer = loop1->AddTimer(
+ [&phased_loop, &loop1, &should_exit, this, kInterval, kOffset]() {
+ if (should_exit) {
+ LOG(INFO) << "Exiting";
+ this->Exit();
+ return;
+ }
+ // Schedule the next callback to be strictly later than the current time
+ // + interval / 2, to ensure a consistent frequency.
+ const aos::monotonic_clock::time_point half_time =
+ loop1->context().monotonic_event_time + kInterval / 2;
+ phased_loop->set_interval_and_offset(
+ kInterval, kOffset - std::chrono::nanoseconds(1), half_time);
+ phased_loop->Reschedule(half_time);
+ should_exit = true;
+ });
+
+ phased_loop = loop1->AddPhasedLoop(
+ [&expected_times, &loop1, manager_timer](int count) {
+ EXPECT_EQ(1, count);
+ expected_times.push_back(loop1->context().monotonic_event_time);
+
+ manager_timer->Setup(loop1->context().monotonic_event_time);
+ },
+ kInterval, kOffset);
+ phased_loop->set_name("Test loop");
+ manager_timer->set_name("Manager timer");
+
+ Run();
+
+ ASSERT_EQ(2u, expected_times.size());
+ ASSERT_EQ(expected_times[0] + kInterval - std::chrono::nanoseconds(1),
+ expected_times[1]);
+}
+
// Tests that senders count correctly in the timing report.
TEST_P(AbstractEventLoopTest, SenderTimingReport) {
FLAGS_timing_report_ms = 1000;