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;