[go: nahoru, domu]

Implement new Scroll jank metric.

This CL adds logic to track missed frames and vsyncs as discussed
in the [doc](http://doc/1Y0u0Tq5eUZff75nYUzQVw6JxmbZAW9m64pJidmnGWsY)

We emit two separate histograms:
1) Event.Latency.ScrollUpdate.DelayedFramesPercentage
  Tracks no. of delayed frames.
2) Event.Latency.ScrollUpdate.MissedVsyncCount
  Tracks no. of vsyncs the frames were delayed by.
The histograms gets emitted every `kHistogramEmitFrequency` reported frames.

Bug: b/276722271
Change-Id: I3a96f632e85aa03c407f2f7797674003897cdfa6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4430615
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Reviewed-by: Omar Elmekkawy <mekk@chromium.org>
Commit-Queue: Kartar Singh <kartarsingh@google.com>
Reviewed-by: Stephen Nusko <nuskos@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1146009}
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 3d10a7e..c55566a 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -205,6 +205,8 @@
     "metrics/lcd_text_metrics_reporter.h",
     "metrics/predictor_jank_tracker.cc",
     "metrics/predictor_jank_tracker.h",
+    "metrics/scroll_jank_dropped_frame_tracker.cc",
+    "metrics/scroll_jank_dropped_frame_tracker.h",
     "metrics/shared_metrics_buffer.h",
     "metrics/total_frame_counter.cc",
     "metrics/total_frame_counter.h",
@@ -756,6 +758,7 @@
     "metrics/jank_injector_unittest.cc",
     "metrics/jank_metrics_unittest.cc",
     "metrics/predictor_jank_tracker_unittest.cc",
+    "metrics/scroll_jank_dropped_frame_tracker_unittest.cc",
     "metrics/total_frame_counter_unittest.cc",
     "metrics/video_playback_roughness_reporter_unittest.cc",
     "mojo_embedder/async_layer_tree_frame_sink_unittest.cc",
diff --git a/cc/metrics/compositor_frame_reporter.cc b/cc/metrics/compositor_frame_reporter.cc
index d1060bad..f4bec63 100644
--- a/cc/metrics/compositor_frame_reporter.cc
+++ b/cc/metrics/compositor_frame_reporter.cc
@@ -1359,13 +1359,22 @@
   float total_predicted_delta = 0;
   bool had_gesture_scrolls = false;
 
+  base::TimeTicks input_generation_ts = base::TimeTicks::Max();
+  base::TimeTicks last_coalesced_ts = base::TimeTicks::Min();
   for (const auto& event : events_metrics_) {
     TRACE_EVENT("input", "GestureType", "gesture", event->type());
     switch (event->type()) {
+      // TODO(kartarsingh): Clean up common logic here. Related discussion:
+      // https://crrev.com/c/4430615/19/cc/metrics/compositor_frame_reporter.cc#1393
       case EventMetrics::EventType::kGestureScrollUpdate:
         normal_input_count += event->AsScrollUpdate()->coalesced_event_count();
         total_predicted_delta += event->AsScrollUpdate()->predicted_delta();
         had_gesture_scrolls = true;
+        input_generation_ts = std::min(
+            input_generation_ts, event->GetDispatchStageTimestamp(
+                                     EventMetrics::DispatchStage::kGenerated));
+        last_coalesced_ts = std::max(last_coalesced_ts,
+                                     event->AsScrollUpdate()->last_timestamp());
         break;
       case EventMetrics::EventType::kFirstGestureScrollUpdate:
         normal_input_count += event->AsScrollUpdate()->coalesced_event_count();
@@ -1375,16 +1384,33 @@
               ->ResetCurrentScrollReporting();
         }
         had_gesture_scrolls = true;
+        input_generation_ts = std::min(
+            input_generation_ts, event->GetDispatchStageTimestamp(
+                                     EventMetrics::DispatchStage::kGenerated));
+        last_coalesced_ts = std::max(last_coalesced_ts,
+                                     event->AsScrollUpdate()->last_timestamp());
         break;
       case EventMetrics::EventType::kInertialGestureScrollUpdate:
         fling_input_count += event->AsScrollUpdate()->coalesced_event_count();
         total_predicted_delta += event->AsScrollUpdate()->predicted_delta();
         had_gesture_scrolls = true;
+        input_generation_ts = std::min(
+            input_generation_ts, event->GetDispatchStageTimestamp(
+                                     EventMetrics::DispatchStage::kGenerated));
+        last_coalesced_ts = std::max(last_coalesced_ts,
+                                     event->AsScrollUpdate()->last_timestamp());
         break;
       default:
         continue;
     }
   }
+  // To handle web test failures which causes an event to be coalesced with an
+  // event having null(0) timestamp.
+  // TODO(b/276722271) : Investigate if this needs to be fixed on test side or
+  // if this is a valid scenario.
+  if (last_coalesced_ts.is_null()) {
+    last_coalesced_ts = input_generation_ts;
+  }
 
   TRACE_EVENT("input,input.scrolling", "PresentedFrameInformation",
               [events_metrics = std::cref(events_metrics_), fling_input_count,
@@ -1393,11 +1419,20 @@
                                        normal_input_count, ctx);
               });
 
+  if (!had_gesture_scrolls) {
+    return;
+  }
+
   const auto end_timestamp = viz_breakdown_.presentation_feedback.timestamp;
-  if (had_gesture_scrolls && global_trackers_.predictor_jank_tracker) {
+  if (global_trackers_.predictor_jank_tracker) {
     global_trackers_.predictor_jank_tracker->ReportLatestScrollDelta(
         total_predicted_delta, end_timestamp, args_.interval);
   }
+  if (global_trackers_.scroll_jank_dropped_frame_tracker) {
+    global_trackers_.scroll_jank_dropped_frame_tracker
+        ->ReportLatestPresentationData(input_generation_ts, last_coalesced_ts,
+                                       end_timestamp, args_.interval);
+  }
 }
 
 void CompositorFrameReporter::ReportEventLatencyTraceEvents() const {
diff --git a/cc/metrics/compositor_frame_reporter.h b/cc/metrics/compositor_frame_reporter.h
index 3de2b27..2882003 100644
--- a/cc/metrics/compositor_frame_reporter.h
+++ b/cc/metrics/compositor_frame_reporter.h
@@ -23,6 +23,7 @@
 #include "cc/metrics/frame_info.h"
 #include "cc/metrics/frame_sequence_metrics.h"
 #include "cc/metrics/predictor_jank_tracker.h"
+#include "cc/metrics/scroll_jank_dropped_frame_tracker.h"
 #include "cc/scheduler/scheduler.h"
 #include "components/viz/common/frame_sinks/begin_frame_args.h"
 #include "components/viz/common/frame_timing_details.h"
@@ -45,6 +46,8 @@
   raw_ptr<EventLatencyTracker, DanglingUntriaged> event_latency_tracker =
       nullptr;
   raw_ptr<PredictorJankTracker> predictor_jank_tracker = nullptr;
+  raw_ptr<ScrollJankDroppedFrameTracker> scroll_jank_dropped_frame_tracker =
+      nullptr;
 };
 
 // This is used for tracing and reporting the duration of pipeline stages within
diff --git a/cc/metrics/compositor_frame_reporting_controller.cc b/cc/metrics/compositor_frame_reporting_controller.cc
index 56acbe0..8184e3f5 100644
--- a/cc/metrics/compositor_frame_reporting_controller.cc
+++ b/cc/metrics/compositor_frame_reporting_controller.cc
@@ -13,6 +13,7 @@
 #include "cc/metrics/dropped_frame_counter.h"
 #include "cc/metrics/frame_sequence_tracker_collection.h"
 #include "cc/metrics/latency_ukm_reporter.h"
+#include "cc/metrics/scroll_jank_dropped_frame_tracker.h"
 #include "components/viz/common/frame_timing_details.h"
 #include "components/viz/common/quads/compositor_frame_metadata.h"
 #include "services/tracing/public/cpp/perfetto/macros.h"
@@ -40,6 +41,8 @@
       layer_tree_host_id_(layer_tree_host_id),
       latency_ukm_reporter_(std::make_unique<LatencyUkmReporter>()),
       predictor_jank_tracker_(std::make_unique<PredictorJankTracker>()),
+      scroll_jank_dropped_frame_tracker_(
+          std::make_unique<ScrollJankDroppedFrameTracker>()),
       previous_latency_predictions_main_(base::Microseconds(-1)),
       previous_latency_predictions_impl_(base::Microseconds(-1)),
       event_latency_predictions_(
@@ -51,6 +54,8 @@
     global_trackers_.latency_ukm_reporter = latency_ukm_reporter_.get();
   }
   global_trackers_.predictor_jank_tracker = predictor_jank_tracker_.get();
+  global_trackers_.scroll_jank_dropped_frame_tracker =
+      scroll_jank_dropped_frame_tracker_.get();
 }
 
 CompositorFrameReportingController::~CompositorFrameReportingController() {
diff --git a/cc/metrics/compositor_frame_reporting_controller.h b/cc/metrics/compositor_frame_reporting_controller.h
index cf41349b..4eef09e 100644
--- a/cc/metrics/compositor_frame_reporting_controller.h
+++ b/cc/metrics/compositor_frame_reporting_controller.h
@@ -18,6 +18,7 @@
 #include "cc/metrics/event_metrics.h"
 #include "cc/metrics/frame_sequence_metrics.h"
 #include "cc/metrics/predictor_jank_tracker.h"
+#include "cc/metrics/scroll_jank_dropped_frame_tracker.h"
 
 namespace viz {
 struct FrameTimingDetails;
@@ -200,6 +201,8 @@
   // must outlive the objects in |submitted_compositor_frames_|.
   std::unique_ptr<LatencyUkmReporter> latency_ukm_reporter_;
   std::unique_ptr<PredictorJankTracker> predictor_jank_tracker_;
+  std::unique_ptr<ScrollJankDroppedFrameTracker>
+      scroll_jank_dropped_frame_tracker_;
 
   std::unique_ptr<CompositorFrameReporter>
       reporters_[PipelineStage::kNumPipelineStages];
diff --git a/cc/metrics/scroll_jank_dropped_frame_tracker.cc b/cc/metrics/scroll_jank_dropped_frame_tracker.cc
new file mode 100644
index 0000000..0ea6a79
--- /dev/null
+++ b/cc/metrics/scroll_jank_dropped_frame_tracker.cc
@@ -0,0 +1,78 @@
+// 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 <algorithm>
+
+#include "base/metrics/histogram_macros.h"
+#include "base/trace_event/common/trace_event_common.h"
+#include "base/trace_event/trace_event.h"
+#include "base/trace_event/trace_id_helper.h"
+#include "base/trace_event/typed_macros.h"
+
+namespace cc {
+
+ScrollJankDroppedFrameTracker::ScrollJankDroppedFrameTracker() = default;
+ScrollJankDroppedFrameTracker::~ScrollJankDroppedFrameTracker() = default;
+
+void ScrollJankDroppedFrameTracker::EmitHistogramsAndResetCounters() {
+  DCHECK_EQ(num_presented_frames_, kHistogramEmitFrequency);
+
+  UMA_HISTOGRAM_PERCENTAGE(kDelayedFramesHistogram,
+                           (100 * missed_frames_) / kHistogramEmitFrequency);
+  UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsHistogram, missed_vsyncs_, 1, 50,
+                              25);
+
+  missed_frames_ = 0;
+  missed_vsyncs_ = 0;
+  // We don't need to reset it to -1 because after the first window we always
+  // have a valid previous frame data to compare the first frame of window.
+  num_presented_frames_ = 0;
+}
+
+void ScrollJankDroppedFrameTracker::ReportLatestPresentationData(
+    base::TimeTicks first_input_generation_ts,
+    base::TimeTicks last_input_generation_ts,
+    base::TimeTicks presentation_ts,
+    base::TimeDelta vsync_interval) {
+  DCHECK_GE(last_input_generation_ts, first_input_generation_ts);
+  DCHECK_GT(presentation_ts, last_input_generation_ts);
+  // TODO(b/276722271) : Analyze and reduce these cases of out of order
+  // frame termination.
+  if (presentation_ts <= prev_presentation_ts_) {
+    TRACE_EVENT_INSTANT("input", "OutOfOrderTerminatedFrame");
+    return;
+  }
+
+  // The presentation delta is usually 16.6ms for 60 Hz devices,
+  // but sometimes random errors result in a delta of up to 20ms
+  // as observed in traces. This adds an error margin of 1/2 a
+  // vsync before considering the Vsync missed.
+  bool missed_frame = (presentation_ts - prev_presentation_ts_) >
+                      (vsync_interval + vsync_interval / 2);
+  bool input_available =
+      (first_input_generation_ts - prev_last_input_generation_ts_) <
+      (vsync_interval + vsync_interval / 2);
+  if (missed_frame && input_available) {
+    missed_frames_++;
+    missed_vsyncs_ +=
+        (presentation_ts - prev_presentation_ts_ - (vsync_interval / 2)) /
+        vsync_interval;
+    TRACE_EVENT_INSTANT("input", "MissedFrame", "missed_frames_",
+                        missed_frames_, "missed_vsyncs_", missed_vsyncs_,
+                        "vsync_interval", vsync_interval);
+  }
+
+  ++num_presented_frames_;
+  if (num_presented_frames_ == kHistogramEmitFrequency) {
+    EmitHistogramsAndResetCounters();
+  }
+  DCHECK_LT(num_presented_frames_, kHistogramEmitFrequency);
+
+  prev_presentation_ts_ = presentation_ts;
+  prev_last_input_generation_ts_ = last_input_generation_ts;
+}
+
+}  // namespace cc
diff --git a/cc/metrics/scroll_jank_dropped_frame_tracker.h b/cc/metrics/scroll_jank_dropped_frame_tracker.h
new file mode 100644
index 0000000..56b0c99
--- /dev/null
+++ b/cc/metrics/scroll_jank_dropped_frame_tracker.h
@@ -0,0 +1,55 @@
+// 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.
+
+#ifndef CC_METRICS_SCROLL_JANK_DROPPED_FRAME_TRACKER_H_
+#define CC_METRICS_SCROLL_JANK_DROPPED_FRAME_TRACKER_H_
+
+#include "base/time/time.h"
+#include "cc/cc_export.h"
+
+namespace cc {
+
+class CC_EXPORT ScrollJankDroppedFrameTracker {
+ public:
+  ScrollJankDroppedFrameTracker();
+  ~ScrollJankDroppedFrameTracker();
+
+  ScrollJankDroppedFrameTracker(const ScrollJankDroppedFrameTracker&) = delete;
+
+  void ReportLatestPresentationData(base::TimeTicks first_input_generation_ts,
+                                    base::TimeTicks last_input_generation_ts,
+                                    base::TimeTicks presentation_ts,
+                                    base::TimeDelta vsync_interval);
+
+  static constexpr int kHistogramEmitFrequency = 64;
+  static constexpr const char* kDelayedFramesHistogram =
+      "Event.Jank.DelayedFramesPercentage";
+  static constexpr const char* kMissedVsyncsHistogram =
+      "Event.Jank.MissedVsyncCount";
+
+ private:
+  void EmitHistogramsAndResetCounters();
+
+  // We could have two different frames with same presentation time and due to
+  // this just having previous frame's data is not enough for calculating the
+  // metric.
+  base::TimeTicks prev_presentation_ts_;
+  base::TimeTicks prev_last_input_generation_ts_;
+
+  // Number of frames which were deemed janky.
+  int missed_frames_ = 0;
+  // Number of vsyncs the frames were delayed by. Whenever a frame is missed it
+  // could be delayed >=1 vsyncs, this helps us track how "long" the janks
+  // are.
+  int missed_vsyncs_ = 0;
+
+  // Not initializing with 0 because the first frame in first window will be
+  // always deemed non-janky which makes the metric slightly biased. Setting
+  // it to -1 essentially ignores first frame.
+  int num_presented_frames_ = -1;
+};
+
+}  // namespace cc
+
+#endif  // CC_METRICS_SCROLL_JANK_DROPPED_FRAME_TRACKER_H_
diff --git a/cc/metrics/scroll_jank_dropped_frame_tracker_unittest.cc b/cc/metrics/scroll_jank_dropped_frame_tracker_unittest.cc
new file mode 100644
index 0000000..d3b2a3e
--- /dev/null
+++ b/cc/metrics/scroll_jank_dropped_frame_tracker_unittest.cc
@@ -0,0 +1,198 @@
+// 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 "testing/gtest/include/gtest/gtest.h"
+
+namespace cc {
+
+namespace {
+
+constexpr base::TimeDelta kVsyncInterval = base::Milliseconds(16);
+
+struct FrameTimestamps {
+  base::TimeTicks first_input_ts;
+  base::TimeTicks last_input_ts;
+  base::TimeTicks presentation_ts;
+
+  FrameTimestamps(base::TimeTicks first_input,
+                  base::TimeTicks last_input,
+                  base::TimeTicks presentation)
+      : first_input_ts(first_input),
+        last_input_ts(last_input),
+        presentation_ts(presentation) {}
+
+  FrameTimestamps(int first_input, int last_input, int presentation)
+      : first_input_ts(base::TimeTicks() + base::Milliseconds(first_input)),
+        last_input_ts(base::TimeTicks() + base::Milliseconds(last_input)),
+        presentation_ts(base::TimeTicks() + base::Milliseconds(presentation)) {}
+};
+
+constexpr int kHistogramEmitFrequency =
+    ScrollJankDroppedFrameTracker::kHistogramEmitFrequency;
+constexpr int kFirstWindowSize = kHistogramEmitFrequency + 1;
+constexpr const char* kDelayedFramesHistogram =
+    ScrollJankDroppedFrameTracker::kDelayedFramesHistogram;
+constexpr const char* kMissedVsyncsHistogram =
+    ScrollJankDroppedFrameTracker::kMissedVsyncsHistogram;
+}  // namespace
+
+class ScrollJankDroppedFrameTrackerTest : public testing::Test {
+ public:
+  ScrollJankDroppedFrameTrackerTest() = default;
+
+  void SetUp() override {
+    histogram_tester = std::make_unique<base::HistogramTester>();
+    scroll_jank_dropped_frame_tracker_ =
+        std::make_unique<ScrollJankDroppedFrameTracker>();
+  }
+
+  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;
+
+      scroll_jank_dropped_frame_tracker_->ReportLatestPresentationData(
+          prev_frame.first_input_ts, prev_frame.last_input_ts,
+          prev_frame.presentation_ts, kVsyncInterval);
+    }
+    return prev_frame;
+  }
+
+  void ReportLatestPresentationDataToTracker(const FrameTimestamps& frame) {
+    scroll_jank_dropped_frame_tracker_->ReportLatestPresentationData(
+        frame.first_input_ts, frame.last_input_ts, frame.presentation_ts,
+        kVsyncInterval);
+  }
+
+  std::unique_ptr<base::HistogramTester> histogram_tester;
+
+ private:
+  std::unique_ptr<ScrollJankDroppedFrameTracker>
+      scroll_jank_dropped_frame_tracker_;
+};
+
+TEST_F(ScrollJankDroppedFrameTrackerTest, EmitsHistograms) {
+  FrameTimestamps f1 = {103, 103, 148};
+
+  FrameTimestamps last_frame =
+      ProduceAndReportMockFrames(f1, kHistogramEmitFrequency);
+
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, 0, 0);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 0, 0);
+
+  // For first window we emit histogram at 65th reported frame.
+  last_frame = ProduceAndReportMockFrames(last_frame, 1);
+
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, 0, 1);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 0, 1);
+
+  // For subsequent windows we emit histogram every 64 frames.
+  ProduceAndReportMockFrames(last_frame, kHistogramEmitFrequency);
+
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, 0, 2);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 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<int> inputs = {103, 111, 119, 127};
+  const std::vector<int> vsyncs = {148, 164};
+
+  FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
+  FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};
+
+  ReportLatestPresentationDataToTracker(f1);
+  ReportLatestPresentationDataToTracker(f2);
+
+  // To trigger histogram emission.
+  int frames_to_emit_histogram = kFirstWindowSize - 2;
+  ProduceAndReportMockFrames(f2, frames_to_emit_histogram);
+
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, 0, 1);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 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<int> inputs = {103, 111, 135, 143};
+  const std::vector<int> vsyncs = {148, 180};
+
+  FrameTimestamps f1 = {103, 111, 148};
+  FrameTimestamps f2 = {135, 143, 180};
+
+  ReportLatestPresentationDataToTracker(f1);
+  ReportLatestPresentationDataToTracker(f2);
+
+  // To trigger histogram emission.
+  int frames_to_emit_histogram = kFirstWindowSize - 2;
+  ProduceAndReportMockFrames(f2, frames_to_emit_histogram);
+
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, 0, 1);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 0, 1);
+}
+
+/*
+Test that when a frame took too long to be produced shows up in the metric.
+vsync                   v0              v1
+                        |       |       |
+input   I0  I1  I2  I3
+        |   |   |   |
+F1:     |---------------| {I0, I1}
+F2:             |-----------------------| {I2, I3}
+ */
+TEST_F(ScrollJankDroppedFrameTrackerTest, MissedVsyncWhenInputWasPresent) {
+  const std::vector<int> inputs = {103, 111, 119, 127};
+  const std::vector<int> vsyncs = {148, 196};
+
+  FrameTimestamps f1 = {inputs[0], inputs[1], vsyncs[0]};
+  FrameTimestamps f2 = {inputs[2], inputs[3], vsyncs[1]};
+
+  ReportLatestPresentationDataToTracker(f1);
+  ReportLatestPresentationDataToTracker(f2);
+
+  // To trigger histogram emission.
+  int frames_to_emit_histogram = kFirstWindowSize - 2;
+  FrameTimestamps last_frame_ts =
+      ProduceAndReportMockFrames(f2, frames_to_emit_histogram);
+
+  int expected_missed_frames = 1;
+  int expected_bucket =
+      (100 * expected_missed_frames) / kHistogramEmitFrequency;
+  histogram_tester->ExpectUniqueSample(kDelayedFramesHistogram, expected_bucket,
+                                       1);
+  histogram_tester->ExpectUniqueSample(kMissedVsyncsHistogram, 2, 1);
+
+  // The counters were reset for next set of `kHistogramEmitFrequency` frames.
+  ProduceAndReportMockFrames(last_frame_ts, kHistogramEmitFrequency);
+
+  histogram_tester->ExpectBucketCount(kDelayedFramesHistogram, 0, 1);
+  histogram_tester->ExpectBucketCount(kMissedVsyncsHistogram, 0, 1);
+}
+
+}  // namespace cc