folly/folly/stats/test/BufferedStatTest.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 <folly/stats/detail/BufferedStat.h>

#include <folly/Range.h>
#include <folly/portability/GTest.h>

using namespace folly;
using namespace folly::detail;

const size_t kDigestSize = 100;

struct MockClock {
 public:
  using duration = std::chrono::steady_clock::duration;
  using time_point = std::chrono::steady_clock::time_point;

  static time_point now() { return Now; }

  static time_point Now;
};

class SimpleDigest {
 public:
  explicit SimpleDigest(size_t sz) { EXPECT_EQ(kDigestSize, sz); }

  SimpleDigest merge(Range<const double*> r) const {
    SimpleDigest digest(100);

    digest.values_ = values_;
    for (auto it = r.begin(); it != r.end(); ++it) {
      digest.values_.push_back(*it);
    }
    return digest;
  }

  static SimpleDigest merge(Range<const SimpleDigest*> r) {
    SimpleDigest digest(100);
    for (auto it = r.begin(); it != r.end(); ++it) {
      for (auto value : it->values_) {
        digest.values_.push_back(value);
      }
    }
    return digest;
  }

  std::vector<double> getValues() const { return values_; }

  bool empty() const { return values_.empty(); }

 private:
  std::vector<double> values_;
};

MockClock::time_point MockClock::Now = MockClock::time_point{};

class BufferedDigestTest : public ::testing::Test {
 protected:
  std::unique_ptr<BufferedDigest<SimpleDigest, MockClock>> bd;
  const size_t nBuckets = 60;
  const size_t bufferSize = 1000;
  const std::chrono::milliseconds bufferDuration{1000};

  void SetUp() override {
    MockClock::Now = MockClock::time_point{};
    bd = std::make_unique<BufferedDigest<SimpleDigest, MockClock>>(
        bufferDuration, bufferSize, kDigestSize);
  }
};

TEST_F(BufferedDigestTest, Buffering) {
  bd->append(0);
  bd->append(1);
  bd->append(2);

  auto digest = bd->get();
  EXPECT_TRUE(digest.empty());
}

TEST_F(BufferedDigestTest, PartiallyPassedExpiry) {
  bd->append(0);
  bd->append(1);
  bd->append(2);

  MockClock::Now += bufferDuration / 10;

  auto digest = bd->get();

  auto values = digest.getValues();
  EXPECT_EQ(0, values[0]);
  EXPECT_EQ(1, values[1]);
  EXPECT_EQ(2, values[2]);
}

TEST_F(BufferedDigestTest, ForceUpdate) {
  bd->append(0);
  bd->append(1);
  bd->append(2);

  // empty since we haven't passed expiry
  auto digest = bd->get();
  EXPECT_TRUE(digest.empty());

  // force update
  bd->flush();
  digest = bd->get();
  auto values = digest.getValues();
  EXPECT_EQ(0, values[0]);
  EXPECT_EQ(1, values[1]);
  EXPECT_EQ(2, values[2]);

  // append 3 and do a normal get; only the previously
  // flushed values should show up and not 3 since we
  // haven't passed expiry
  bd->append(3);
  digest = bd->get();
  values = digest.getValues();
  EXPECT_EQ(0, values[0]);
  EXPECT_EQ(1, values[1]);
  EXPECT_EQ(2, values[2]);

  // pass expiry; 3 should now be visible
  MockClock::Now += bufferDuration;
  digest = bd->get();
  values = digest.getValues();
  EXPECT_EQ(0, values[0]);
  EXPECT_EQ(1, values[1]);
  EXPECT_EQ(2, values[2]);
  EXPECT_EQ(3, values[3]);
}

class BufferedSlidingWindowTest : public ::testing::Test {
 protected:
  std::unique_ptr<BufferedSlidingWindow<SimpleDigest, MockClock>> bsw;
  const size_t nBuckets = 60;
  const size_t bufferSize = 1000;
  const std::chrono::milliseconds windowDuration{1000};

  void SetUp() override {
    MockClock::Now = MockClock::time_point{};
    bsw = std::make_unique<BufferedSlidingWindow<SimpleDigest, MockClock>>(
        nBuckets, windowDuration, bufferSize, kDigestSize);
  }
};

TEST_F(BufferedSlidingWindowTest, Buffering) {
  bsw->append(0);
  bsw->append(1);
  bsw->append(2);

  auto digests = bsw->get();
  EXPECT_EQ(0, digests.size());
}

TEST_F(BufferedSlidingWindowTest, PartiallyPassedExpiry) {
  bsw->append(0);
  bsw->append(1);
  bsw->append(2);

  MockClock::Now += windowDuration / 10;

  auto digests = bsw->get();

  EXPECT_EQ(1, digests.size());
  EXPECT_EQ(3, digests[0].getValues().size());

  for (size_t i = 0; i < 3; ++i) {
    EXPECT_EQ(double(i), digests[0].getValues()[i]);
  }
}

TEST_F(BufferedSlidingWindowTest, ForceUpdate) {
  bsw->append(0);
  bsw->append(1);
  bsw->append(2);

  // empty since we haven't passed expiry
  auto digests = bsw->get();
  EXPECT_EQ(0, digests.size());

  // flush
  bsw->flush();
  digests = bsw->get();
  EXPECT_EQ(1, digests.size());
  EXPECT_EQ(3, digests[0].getValues().size());
  for (size_t i = 0; i < 3; ++i) {
    EXPECT_EQ(double(i), digests[0].getValues()[i]);
  }

  // append 3 and flush again; 3 will be merged with
  // current window
  bsw->append(3);
  bsw->flush();
  digests = bsw->get();
  EXPECT_EQ(1, digests.size());
  EXPECT_EQ(4, digests[0].getValues().size());
  for (size_t i = 0; i < 4; ++i) {
    EXPECT_EQ(double(i), digests[0].getValues()[i]);
  }

  // append 4 and do a regular get. previous values
  // show up but not 4
  bsw->append(4);
  digests = bsw->get();
  EXPECT_EQ(1, digests.size());
  EXPECT_EQ(4, digests[0].getValues().size());
  for (size_t i = 0; i < 4; ++i) {
    EXPECT_EQ(double(i), digests[0].getValues()[i]);
  }

  // pass expiry
  MockClock::Now += windowDuration;
  digests = bsw->get();
  EXPECT_EQ(2, digests.size());

  EXPECT_EQ(1, digests[0].getValues().size());
  EXPECT_EQ(4, digests[0].getValues().front());

  EXPECT_EQ(4, digests[1].getValues().size());
  for (size_t i = 0; i < 4; ++i) {
    EXPECT_EQ(double(i), digests[1].getValues()[i]);
  }
}

TEST_F(BufferedSlidingWindowTest, BufferingAfterSlide) {
  MockClock::Now += std::chrono::milliseconds{1};

  bsw->append(1);

  auto digests = bsw->get();
  EXPECT_EQ(0, digests.size());
}

TEST_F(BufferedSlidingWindowTest, TwoSlides) {
  bsw->append(0);

  MockClock::Now += windowDuration;

  bsw->append(1);

  MockClock::Now += windowDuration;

  auto digests = bsw->get();

  EXPECT_EQ(2, digests.size());
  EXPECT_EQ(1, digests[0].getValues().size());
  EXPECT_EQ(1, digests[0].getValues()[0]);
  EXPECT_EQ(1, digests[1].getValues().size());
  EXPECT_EQ(0, digests[1].getValues()[0]);
}

TEST_F(BufferedSlidingWindowTest, MultiWindowDurationSlide) {
  bsw->append(0);

  MockClock::Now += windowDuration * 2;

  auto digests = bsw->get();
  EXPECT_EQ(1, digests.size());
}

TEST_F(BufferedSlidingWindowTest, SlidePastWindow) {
  bsw->append(0);

  MockClock::Now += windowDuration * (nBuckets + 1);

  auto digests = bsw->get();

  EXPECT_EQ(0, digests.size());
}