folly/folly/io/async/test/HHWheelTimerSlowTests.cpp

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <thread>
#include <vector>

#include <folly/Random.h>
#include <folly/io/async/EventBase.h>
#include <folly/io/async/HHWheelTimer.h>
#include <folly/io/async/test/UndelayedDestruction.h>
#include <folly/io/async/test/Util.h>
#include <folly/portability/GTest.h>

using namespace folly;
using std::chrono::milliseconds;

typedef UndelayedDestruction<HHWheelTimer> StackWheelTimer;

class TestTimeout : public HHWheelTimer::Callback {
 public:
  TestTimeout() {}
  TestTimeout(HHWheelTimer* t, milliseconds timeout) {
    t->scheduleTimeout(this, timeout);
  }

  void timeoutExpired() noexcept override {
    timestamps.emplace_back();
    if (fn) {
      fn();
    }
  }

  void callbackCanceled() noexcept override {
    canceledTimestamps.emplace_back();
    if (fn) {
      fn();
    }
  }

  std::deque<TimePoint> timestamps;
  std::deque<TimePoint> canceledTimestamps;
  std::function<void()> fn;
};

struct HHWheelTimerTest : public ::testing::Test {
  EventBase eventBase;
};

/* Test takes ~2.5 minutes to run */
TEST_F(HHWheelTimerTest, Level2) {
  HHWheelTimer& t = eventBase.timer();

  TestTimeout t1;
  TestTimeout t2;

  ASSERT_EQ(t.count(), 0);

  t.scheduleTimeout(&t1, milliseconds(605 * 256));
  t.scheduleTimeout(&t2, milliseconds(300 * 256));

  ASSERT_EQ(t.count(), 2);

  TimePoint start;
  eventBase.loop();
  TimePoint end;

  ASSERT_EQ(t1.timestamps.size(), 1);
  ASSERT_EQ(t2.timestamps.size(), 1);
  ASSERT_EQ(t.count(), 0);

  // Check that the timeout was delayed by sleep
  T_CHECK_TIMEOUT(
      start,
      t1.timestamps[0],
      milliseconds(605 * 256),
      milliseconds(256 * 256));
  T_CHECK_TIMEOUT(
      start,
      t2.timestamps[0],
      milliseconds(300 * 256),
      milliseconds(256 * 256));
}

/*
 * Test the tick interval parameter
 */
TEST_F(HHWheelTimerTest, AtMostEveryN) {
  // Create a timeout set with a 10ms interval, to fire no more than once
  // every 3ms.
  milliseconds interval(10);
  milliseconds atMostEveryN(3);
  StackWheelTimer t(&eventBase, atMostEveryN);

  // Create 60 timeouts to be added to ts1 at 1ms intervals.
  uint32_t numTimeouts = 60;
  std::vector<TestTimeout> timeouts(numTimeouts);

  // Create a scheduler timeout to add the timeouts 1ms apart.
  uint32_t index = 0;
  StackWheelTimer ts1(&eventBase, milliseconds(1));
  TestTimeout scheduler(&ts1, milliseconds(1));
  scheduler.fn = [&] {
    if (index >= numTimeouts) {
      return;
    }
    // Call timeoutExpired() on the timeout so it will record a timestamp.
    // This is done only so we can record when we scheduled the timeout.
    // This way if ts1 starts to fall behind a little over time we will still
    // be comparing the ts1 timeouts to when they were first scheduled (rather
    // than when we intended to schedule them).  The scheduler may fall behind
    // eventually since we don't really schedule it once every millisecond.
    // Each time it finishes we schedule it for 1 millisecond in the future.
    // The amount of time it takes to run, and any delays it encounters
    // getting scheduled may eventually add up over time.
    timeouts[index].timeoutExpired();

    // Schedule the new timeout
    t.scheduleTimeout(&timeouts[index], interval);
    // Reschedule ourself
    ts1.scheduleTimeout(&scheduler, milliseconds(1));
    ++index;
  };

  TimePoint start;
  eventBase.loop();
  TimePoint end;

  // This should take roughly 60 + 10 ms to finish. If it takes more than
  // 250 ms to finish the system is probably heavily loaded, so skip.
  if (std::chrono::duration_cast<std::chrono::milliseconds>(
          end.getTime() - start.getTime())
          .count() > 250) {
    LOG(WARNING) << "scheduling all timeouts takes too long";
    return;
  }

  // We scheduled timeouts 1ms apart, when the HHWheelTimer is only allowed
  // to wake up at most once every 3ms.  It will therefore wake up every 3ms
  // and fire groups of approximately 3 timeouts at a time.
  //
  // This is "approximately 3" since it may get slightly behind and fire 4 in
  // one interval, etc.  T_CHECK_TIMEOUT normally allows a few milliseconds of
  // tolerance.  We have to add the same into our checking algorithm here.
  for (uint32_t idx = 0; idx < numTimeouts; ++idx) {
    ASSERT_EQ(timeouts[idx].timestamps.size(), 2);

    TimePoint scheduledTime(timeouts[idx].timestamps[0]);
    TimePoint firedTime(timeouts[idx].timestamps[1]);

    // Assert that the timeout fired at roughly the right time.
    // T_CHECK_TIMEOUT() normally has a tolerance of 5ms.  Allow an additional
    // atMostEveryN.
    milliseconds tolerance = milliseconds(5) + interval;
    T_CHECK_TIMEOUT(scheduledTime, firedTime, atMostEveryN, tolerance);

    // Assert that the difference between the previous timeout and now was
    // either very small (fired in the same event loop), or larger than
    // atMostEveryN.
    if (idx == 0) {
      // no previous value
      continue;
    }
    TimePoint prev(timeouts[idx - 1].timestamps[1]);

    auto delta = (firedTime.getTimeStart() - prev.getTimeEnd()) -
        (firedTime.getTimeWaiting() - prev.getTimeWaiting());
    if (delta > milliseconds(1)) {
      T_CHECK_TIMEOUT(prev, firedTime, atMostEveryN);
    }
  }
}

/*
 * Test an event loop that is blocking
 */

TEST_F(HHWheelTimerTest, SlowLoop) {
  StackWheelTimer t(&eventBase, milliseconds(1));

  TestTimeout t1;
  TestTimeout t2;

  ASSERT_EQ(t.count(), 0);

  eventBase.runInLoop([]() {
    /* sleep override */
    std::this_thread::sleep_for(std::chrono::microseconds(10000));
  });
  t.scheduleTimeout(&t1, milliseconds(5));

  ASSERT_EQ(t.count(), 1);

  TimePoint start;
  eventBase.loop();
  TimePoint end;

  ASSERT_EQ(t1.timestamps.size(), 1);
  ASSERT_EQ(t.count(), 0);

  // Check that the timeout was delayed by sleep
  T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(10), milliseconds(1));
  T_CHECK_TIMEOUT(start, end, milliseconds(10), milliseconds(1));

  eventBase.runInLoop([]() {
    /* sleep override */
    std::this_thread::sleep_for(std::chrono::microseconds(10000));
  });
  t.scheduleTimeout(&t2, milliseconds(5));

  ASSERT_EQ(t.count(), 1);

  TimePoint start2;
  eventBase.loop();
  TimePoint end2;

  ASSERT_EQ(t2.timestamps.size(), 1);
  ASSERT_EQ(t.count(), 0);

  // Check that the timeout was NOT delayed by sleep
  T_CHECK_TIMEOUT(start2, t2.timestamps[0], milliseconds(10), milliseconds(1));
  T_CHECK_TIMEOUT(start2, end2, milliseconds(10), milliseconds(1));
}

/*
 * Test upper timer levels.  Slow by necessity :/
 */

TEST_F(HHWheelTimerTest, Level1) {
  HHWheelTimer& t = eventBase.timer();

  TestTimeout t1;
  TestTimeout t2;

  ASSERT_EQ(t.count(), 0);

  t.scheduleTimeout(&t1, milliseconds(605));
  t.scheduleTimeout(&t2, milliseconds(300));

  ASSERT_EQ(t.count(), 2);

  TimePoint start;
  eventBase.loop();
  TimePoint end;

  ASSERT_EQ(t1.timestamps.size(), 1);
  ASSERT_EQ(t2.timestamps.size(), 1);
  ASSERT_EQ(t.count(), 0);

  // Check that the timeout was delayed by sleep
  T_CHECK_TIMEOUT(
      start, t1.timestamps[0], milliseconds(605), milliseconds(256));
  T_CHECK_TIMEOUT(
      start, t2.timestamps[0], milliseconds(300), milliseconds(256));
}

TEST_F(HHWheelTimerTest, Stress) {
  StackWheelTimer t(&eventBase, milliseconds(1));

  long timeoutcount = 10000;
  TestTimeout timeouts[10000];
  long runtimeouts = 0;
  for (long i = 0; i < timeoutcount; i++) {
    long timeout = Random::rand32(1, 10000);
    if (Random::rand32(3)) {
      // NOTE: hhwheel timer runs before eventbase runAfterDelay,
      // so runAfterDelay cancelTimeout() must run  at least one timerwheel
      // before scheduleTimeout, to ensure it runs first.
      timeout += 256;
      t.scheduleTimeout(&timeouts[i], std::chrono::milliseconds(timeout));
      eventBase.runAfterDelay(
          [&, i]() {
            timeouts[i].fn = nullptr;
            timeouts[i].cancelTimeout();
            runtimeouts++;
            LOG(INFO) << "Ran " << runtimeouts << " timeouts, cancelled";
          },
          timeout - 256);
      timeouts[i].fn = [&, i, timeout]() {
        LOG(INFO) << "FAIL:timer " << i << " still fired in " << timeout;
        ADD_FAILURE();
      };
    } else {
      t.scheduleTimeout(&timeouts[i], std::chrono::milliseconds(timeout));
      timeouts[i].fn = [&, i]() {
        timeoutcount++;
        long newtimeout = Random::rand32(1, 10000);
        t.scheduleTimeout(&timeouts[i], std::chrono::milliseconds(newtimeout));
        runtimeouts++;
        /* sleep override */ usleep(1000);
        LOG(INFO) << "Ran " << runtimeouts << " timeouts of " << timeoutcount;
        timeouts[i].fn = [&]() {
          runtimeouts++;
          LOG(INFO) << "Ran " << runtimeouts << " timeouts of " << timeoutcount;
        };
      };
    }
  }

  LOG(INFO) << "RUNNING TEST";
  eventBase.loop();

  EXPECT_EQ(runtimeouts, timeoutcount);
}