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

#include "cc/metrics/scroll_jank_dropped_frame_tracker.h"

#include <memory>
#include <vector>

#include "base/test/metrics/histogram_tester.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "cc/metrics/event_metrics.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace cc {

namespace {

constexpr base::TimeDelta kVsyncInterval = base::Milliseconds(16);

constexpr base::TimeTicks MillisSinceEpoch(int64_t millis) {
  return base::TimeTicks() + base::Milliseconds(millis);
}

struct FrameTimestamps {
  base::TimeTicks first_input_ts;
  base::TimeTicks last_input_ts;
  base::TimeTicks presentation_ts;
};

constexpr int kHistogramEmitFrequency =
    ScrollJankDroppedFrameTracker::kHistogramEmitFrequency;
constexpr int kFirstWindowSize = kHistogramEmitFrequency + 1;
constexpr const char* kDelayedFramesWindowHistogram =
    ScrollJankDroppedFrameTracker::kDelayedFramesWindowHistogram;
constexpr const char* kDelayedFramesPerScrollHistogram =
    ScrollJankDroppedFrameTracker::kDelayedFramesPerScrollHistogram;
constexpr const char* kMissedVsyncsSumInWindowHistogram =
    ScrollJankDroppedFrameTracker::kMissedVsyncsSumInWindowHistogram;
constexpr const char* kMissedVsyncsMaxInWindowHistogram =
    ScrollJankDroppedFrameTracker::kMissedVsyncsMaxInWindowHistogram;
constexpr const char* kMissedVsyncsSumPerScrollHistogram =
    ScrollJankDroppedFrameTracker::kMissedVsyncsSumPerScrollHistogram;
constexpr const char* kMissedVsyncsMaxPerScrollHistogram =
    ScrollJankDroppedFrameTracker::kMissedVsyncsMaxPerScrollHistogram;
constexpr const char* kMissedVsyncsPerFrameHistogram =
    ScrollJankDroppedFrameTracker::kMissedVsyncsPerFrameHistogram;
}  // namespace

class ScrollJankDroppedFrameTrackerTest : public testing::Test {
 public:
  ScrollJankDroppedFrameTrackerTest() = default;

  void SetUp() override {
    ResetHistogramTester();
    scroll_jank_dropped_frame_tracker_ =
        std::make_unique<ScrollJankDroppedFrameTracker>();
    scroll_jank_dropped_frame_tracker_->OnScrollStarted();
  }

  void ResetHistogramTester() {
    histogram_tester = std::make_unique<base::HistogramTester>();
  }

  FrameTimestamps ProduceAndReportMockFrames(FrameTimestamps prev_frame,
                                             int num_frames) {
    for (int i = 1; i <= num_frames; i++) {
      prev_frame.first_input_ts += kVsyncInterval;
      prev_frame.last_input_ts += kVsyncInterval;
      prev_frame.presentation_ts += kVsyncInterval;
      ReportLatestPresentationDataToTracker(prev_frame);
    }
    return prev_frame;
  }

  void ReportLatestPresentationDataToTracker(const FrameTimestamps& frame) {
    base::SimpleTestTickClock tick_clock;
    tick_clock.SetNowTicks(frame.first_input_ts);
    auto event = ScrollUpdateEventMetrics::CreateForTesting(
        ui::EventType::kGestureScrollUpdate, ui::ScrollInputType::kWheel,
        /*is_inertial=*/false,
        ScrollUpdateEventMetrics::ScrollUpdateType::kContinued,
        /*delta=*/10.0f, frame.first_input_ts, base::TimeTicks(), &tick_clock,
        /*trace_id=*/std::nullopt,
        /*scroll_begin_generated_timestamp=*/MillisSinceEpoch(1),
        /*scroll_begin_arrival_timestamp=*/MillisSinceEpoch(2));
    scroll_jank_dropped_frame_tracker_->ReportLatestPresentationData(
        *event, frame.last_input_ts, frame.presentation_ts, kVsyncInterval);
  }

  std::unique_ptr<base::HistogramTester> histogram_tester;

 protected:
  std::unique_ptr<ScrollJankDroppedFrameTracker>
      scroll_jank_dropped_frame_tracker_;
};

TEST_F(ScrollJankDroppedFrameTrackerTest, EmitsHistograms) {
  FrameTimestamps f1 = {MillisSinceEpoch(103), MillisSinceEpoch(103),
                        MillisSinceEpoch(148)};

  FrameTimestamps last_frame =
      ProduceAndReportMockFrames(f1, kHistogramEmitFrequency);

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram, 0, 0);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram, 0, 0);

  // For first window we emit histogram at 65th reported frame.
  last_frame = ProduceAndReportMockFrames(last_frame, 1);

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram, 0, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram, 0, 1);

  // For subsequent windows we emit histogram every 64 frames.
  ProduceAndReportMockFrames(last_frame, kHistogramEmitFrequency);

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram, 0, 2);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram, 0, 2);
}

/*
Test that regular frame production doesn't cause missed frames.
vsync                   v0      v1
                        |       |
input   I0  I1  I2  I3
        |   |   |   |
F1:     |---------------| {I0, I1}
F2:             |---------------| {I2, I3}
 */
TEST_F(ScrollJankDroppedFrameTrackerTest, FrameProducedEveryVsync) {
  const std::vector<base::TimeTicks> inputs = {
      MillisSinceEpoch(103), MillisSinceEpoch(111), MillisSinceEpoch(119),
      MillisSinceEpoch(127)};
  const std::vector<base::TimeTicks> vsyncs = {MillisSinceEpoch(148),
                                               MillisSinceEpoch(164)};

  FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
  FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};

  ReportLatestPresentationDataToTracker(f1);
  ReportLatestPresentationDataToTracker(f2);

  // To trigger per window histogram emission.
  int frames_to_emit_histogram = kFirstWindowSize - 2;
  ProduceAndReportMockFrames(f2, frames_to_emit_histogram);

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram, 0, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram, 0, 1);
}

/*
Test that sporadic input timing doesn't cause missed frames when no
frame is expected.
vsync                       v0              v1
                    |       |       |       |
input   I0  I1        I2  I3
        |   |         |   |
F1:     |-------------------| {I0, I1}
F2:                   |---------------------| {I2, I3}
 */
TEST_F(ScrollJankDroppedFrameTrackerTest, NoFrameProducedForMissingInput) {
  const std::vector<base::TimeTicks> inputs = {
      MillisSinceEpoch(103), MillisSinceEpoch(111), MillisSinceEpoch(135),
      MillisSinceEpoch(143)};
  const std::vector<base::TimeTicks> vsyncs = {MillisSinceEpoch(148),
                                               MillisSinceEpoch(180)};

  FrameTimestamps f1 = {MillisSinceEpoch(103), MillisSinceEpoch(111),
                        MillisSinceEpoch(148)};
  FrameTimestamps f2 = {MillisSinceEpoch(135), MillisSinceEpoch(143),
                        MillisSinceEpoch(180)};

  ReportLatestPresentationDataToTracker(f1);
  ReportLatestPresentationDataToTracker(f2);

  // To trigger per window histogram emission.
  int frames_to_emit_histogram = kFirstWindowSize - 2;
  ProduceAndReportMockFrames(f2, frames_to_emit_histogram);

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram, 0, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram, 0, 1);
}

/*
Test that when a frame took too long to be produced shows up in the metric.
vsync                   v0              v1        v2
                        |    |    |     |    |    |
input   I0  I1  I2  I3  I4  I5
        |   |   |   |   |   |
F1:     |---------------| {I0, I1}
F2:             |-----------------------| {I2, I3}
F3:                     |-------------------------| {I4, I5}
 */
TEST_F(ScrollJankDroppedFrameTrackerTest, MissedVsyncWhenInputWasPresent) {
  const std::vector<base::TimeTicks> inputs = {
      MillisSinceEpoch(103), MillisSinceEpoch(111), MillisSinceEpoch(119),
      MillisSinceEpoch(127), MillisSinceEpoch(135), MillisSinceEpoch(143)};
  const std::vector<base::TimeTicks> vsyncs = {
      MillisSinceEpoch(148), MillisSinceEpoch(196), MillisSinceEpoch(228)};

  FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
  FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};
  FrameTimestamps f3 = {inputs[4], inputs[5], vsyncs[2]};

  ReportLatestPresentationDataToTracker(f1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsPerFrameHistogram, 0, 1);
  ReportLatestPresentationDataToTracker(f2);
  histogram_tester->ExpectBucketCount(kMissedVsyncsPerFrameHistogram, 2, 1);
  ReportLatestPresentationDataToTracker(f3);
  histogram_tester->ExpectBucketCount(kMissedVsyncsPerFrameHistogram, 1, 1);

  // To trigger per window histogram emission.
  int frames_to_emit_histogram = kFirstWindowSize - 3;
  FrameTimestamps last_frame_ts =
      ProduceAndReportMockFrames(f3, frames_to_emit_histogram);
  histogram_tester->ExpectBucketCount(kMissedVsyncsPerFrameHistogram, 0, 63);

  // F2 and F3 are janky frames.
  const int expected_missed_frames = 2;
  const int expected_delayed_frames_percentage =
      (100 * expected_missed_frames) / kHistogramEmitFrequency;
  // Frame F2 missed 2 vsyncs, F3 missed 1 vsync.
  const int expected_max = 2;
  const int expected_sum = 3;

  histogram_tester->ExpectUniqueSample(kDelayedFramesWindowHistogram,
                                       expected_delayed_frames_percentage, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumInWindowHistogram,
                                       expected_sum, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsMaxInWindowHistogram,
                                       expected_max, 1);

  // The counters were reset for next set of `kHistogramEmitFrequency` frames.
  ProduceAndReportMockFrames(last_frame_ts, kHistogramEmitFrequency);

  histogram_tester->ExpectBucketCount(kDelayedFramesWindowHistogram, 0, 1);
  histogram_tester->ExpectBucketCount(kMissedVsyncsSumInWindowHistogram, 0, 1);
  histogram_tester->ExpectBucketCount(kMissedVsyncsMaxInWindowHistogram, 0, 1);
  // Other non-zero buckets for histogram were tested earlier in the code.
  histogram_tester->ExpectBucketCount(kMissedVsyncsPerFrameHistogram, 0, 127);
}

/*
Tests that the scroll jank metric's histograms for a scroll are emitted at the
beginning of the next scroll.
vsync                   v0              v1        v2
                        |    |    |     |    |    |
input   I0  I1  I2  I3  I4  I5
        |   |   |   |   |   |
F1:     |---------------| {I0, I1}
F2:             |-----------------------| {I2, I3}
F3:                     |-------------------------| {I4, I5}
*/
TEST_F(ScrollJankDroppedFrameTrackerTest,
       ShouldEmitMetricsAtStartOfNextScroll) {
  const std::vector<base::TimeTicks> inputs = {
      MillisSinceEpoch(103), MillisSinceEpoch(111), MillisSinceEpoch(119),
      MillisSinceEpoch(127), MillisSinceEpoch(135), MillisSinceEpoch(143)};
  const std::vector<base::TimeTicks> vsyncs = {
      MillisSinceEpoch(148), MillisSinceEpoch(196), MillisSinceEpoch(228)};

  FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
  FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};
  FrameTimestamps f3 = {inputs[4], inputs[5], vsyncs[2]};

  ReportLatestPresentationDataToTracker(f1);
  ReportLatestPresentationDataToTracker(f2);
  ReportLatestPresentationDataToTracker(f3);
  const int total_frames = 10;
  ProduceAndReportMockFrames(f3, total_frames - 3);

  histogram_tester->ExpectTotalCount(kMissedVsyncsSumPerScrollHistogram, 0);
  histogram_tester->ExpectTotalCount(kMissedVsyncsMaxPerScrollHistogram, 0);
  histogram_tester->ExpectTotalCount(kDelayedFramesPerScrollHistogram, 0);

  // The tracker should emit all metrics at the beginning of the next scroll.
  ResetHistogramTester();
  scroll_jank_dropped_frame_tracker_->OnScrollStarted();

  // F2 and F3 are janky frames.
  const int expected_missed_frames = 2;
  const int expected_delayed_frames_percentage =
      (100 * expected_missed_frames) / total_frames;
  // Frame F2 missed 2 vsyncs, F3 missed 1 vsync.
  const int expected_max = 2;
  const int expected_sum = 3;

  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumPerScrollHistogram,
                                       expected_sum, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsMaxPerScrollHistogram,
                                       expected_max, 1);
  histogram_tester->ExpectUniqueSample(kDelayedFramesPerScrollHistogram,
                                       expected_delayed_frames_percentage, 1);

  // The tracker SHOULDN'T emit any more metrics when it's destroyed.
  ResetHistogramTester();
  delete scroll_jank_dropped_frame_tracker_.release();

  histogram_tester->ExpectTotalCount(kMissedVsyncsSumPerScrollHistogram, 0);
  histogram_tester->ExpectTotalCount(kMissedVsyncsMaxPerScrollHistogram, 0);
  histogram_tester->ExpectTotalCount(kDelayedFramesPerScrollHistogram, 0);
}

/*
Tests for the scroll jank metric's per-scroll histograms. To avoid duplication,
all per-scroll tests use the same scenario depicted below.
vsync                   v0              v1        v2
                        |    |    |     |    |    |
input   I0  I1  I2  I3  I4  I5
        |   |   |   |   |    |
F1:     |---------------| {I0, I1}
F2:             |-----------------------| {I2, I3}
F3:                     |-------------------------| {I4, I5}
*/
class ScrollJankDroppedFrameTrackerPerScrollTest
    : public ScrollJankDroppedFrameTrackerTest {
 public:
  ScrollJankDroppedFrameTrackerPerScrollTest() = default;

  void ProduceAndReportScrollFrames() {
    const std::vector<base::TimeTicks> inputs = {
        MillisSinceEpoch(103), MillisSinceEpoch(111), MillisSinceEpoch(119),
        MillisSinceEpoch(127), MillisSinceEpoch(135), MillisSinceEpoch(143)};
    const std::vector<base::TimeTicks> vsyncs = {
        MillisSinceEpoch(148), MillisSinceEpoch(196), MillisSinceEpoch(228)};

    FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
    FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};
    FrameTimestamps f3 = {inputs[4], inputs[5], vsyncs[2]};

    ReportLatestPresentationDataToTracker(f1);
    ReportLatestPresentationDataToTracker(f2);
    ReportLatestPresentationDataToTracker(f3);

    ProduceAndReportMockFrames(f3, total_frames - 3);
  }

  static const int total_frames = 10;

  // F2 and F3 are janky frames.
  static const int expected_missed_frames = 2;
  static const int expected_delayed_frames_percentage =
      (100 * expected_missed_frames) / 10;

  // Frame F2 missed 2 vsyncs, F3 missed 1 vsync.
  static const int expected_missed_vsyncs_sum = 3;
  static const int expected_missed_vsyncs_max = 2;
};

/*
Tests that the scroll jank metric's histograms for a scroll are emitted when the
tracker is destroyed.
*/
TEST_F(ScrollJankDroppedFrameTrackerPerScrollTest,
       ShouldEmitMetricsWhenDestroyed) {
  ProduceAndReportScrollFrames();

  // The tracker should emit all metrics when it's destroyed.
  delete scroll_jank_dropped_frame_tracker_.release();

  histogram_tester->ExpectUniqueSample(kMissedVsyncsSumPerScrollHistogram,
                                       expected_missed_vsyncs_sum, 1);
  histogram_tester->ExpectUniqueSample(kMissedVsyncsMaxPerScrollHistogram,
                                       expected_missed_vsyncs_max, 1);
  histogram_tester->ExpectUniqueSample(kDelayedFramesPerScrollHistogram,
                                       expected_delayed_frames_percentage, 1);
}

}  // namespace cc
