// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cc/metrics/dropped_frame_counter.h"

#include <vector>

#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/time/time.h"
#include "cc/animation/animation_host.h"
#include "cc/test/fake_content_layer_client.h"
#include "cc/test/fake_picture_layer.h"
#include "cc/test/layer_tree_test.h"

namespace cc {
namespace {

class DroppedFrameCounterTestBase : public LayerTreeTest {
 public:
  DroppedFrameCounterTestBase() = default;
  ~DroppedFrameCounterTestBase() override = default;

  virtual void SetUpTestConfigAndExpectations() = 0;

  void InitializeSettings(LayerTreeSettings* settings) override {
    settings->commit_to_active_tree = false;
  }

  void SetupTree() override {
    LayerTreeTest::SetupTree();

    Layer* root_layer = layer_tree_host()->root_layer();
    scroll_layer_ = FakePictureLayer::Create(&client_);
    // Set up the layer so it always has something to paint.
    scroll_layer_->set_always_update_resources(true);
    scroll_layer_->SetBounds({3, 3});
    client_.set_bounds({3, 3});
    root_layer->AddChild(scroll_layer_);
  }

  void RunTest(CompositorMode mode) override {
    SetUpTestConfigAndExpectations();
    LayerTreeTest::RunTest(mode);
  }

  void BeginTest() override {
    ASSERT_GT(config_.animation_frames, 0u);

    // Start with requesting main-frames.
    PostSetNeedsCommitToMainThread();
  }

  void AfterTest() override {
    EXPECT_GE(total_frames_, config_.animation_frames);
    // It is possible to drop even more frame than what the test expects (e.g.
    // in slower machines, slower builds such as asan/tsan builds, etc.), since
    // the test does not strictly control both threads and deadlines. Therefore,
    // it is not possible to check for strict equality here.
    EXPECT_LE(expect_.min_dropped_main, dropped_main_);
    EXPECT_LE(expect_.min_dropped_compositor, dropped_compositor_);
    EXPECT_LE(expect_.min_dropped_smoothness, dropped_smoothness_);
  }

  // Compositor thread function overrides:
  void WillBeginImplFrameOnThread(LayerTreeHostImpl* host_impl,
                                  const viz::BeginFrameArgs& args,
                                  bool has_damage) override {
    if (TestEnded())
      return;

    // Request a re-draw, and set a non-empty damage region (otherwise the
    // draw is aborted with 'no damage').
    host_impl->SetNeedsRedraw();
    host_impl->SetViewportDamage(gfx::Rect(0, 0, 10, 20));

    if (skip_main_thread_next_frame_) {
      skip_main_thread_next_frame_ = false;
    } else {
      // Request update from the main-thread too.
      host_impl->SetNeedsCommit();
    }
  }

  void DrawLayersOnThread(LayerTreeHostImpl* host_impl) override {
    // If the main-thread is blocked, then unblock it once the compositor thread
    // has already drawn a frame.
    base::WaitableEvent* wait = nullptr;
    {
      base::AutoLock lock(wait_lock_);
      wait = wait_;
    }

    if (wait) {
      // When the main-thread blocks during a frame, skip the main-thread for
      // the next frame, so that the main-thread can be in sync with the
      // compositor thread again.
      skip_main_thread_next_frame_ = true;
      wait->Signal();
    }
  }

  void DidReceivePresentationTimeOnThread(
      LayerTreeHostImpl* host_impl,
      uint32_t frame_token,
      const gfx::PresentationFeedback& feedback) override {
    ++presented_frames_;
    if (presented_frames_ < config_.animation_frames)
      return;

    auto* dropped_frame_counter = host_impl->dropped_frame_counter();
    DCHECK(dropped_frame_counter);

    total_frames_ = dropped_frame_counter->total_frames();
    dropped_main_ = dropped_frame_counter->total_main_dropped();
    dropped_compositor_ = dropped_frame_counter->total_compositor_dropped();
    dropped_smoothness_ = dropped_frame_counter->total_smoothness_dropped();
    EndTest();
  }

  // Main-thread function overrides:
  void BeginMainFrame(const viz::BeginFrameArgs& args) override {
    if (TestEnded())
      return;

    bool should_wait = false;
    if (config_.should_drop_main_every > 0) {
      should_wait =
          args.frame_id.sequence_number % config_.should_drop_main_every == 0;
    }

    if (should_wait) {
      base::WaitableEvent wait{base::WaitableEvent::ResetPolicy::MANUAL,
                               base::WaitableEvent::InitialState::NOT_SIGNALED};
      {
        base::AutoLock lock(wait_lock_);
        wait_ = &wait;
      }
      wait.Wait();
      {
        base::AutoLock lock(wait_lock_);
        wait_ = nullptr;
      }
    }

    // Make some changes so that the main-thread needs to push updates to the
    // compositor thread (i.e. force a commit).
    auto const bounds = scroll_layer_->bounds();
    scroll_layer_->SetBounds({bounds.width(), bounds.height() + 1});
    if (config_.should_register_main_thread_animation) {
      animation_host()->SetAnimationCounts(1);
      animation_host()->SetCurrentFrameHadRaf(true);
      animation_host()->SetNextFrameHasPendingRaf(true);
    }
  }

 protected:
  // The test configuration options. This is set before the test starts, and
  // remains unchanged after that. So it is safe to read these fields from
  // either threads.
  struct TestConfig {
    uint32_t should_drop_main_every = 0;
    uint32_t animation_frames = 0;
    bool should_register_main_thread_animation = false;
  } config_;

  // The test expectations. This is set before the test starts, and
  // remains unchanged after that. So it is safe to read these fields from
  // either threads.
  struct TestExpectation {
    uint32_t min_dropped_main = 0;
    uint32_t min_dropped_compositor = 0;
    uint32_t min_dropped_smoothness = 0;
  } expect_;

 private:
  // Set up a dummy picture layer so that every begin-main frame requires a
  // commit (without the dummy layer, the main-thread never has to paint, which
  // causes an early 'no damage' abort of the main-frame.
  FakeContentLayerClient client_;
  scoped_refptr<FakePictureLayer> scroll_layer_;

  // This field is used only on the compositor thread to track how many frames
  // have been processed.
  uint32_t presented_frames_ = 0;

  // The |wait_| event is used when the test wants to deliberately force the
  // main-thread to block while processing begin-main-frames.
  base::Lock wait_lock_;
  base::WaitableEvent* wait_ = nullptr;

  // These fields are populated in the compositor thread when the desired number
  // of frames have been processed. These fields are subsequently compared
  // against the expectation after the test ends.
  uint32_t total_frames_ = 0;
  uint32_t dropped_main_ = 0;
  uint32_t dropped_compositor_ = 0;
  uint32_t dropped_smoothness_ = 0;

  bool skip_main_thread_next_frame_ = false;
};

class DroppedFrameCounterNoDropTest : public DroppedFrameCounterTestBase {
 public:
  ~DroppedFrameCounterNoDropTest() override = default;

  void SetUpTestConfigAndExpectations() override {
    config_.animation_frames = 28;
    config_.should_register_main_thread_animation = false;

    expect_.min_dropped_main = 0;
    expect_.min_dropped_compositor = 0;
    expect_.min_dropped_smoothness = 0;
  }
};

MULTI_THREAD_TEST_F(DroppedFrameCounterNoDropTest);

class DroppedFrameCounterMainDropsNoSmoothness
    : public DroppedFrameCounterTestBase {
 public:
  ~DroppedFrameCounterMainDropsNoSmoothness() override = default;

  void SetUpTestConfigAndExpectations() override {
    config_.animation_frames = 28;
    config_.should_drop_main_every = 5;
    config_.should_register_main_thread_animation = false;

    expect_.min_dropped_main = 5;
    expect_.min_dropped_smoothness = 0;
  }
};

// TODO(crbug.com/1115376) Disabled for flakiness.
// MULTI_THREAD_TEST_F(DroppedFrameCounterMainDropsNoSmoothness);

class DroppedFrameCounterMainDropsSmoothnessTest
    : public DroppedFrameCounterTestBase {
 public:
  ~DroppedFrameCounterMainDropsSmoothnessTest() override = default;

  void SetUpTestConfigAndExpectations() override {
    config_.animation_frames = 28;
    config_.should_drop_main_every = 5;
    config_.should_register_main_thread_animation = true;

    expect_.min_dropped_main = 5;
    expect_.min_dropped_smoothness = 5;
  }
};

// TODO(crbug.com/1115376) Disabled for flakiness.
// MULTI_THREAD_TEST_F(DroppedFrameCounterMainDropsSmoothnessTest);

class DroppedFrameCounterTest : public testing::Test {
 public:
  DroppedFrameCounterTest() {
    dropped_frame_counter_.set_total_counter(&total_frame_counter_);
    dropped_frame_counter_.OnFcpReceived();
  }
  ~DroppedFrameCounterTest() override = default;

  // For each boolean in frame_states produces a frame
  void SimulateFrameSequence(std::vector<bool> frame_states, int repeat) {
    for (int i = 0; i < repeat; i++) {
      for (auto is_dropped : frame_states) {
        viz::BeginFrameArgs args_ = SimulateBeginFrameArgs();
        dropped_frame_counter_.OnBeginFrame(args_, /*is_scroll_active=*/false);
        dropped_frame_counter_.OnEndFrame(args_, is_dropped);
        sequence_number_++;
        frame_time_ += interval_;
      }
    }
  }

  void AdvancetimeByIntervals(int interval_count) {
    frame_time_ += interval_ * interval_count;
  }

  double MaxPercentDroppedFrame() {
    return dropped_frame_counter_.sliding_window_max_percent_dropped();
  }

  double PercentDroppedFrame95Percentile() {
    return dropped_frame_counter_.SlidingWindow95PercentilePercentDropped();
  }

  double GetTotalFramesInWindow() {
    return base::TimeDelta::FromSeconds(1) / interval_;
  }

  void SetInterval(base::TimeDelta interval) { interval_ = interval; }

  base::TimeTicks GetNextFrameTime() const { return frame_time_ + interval_; }

 public:
  DroppedFrameCounter dropped_frame_counter_;

 private:
  TotalFrameCounter total_frame_counter_;
  uint64_t sequence_number_ = 1;
  uint64_t source_id_ = 1;
  const base::TickClock* tick_clock_ = base::DefaultTickClock::GetInstance();
  base::TimeTicks frame_time_ = tick_clock_->NowTicks();
  base::TimeDelta interval_ =
      base::TimeDelta::FromMicroseconds(16667);  // 16.667 ms

  viz::BeginFrameArgs SimulateBeginFrameArgs() {
    viz::BeginFrameId current_id_(source_id_, sequence_number_);
    viz::BeginFrameArgs args = viz::BeginFrameArgs();
    args.frame_id = current_id_;
    args.frame_time = frame_time_;
    args.interval = interval_;
    return args;
  }
};

TEST_F(DroppedFrameCounterTest, SimplePattern1) {
  // 2 out of every 3 frames are dropped (In total 80 frames out of 120).
  SimulateFrameSequence({true, true, true, false, true, false}, 20);

  // The max is the following window:
  //    16 * <sequence> + {true, true, true, false
  // Which means a max of 67 dropped frames.
  EXPECT_EQ(std::round(MaxPercentDroppedFrame()), 67);
  EXPECT_EQ(PercentDroppedFrame95Percentile(), 67);  // all values are in the
  // 67th bucket, and as a result 95th percentile is also 67.
}

TEST_F(DroppedFrameCounterTest, SimplePattern2) {
  // 1 out of every 5 frames are dropped (In total 24 frames out of 120).
  SimulateFrameSequence({false, false, false, false, true}, 24);

  double expected_percent_dropped_frame = (12 / GetTotalFramesInWindow()) * 100;
  EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame);
  EXPECT_EQ(PercentDroppedFrame95Percentile(), 20);  // all values are in the
  // 20th bucket, and as a result 95th percentile is also 20.
}

TEST_F(DroppedFrameCounterTest, IncompleteWindow) {
  // There are only 5 frames submitted and both Max and 95pct should report
  // zero.
  SimulateFrameSequence({false, false, false, false, true}, 1);
  EXPECT_EQ(MaxPercentDroppedFrame(), 0.0);
  EXPECT_EQ(PercentDroppedFrame95Percentile(), 0);
}

TEST_F(DroppedFrameCounterTest, MaxPercentDroppedChanges) {
  // First 60 frames have 20% dropped.
  SimulateFrameSequence({false, false, false, false, true}, 12);

  double expected_percent_dropped_frame1 =
      (12 / GetTotalFramesInWindow()) * 100;
  EXPECT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame1);
  EXPECT_FLOAT_EQ(PercentDroppedFrame95Percentile(), 20);  // There is only one
  // element in the histogram and that is 20.

  // 30 new frames are added that have 18 dropped frames.
  // and the 30 frame before that had 6 dropped frames.
  // So in total in the window has 24 frames dropped out of 60 frames.
  SimulateFrameSequence({false, false, true, true, true}, 6);
  double expected_percent_dropped_frame2 =
      (24 / GetTotalFramesInWindow()) * 100;
  EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame2);

  // 30 new frames are added that have 24 dropped frames.
  // and the 30 frame before that had 18 dropped frames.
  // So in total in the window has 42 frames dropped out of 60 frames.
  SimulateFrameSequence({false, true, true, true, true}, 6);
  double expected_percent_dropped_frame3 =
      (42 / GetTotalFramesInWindow()) * 100;
  EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame3);

  // Percent dropped frame of window increases gradually to 70%.
  // 1 value exist when we reach 60 frames and 1 value thereafter for each
  // frame added. So there 61 values in histogram. Last value is 70 (2 sampels)
  // and then 67 with 1 sample, which would be the 95th percentile.
  EXPECT_EQ(PercentDroppedFrame95Percentile(), 67);
}

TEST_F(DroppedFrameCounterTest, MaxPercentDroppedWithIdleFrames) {
  // First 20 frames have 4 frames dropped (20%).
  SimulateFrameSequence({false, false, false, false, true}, 4);

  // Then no frames are added for 20 intervals.
  AdvancetimeByIntervals(20);

  // Then 20 frames have 16 frames dropped (60%).
  SimulateFrameSequence({false, false, true, true, true}, 4);

  // So in total, there are 40 frames in the 1 second window with 16 dropped
  // frames (40% in total).
  double expected_percent_dropped_frame = (16 / GetTotalFramesInWindow()) * 100;
  EXPECT_FLOAT_EQ(MaxPercentDroppedFrame(), expected_percent_dropped_frame);
}

TEST_F(DroppedFrameCounterTest, NoCrashForIntervalLargerThanWindow) {
  SetInterval(base::TimeDelta::FromMilliseconds(1000));
  SimulateFrameSequence({false, false}, 1);
}

TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFrames) {
  // Test scenario:
  //  . 4s of 20% dropped frames.
  //  . 96s of idle time.
  // The 96%ile dropped-frame metric should be 0.

  // Set an interval that rounds up nicely with 1 second.
  constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
  constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
  static_assert(
      kFps % 5 == 0,
      "kFps must be a multiple of 5 because this test depends on it.");
  SetInterval(kInterval);

  const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();

  // First 4 seconds with 20% dropped frames.
  SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);

  // Then no frames are added for 97s. Note that this 1s more than 96 seconds,
  // because the last second remains in the sliding window.
  AdvancetimeByIntervals(kFps * 97);

  // A single frame to flush the pipeline.
  SimulateFrameSequence({false}, 1);

  EXPECT_EQ(histogram->total_count(), 100u * kFps);
  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.96), 0u);
  EXPECT_GT(histogram->GetPercentDroppedFramePercentile(0.97), 0u);
}

TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFramesWhileHidden) {
  // The test scenario is the same as |Percentile95WithIdleFrames| test:
  //  . 4s of 20% dropped frames.
  //  . 96s of idle time.
  // However, the 96s of idle time happens *after* the page becomes invisible
  // (e.g. after a tab-switch). In this case, the idle time *should not*
  // contribute to the sliding window.

  // Set an interval that rounds up nicely with 1 second.
  constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
  constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
  static_assert(
      kFps % 5 == 0,
      "kFps must be a multiple of 5 because this test depends on it.");
  SetInterval(kInterval);

  const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();

  // First 4 seconds with 20% dropped frames.
  SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);

  // Hide the page (thus resetting the pending frames), then idle for 96s before
  // producing a single frame.
  dropped_frame_counter_.ResetPendingFrames(GetNextFrameTime());
  AdvancetimeByIntervals(kFps * 97);

  // A single frame to flush the pipeline.
  SimulateFrameSequence({false}, 1);

  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);
}

TEST_F(DroppedFrameCounterTest, Percentile95WithIdleFramesThenHide) {
  // The test scenario is the same as |Percentile95WithIdleFramesWhileHidden|:
  //  . 4s of 20% dropped frames.
  //  . 96s of idle time.
  // However, the 96s of idle time happens *before* the page becomes invisible
  // (e.g. after a tab-switch). In this case, the idle time *should*
  // contribute to the sliding window.

  // Set an interval that rounds up nicely with 1 second.
  constexpr auto kInterval = base::TimeDelta::FromMilliseconds(10);
  constexpr size_t kFps = base::TimeDelta::FromSeconds(1) / kInterval;
  static_assert(
      kFps % 5 == 0,
      "kFps must be a multiple of 5 because this test depends on it.");
  SetInterval(kInterval);

  const auto* histogram = dropped_frame_counter_.GetSlidingWindowHistogram();

  // First 4 seconds with 20% dropped frames.
  SimulateFrameSequence({false, false, false, false, true}, (kFps / 5) * 4);
  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.95), 20u);

  // Idle for 96s before hiding the page.
  AdvancetimeByIntervals(kFps * 97);
  dropped_frame_counter_.ResetPendingFrames(GetNextFrameTime());
  AdvancetimeByIntervals(kFps * 97);

  // A single frame to flush the pipeline.
  SimulateFrameSequence({false}, 1);

  EXPECT_EQ(histogram->GetPercentDroppedFramePercentile(0.96), 0u);
  EXPECT_GT(histogram->GetPercentDroppedFramePercentile(0.97), 0u);
}

}  // namespace
}  // namespace cc
