// 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/jank_metrics.h"

#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

#include "base/macros.h"
#include "base/strings/strcat.h"
#include "base/test/metrics/histogram_tester.h"
#include "cc/metrics/frame_sequence_tracker.h"
#include "cc/metrics/throughput_ukm_reporter.h"
#include "cc/trees/ukm_manager.h"
#include "components/ukm/test_ukm_recorder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {
const base::TimeDelta kDefaultFrameInterval =
    base::TimeDelta::FromMillisecondsD(16.67);

// All sequence numbers for simulated frame events will start at this number.
// This makes it easier to numerically distinguish sequence numbers versus
// frame tokens, which always start at 1.
const uint32_t kSequenceNumberStartsAt = 100u;
}  // namespace

namespace cc {

class JankMetricsTest : public testing::Test {
 public:
  JankMetricsTest() = default;
  ~JankMetricsTest() override = default;

  // Simulate a series of Submit, NoUpdate, and Presentation events and notify
  // |jank_reporter|, as specified by |frame_sequences|. The exact presentation
  // time of frames can be slightly manipulated by |presentation_time_shifts|.
  void SimulateFrameSequence(
      JankMetrics* jank_reporter,
      const std::array<std::string, 3>& frame_sequences,
      const std::unordered_map<char, double>& presentation_time_shifts = {}) {
    // |frame_sequences| is an array of 3 strings of EQUAL LENGTH, representing
    // the (S)UBMIT, (N)O-UPDATE, (P)RESENTATION events, respectively. In all 3
    // strings:
    //   any char == a vsync interval (16.67ms) with a unique sequence number.
    //   '-' == no event in this vsync interval.
    // In SUBMIT string:
    //   [a-zA-Z] == A SUBMIT occurs at this vsync interval. Each symbol in this
    //   string must be unique.
    // In NO-UPDATE string:
    //   Any non '-' letter == A NO-UPDATE frame is reported at this vsync
    //   interval.
    // In PRESENTATION string:
    //   [a-zA-Z] == A PRESENTATION occurs at this vsync interval. Each
    //   symbol must be unique and MUST HAVE APPEARED in the SUBMIT string.
    //
    // NOTE this test file stylistically denotes the frames that should jank
    // with uppercases (although this is not a strict).
    //
    // Each item in |presentation_time_shifts| maps a presentation frame letter
    // (must have appeared in string P) to how much time (in ms) this
    // presentation deviates from expected. For example: {'a': -3.2} means frame
    // 'a' is presented 3.2ms before expected.
    //
    // e.g.
    // S = "a-b--c--D--"
    // N = "---**------"
    // P = "-a-b---c-D-"
    // presentation_time_shifts = {'c':-8.4, 'D':8.4}
    //
    // means submit at vsync 0, 2, 5, 8, presentation at 1, 3, 7, 9. Due to the
    // no-update frames 3 and 4, no janks will be reported for 'c'. However, the
    // large fluctuation of presentation time of 'c' and 'D', there is a jank
    // at 'D'.
    //
    // Without the no-update frames and presentation_time_shifts, one jank would
    // have been reported at 'c'.
    auto& submits = frame_sequences[0];
    auto& ignores = frame_sequences[1];
    auto& presnts = frame_sequences[2];

    // All three sequences must have the same size.
    EXPECT_EQ(submits.size(), ignores.size());
    EXPECT_EQ(submits.size(), presnts.size());

    // Map submitted frame to their tokens
    std::unordered_map<char, uint32_t> submit_to_token;

    base::TimeTicks start_time = base::TimeTicks::Now();

    // Scan S to collect all symbols
    for (uint32_t frame_token = 1, i = 0; i < submits.size(); ++i) {
      uint32_t sequence_number = kSequenceNumberStartsAt + i;
      if (submits[i] != '-') {
        submit_to_token[submits[i]] = frame_token;
        jank_reporter->AddSubmitFrame(/*frame_token=*/frame_token,
                                      /*sequence_number=*/sequence_number);
        frame_token++;
      }

      if (ignores[i] != '-') {
        jank_reporter->AddFrameWithNoUpdate(
            /*sequence_number=*/sequence_number,
            /*frame_interval=*/kDefaultFrameInterval);
      }

      if (presnts[i] != '-') {
        // The present frame must have been previously submitted
        EXPECT_EQ(submit_to_token.count(presnts[i]), 1u);

        double presentation_offset = 0.0;  // ms
        if (presentation_time_shifts.count(presnts[i]))
          presentation_offset = presentation_time_shifts.at(presnts[i]);

        jank_reporter->AddPresentedFrame(
            /*presented_frame_token=*/submit_to_token[presnts[i]],
            /*current_presentation_timestamp=*/start_time +
                i * kDefaultFrameInterval +
                base::TimeDelta::FromMillisecondsD(presentation_offset),
            /*frame_interval=*/kDefaultFrameInterval);
        submit_to_token.erase(presnts[i]);
      }
    }
  }
};

TEST_F(JankMetricsTest, CompositorAnimationOneJankWithMildFluctuation) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type =
      FrameSequenceTrackerType::kCompositorAnimation;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kCompositor;
  JankMetrics jank_reporter{tracker_type, thread_type};

  // One Jank; there are no no-update frames. The fluctuation in presentation of
  // 'd' is not big enough to cause another jank.
  SimulateFrameSequence(&jank_reporter,
                        {
                            /*submit   */ "ab-C-d",
                            /*noupdate */ "------",
                            /*present  */ "ab-C-d",
                        },
                        {{'d', +8.0 /*ms*/}});
  jank_reporter.ReportJankMetrics(100u);

  // One sample of 1 janks reported for "Compositor".
  const char* metric =
      "Graphics.Smoothness.Jank.Compositor.CompositorAnimation";
  const char* invalid_metric =
      "Graphics.Smoothness.Jank.Main.CompositorAnimation";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(1, 1)));

  // No reporting for "Main".
  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

TEST_F(JankMetricsTest, MainThreadAnimationOneJankWithNoUpdate) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type =
      FrameSequenceTrackerType::kMainThreadAnimation;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kMain;
  JankMetrics jank_reporter{tracker_type, thread_type};

  // There are only 1 jank because of a no-update frame.

  SimulateFrameSequence(&jank_reporter, {
                                            /*submit   */ "ab-c--D",
                                            /*noupdate */ "--*----",
                                            /*present  */ "ab-c--D",
                                        });
  jank_reporter.ReportJankMetrics(100u);

  // One jank is reported for "Main".
  const char* metric = "Graphics.Smoothness.Jank.Main.MainThreadAnimation";
  const char* invalid_metric =
      "Graphics.Smoothness.Jank.Compositor.MainThreadAnimation";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(1, 1)));

  // No jank is reported for "Compositor"
  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

TEST_F(JankMetricsTest, VideoManyJanksOver300ExpectedFrames) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type = FrameSequenceTrackerType::kVideo;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kCompositor;
  JankMetrics jank_reporter{tracker_type, thread_type};

  // 7 janks.
  SimulateFrameSequence(&jank_reporter,
                        {
                            /*submit   */ "ab-C--DeFGh-IJk---L---------",
                            /*noupdate */ "----------------------------",
                            /*present  */ "---ab-C--De-F--Gh-I---Jk---L",
                        });

  jank_reporter.ReportJankMetrics(300u);

  // Report in the 7/300 ~= 2% bucket for "Compositor"
  const char* metric = "Graphics.Smoothness.Jank.Compositor.Video";
  const char* invalid_metric = "Graphics.Smoothness.Jank.Main.Video";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(2, 1)));

  // No jank is reported for "Main"
  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

TEST_F(JankMetricsTest, WheelScrollMainThreadNoJanksWithNoUpdates) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type =
      FrameSequenceTrackerType::kWheelScroll;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kMain;
  JankMetrics jank_reporter{tracker_type, thread_type};

  SimulateFrameSequence(&jank_reporter,
                        {
                            /*submit   */ "ab-c--d------e---------f-",
                            /*noupdate */ "--*-**-******-*********--",
                            /*present  */ "---ab-c-d-----e---------f",
                        });
  jank_reporter.ReportJankMetrics(100u);

  // Expect 2 janks for "Main" and no jank for "Compositor"
  const char* metric = "Graphics.Smoothness.Jank.Main.WheelScroll";
  const char* invalid_metric =
      "Graphics.Smoothness.Jank.Compositor.WheelScroll";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(0, 1)));

  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

TEST_F(JankMetricsTest, WheelScrollCompositorTwoJanksWithLargeFluctuation) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type =
      FrameSequenceTrackerType::kWheelScroll;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kCompositor;
  JankMetrics jank_reporter{tracker_type, thread_type};

  // Two janks; there are no no-update frames. The fluctuations in presentation
  // of 'C' and 'D' are just big enough to cause another jank.
  SimulateFrameSequence(&jank_reporter,
                        {
                            /*submit   */ "ab-C-D",
                            /*noupdate */ "------",
                            /*present  */ "ab-C-D",
                        },
                        {{'C', -2.0 /*ms*/}, {'D', +7.0 /*ms*/}});
  jank_reporter.ReportJankMetrics(100u);

  // One sample of 2 janks reported for "Compositor".
  const char* metric = "Graphics.Smoothness.Jank.Compositor.WheelScroll";
  const char* invalid_metric = "Graphics.Smoothness.Jank.Main.WheelScroll";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(2, 1)));

  // No reporting for "Main".
  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

TEST_F(JankMetricsTest, TouchScrollCompositorThreadManyJanksLongLatency) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type =
      FrameSequenceTrackerType::kTouchScroll;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kCompositor;

  JankMetrics jank_reporter{tracker_type, thread_type};

  // There are long delays from submit to presentations.
  SimulateFrameSequence(
      &jank_reporter, {
                          /*submit   */ "abB-c--D--EFgH----------------------",
                          /*noupdate */ "---*--------------------------------",
                          /*present  */ "----------ab-B--c---D----E-----Fg--H",
                      });
  jank_reporter.ReportJankMetrics(120u);

  // Expect janks in the 5/120 ~= 4% bucket for "Compositor", and no jank
  // for "Main"
  const char* metric = "Graphics.Smoothness.Jank.Compositor.TouchScroll";
  const char* invalid_metric = "Graphics.Smoothness.Jank.Main.TouchScroll";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(4, 1)));

  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

// Test if the jank reporter can correctly merge janks from another jank
// reporter.
TEST_F(JankMetricsTest, RAFMergeJanks) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type = FrameSequenceTrackerType::kRAF;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kMain;

  JankMetrics jank_reporter{tracker_type, thread_type};
  std::unique_ptr<JankMetrics> other_reporter =
      std::make_unique<JankMetrics>(tracker_type, thread_type);

  std::array<std::string, 3> seqs = {
      /*submit   */ "a-b-Cd-e-F--D-",
      /*noupdate */ "-*----*-------",
      /*present  */ "-a-b-Cd-e-F--D",
  };
  SimulateFrameSequence(&jank_reporter, seqs);
  SimulateFrameSequence(other_reporter.get(), seqs);

  jank_reporter.Merge(std::move(other_reporter));
  jank_reporter.ReportJankMetrics(100u);

  // Expect 6 janks for "Main" (3 from each reporter)
  const char* metric = "Graphics.Smoothness.Jank.Main.RAF";
  const char* invalid_metric = "Graphics.Smoothness.Jank.Compositor.RAF";

  histogram_tester.ExpectTotalCount(metric, 1u);
  EXPECT_THAT(histogram_tester.GetAllSamples(metric),
              testing::ElementsAre(base::Bucket(6, 1)));

  histogram_tester.ExpectTotalCount(invalid_metric, 0u);
}

// Test if jank reporting is correctly disabled for Custom trackers.
TEST_F(JankMetricsTest, CustomNotReported) {
  base::HistogramTester histogram_tester;
  FrameSequenceTrackerType tracker_type = FrameSequenceTrackerType::kCustom;
  FrameSequenceMetrics::ThreadType thread_type =
      FrameSequenceMetrics::ThreadType::kMain;
  JankMetrics jank_reporter{tracker_type, thread_type};

  // There should be 4 janks, but the jank reporter does not track or report
  // them.
  SimulateFrameSequence(&jank_reporter, {
                                            /*submit   */ "ab-C--D---E----F",
                                            /*noupdate */ "----------------",
                                            /*present  */ "ab-C--D---E----F",
                                        });
  jank_reporter.ReportJankMetrics(100u);

  // Expect no jank reports even though the sequence contains jank
  histogram_tester.ExpectTotalCount("Graphics.Smoothness.Jank.Main.Custom", 0u);
  histogram_tester.ExpectTotalCount(
      "Graphics.Smoothness.Jank.Compositor.Custom", 0u);
}

}  // namespace cc
