Add MissedDeadlineFrames to track frames that miss the vsync deadline

This change adds a MissedDeadlineFrames UMA that tracks frames that
miss the vsync deadline during scroll/animations.

Bug: 1092938
Change-Id: I044985583193d94c90485f1308be5645d553af7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2343642
Commit-Queue: Matthew Crabill <[email protected]>
Reviewed-by: Sadrul Chowdhury <[email protected]>
Reviewed-by: Xida Chen <[email protected]>
Reviewed-by: David Bokan <[email protected]>
Reviewed-by: Behdad Bakhshinategh <[email protected]>
Reviewed-by: Stephen Chenney <[email protected]>
Cr-Commit-Position: refs/heads/master@{#807596}
diff --git a/cc/metrics/frame_sequence_metrics.cc b/cc/metrics/frame_sequence_metrics.cc
index 67d182b..01c2a5f 100644
--- a/cc/metrics/frame_sequence_metrics.cc
+++ b/cc/metrics/frame_sequence_metrics.cc
@@ -51,6 +51,13 @@
        FrameSequenceTracker::GetFrameSequenceTrackerTypeName(type)});
 }
 
+std::string GetMissedDeadlineHistogramName(FrameSequenceTrackerType type,
+                                           const char* thread_name) {
+  return base::StrCat(
+      {"Graphics.Smoothness.PercentMissedDeadlineFrames.", thread_name, ".",
+       FrameSequenceTracker::GetFrameSequenceTrackerTypeName(type)});
+}
+
 std::string GetFrameSequenceLengthHistogramName(FrameSequenceTrackerType type) {
   return base::StrCat(
       {"Graphics.Smoothness.FrameSequenceLength.",
@@ -206,6 +213,10 @@
   DCHECK_LE(main_throughput_.frames_produced, main_throughput_.frames_expected);
   DCHECK_LE(aggregated_throughput_.frames_produced,
             aggregated_throughput_.frames_expected);
+  DCHECK_LE(impl_throughput_.frames_ontime, impl_throughput_.frames_expected);
+  DCHECK_LE(main_throughput_.frames_ontime, main_throughput_.frames_expected);
+  DCHECK_LE(aggregated_throughput_.frames_ontime,
+            aggregated_throughput_.frames_expected);
 
   // Terminates |trace_data_| for all types of FrameSequenceTracker.
   trace_data_.Terminate();
@@ -221,50 +232,94 @@
     return;
   }
 
+  const bool main_report = ThroughputData::CanReportHistogram(
+      this, ThreadType::kMain, main_throughput_);
+  const bool compositor_report = ThroughputData::CanReportHistogram(
+      this, ThreadType::kCompositor, impl_throughput_);
+
+  base::Optional<int> impl_throughput_percent_dropped;
+  base::Optional<int> impl_throughput_percent_missed;
+  base::Optional<int> main_throughput_percent_dropped;
+  base::Optional<int> main_throughput_percent_missed;
+
   // Report the throughput metrics.
-  base::Optional<int> impl_throughput_percent = ThroughputData::ReportHistogram(
-      this, ThreadType::kCompositor,
-      GetIndexForMetric(FrameSequenceMetrics::ThreadType::kCompositor, type_),
-      impl_throughput_);
-  base::Optional<int> main_throughput_percent = ThroughputData::ReportHistogram(
-      this, ThreadType::kMain,
-      GetIndexForMetric(FrameSequenceMetrics::ThreadType::kMain, type_),
-      main_throughput_);
+  if (compositor_report) {
+    impl_throughput_percent_dropped =
+        ThroughputData::ReportDroppedFramePercentHistogram(
+            this, ThreadType::kCompositor,
+            GetIndexForMetric(FrameSequenceMetrics::ThreadType::kCompositor,
+                              type_),
+            impl_throughput_);
+    impl_throughput_percent_missed =
+        ThroughputData::ReportMissedDeadlineFramePercentHistogram(
+            this, ThreadType::kCompositor,
+            GetIndexForMetric(FrameSequenceMetrics::ThreadType::kCompositor,
+                              type_),
+            impl_throughput_);
+  }
+  if (main_report) {
+    main_throughput_percent_dropped =
+        ThroughputData::ReportDroppedFramePercentHistogram(
+            this, ThreadType::kMain,
+            GetIndexForMetric(FrameSequenceMetrics::ThreadType::kMain, type_),
+            main_throughput_);
+    main_throughput_percent_missed =
+        ThroughputData::ReportMissedDeadlineFramePercentHistogram(
+            this, ThreadType::kMain,
+            GetIndexForMetric(FrameSequenceMetrics::ThreadType::kMain, type_),
+            main_throughput_);
+  }
 
   // Report for the 'scrolling thread' for the scrolling interactions.
   if (scrolling_thread_ != ThreadType::kUnknown) {
-    base::Optional<int> scrolling_thread_throughput;
+    base::Optional<int> scrolling_thread_throughput_dropped;
+    base::Optional<int> scrolling_thread_throughput_missed;
     switch (scrolling_thread_) {
       case ThreadType::kCompositor:
-        scrolling_thread_throughput = impl_throughput_percent;
+        scrolling_thread_throughput_dropped = impl_throughput_percent_dropped;
+        scrolling_thread_throughput_missed = impl_throughput_percent_missed;
         break;
       case ThreadType::kMain:
-        scrolling_thread_throughput = main_throughput_percent;
+        scrolling_thread_throughput_dropped = main_throughput_percent_dropped;
+        scrolling_thread_throughput_missed = main_throughput_percent_missed;
         break;
       case ThreadType::kUnknown:
         NOTREACHED();
         break;
     }
-    if (scrolling_thread_throughput.has_value()) {
-      // It's OK to use the UMA histogram in the following code while still
-      // using |GetThroughputHistogramName()| to get the name of the metric,
-      // since the input-params to the function never change at runtime.
+    // It's OK to use the UMA histogram in the following code while still
+    // using |GetThroughputHistogramName()| to get the name of the metric,
+    // since the input-params to the function never change at runtime.
+    if (scrolling_thread_throughput_dropped.has_value() &&
+        scrolling_thread_throughput_missed.has_value()) {
       if (type_ == FrameSequenceTrackerType::kWheelScroll) {
         UMA_HISTOGRAM_PERCENTAGE(
             GetThroughputHistogramName(FrameSequenceTrackerType::kWheelScroll,
                                        "ScrollingThread"),
-            scrolling_thread_throughput.value());
+            scrolling_thread_throughput_dropped.value());
+        UMA_HISTOGRAM_PERCENTAGE(
+            GetMissedDeadlineHistogramName(
+                FrameSequenceTrackerType::kWheelScroll, "ScrollingThread"),
+            scrolling_thread_throughput_missed.value());
       } else if (type_ == FrameSequenceTrackerType::kTouchScroll) {
         UMA_HISTOGRAM_PERCENTAGE(
             GetThroughputHistogramName(FrameSequenceTrackerType::kTouchScroll,
                                        "ScrollingThread"),
-            scrolling_thread_throughput.value());
+            scrolling_thread_throughput_dropped.value());
+        UMA_HISTOGRAM_PERCENTAGE(
+            GetMissedDeadlineHistogramName(
+                FrameSequenceTrackerType::kTouchScroll, "ScrollingThread"),
+            scrolling_thread_throughput_missed.value());
       } else {
         DCHECK_EQ(type_, FrameSequenceTrackerType::kScrollbarScroll);
         UMA_HISTOGRAM_PERCENTAGE(
             GetThroughputHistogramName(
                 FrameSequenceTrackerType::kScrollbarScroll, "ScrollingThread"),
-            scrolling_thread_throughput.value());
+            scrolling_thread_throughput_dropped.value());
+        UMA_HISTOGRAM_PERCENTAGE(
+            GetMissedDeadlineHistogramName(
+                FrameSequenceTrackerType::kScrollbarScroll, "ScrollingThread"),
+            scrolling_thread_throughput_missed.value());
       }
     }
   }
@@ -315,10 +370,9 @@
     jank_reporter_->AddPresentedFrame(presentation_time, frame_interval);
 }
 
-base::Optional<int> FrameSequenceMetrics::ThroughputData::ReportHistogram(
+bool FrameSequenceMetrics::ThroughputData::CanReportHistogram(
     FrameSequenceMetrics* metrics,
     ThreadType thread_type,
-    int metric_index,
     const ThroughputData& data) {
   const auto sequence_type = metrics->type();
   DCHECK_LT(sequence_type, FrameSequenceTrackerType::kMaxType);
@@ -326,7 +380,26 @@
   // All video frames are compositor thread only.
   if (sequence_type == FrameSequenceTrackerType::kVideo &&
       thread_type == ThreadType::kMain)
-    return base::nullopt;
+    return false;
+
+  if (data.frames_expected < kMinFramesForThroughputMetric)
+    return false;
+
+  const bool is_animation =
+      ShouldReportForAnimation(sequence_type, thread_type);
+
+  return is_animation || IsInteractionType(sequence_type) ||
+         sequence_type == FrameSequenceTrackerType::kVideo;
+}
+
+int FrameSequenceMetrics::ThroughputData::ReportDroppedFramePercentHistogram(
+    FrameSequenceMetrics* metrics,
+    ThreadType thread_type,
+    int metric_index,
+    const ThroughputData& data) {
+  const auto sequence_type = metrics->type();
+  DCHECK_LT(sequence_type, FrameSequenceTrackerType::kMaxType);
+  DCHECK(CanReportHistogram(metrics, thread_type, data));
 
   if (metrics->GetEffectiveThread() == thread_type) {
     STATIC_HISTOGRAM_POINTER_GROUP(
@@ -339,9 +412,6 @@
             base::HistogramBase::kUmaTargetedHistogramFlag));
   }
 
-  if (data.frames_expected < kMinFramesForThroughputMetric)
-    return base::nullopt;
-
   // Throughput means the percent of frames that was expected to show on the
   // screen but didn't. In other words, the lower the throughput is, the
   // smoother user experience.
@@ -393,15 +463,9 @@
     }
   }
 
-  if (!is_animation && !IsInteractionType(sequence_type) &&
-      sequence_type != FrameSequenceTrackerType::kVideo) {
-    return base::nullopt;
-  }
-
-  const char* thread_name =
-      thread_type == ThreadType::kCompositor
-          ? "CompositorThread"
-          : thread_type == ThreadType::kMain ? "MainThread" : "SlowerThread";
+  const char* thread_name = thread_type == ThreadType::kCompositor
+                                ? "CompositorThread"
+                                : "MainThread";
   STATIC_HISTOGRAM_POINTER_GROUP(
       GetThroughputHistogramName(sequence_type, thread_name), metric_index,
       kMaximumHistogramIndex, Add(percent),
@@ -411,6 +475,66 @@
   return percent;
 }
 
+int FrameSequenceMetrics::ThroughputData::
+    ReportMissedDeadlineFramePercentHistogram(FrameSequenceMetrics* metrics,
+                                              ThreadType thread_type,
+                                              int metric_index,
+                                              const ThroughputData& data) {
+  const auto sequence_type = metrics->type();
+  DCHECK_LT(sequence_type, FrameSequenceTrackerType::kMaxType);
+
+  // Throughput means the percent of frames that was expected to show on the
+  // screen but didn't. In other words, the lower the throughput is, the
+  // smoother user experience.
+  const int percent = data.MissedDeadlineFramePercent();
+
+  const bool is_animation =
+      ShouldReportForAnimation(sequence_type, thread_type);
+  const bool is_interaction = ShouldReportForInteraction(metrics, thread_type);
+
+  if (is_animation) {
+    TRACE_EVENT_INSTANT2(
+        "cc,benchmark", "PercentMissedDeadlineFrames.AllAnimations",
+        TRACE_EVENT_SCOPE_THREAD, "frames_expected", data.frames_expected,
+        "frames_ontime", data.frames_ontime);
+
+    UMA_HISTOGRAM_PERCENTAGE(
+        "Graphics.Smoothness.PercentMissedDeadlineFrames.AllAnimations",
+        percent);
+  }
+
+  if (is_interaction) {
+    TRACE_EVENT_INSTANT2(
+        "cc,benchmark", "PercentMissedDeadlineFrames.AllInteractions",
+        TRACE_EVENT_SCOPE_THREAD, "frames_expected", data.frames_expected,
+        "frames_ontime", data.frames_ontime);
+    UMA_HISTOGRAM_PERCENTAGE(
+        "Graphics.Smoothness.PercentMissedDeadlineFrames.AllInteractions",
+        percent);
+  }
+
+  if (is_animation || is_interaction) {
+    TRACE_EVENT_INSTANT2(
+        "cc,benchmark", "PercentMissedDeadlineFrames.AllSequences",
+        TRACE_EVENT_SCOPE_THREAD, "frames_expected", data.frames_expected,
+        "frames_ontime", data.frames_ontime);
+    UMA_HISTOGRAM_PERCENTAGE(
+        "Graphics.Smoothness.PercentMissedDeadlineFrames.AllSequences",
+        percent);
+  }
+
+  const char* thread_name = thread_type == ThreadType::kCompositor
+                                ? "CompositorThread"
+                                : "MainThread";
+  STATIC_HISTOGRAM_POINTER_GROUP(
+      GetMissedDeadlineHistogramName(sequence_type, thread_name), metric_index,
+      kMaximumHistogramIndex, Add(percent),
+      base::LinearHistogram::FactoryGet(
+          GetMissedDeadlineHistogramName(sequence_type, thread_name), 1, 100,
+          101, base::HistogramBase::kUmaTargetedHistogramFlag));
+  return percent;
+}
+
 std::unique_ptr<base::trace_event::TracedValue>
 FrameSequenceMetrics::ThroughputData::ToTracedValue(
     const ThroughputData& impl,
@@ -420,9 +544,11 @@
   if (effective_thread == ThreadType::kMain) {
     dict->SetInteger("main-frames-produced", main.frames_produced);
     dict->SetInteger("main-frames-expected", main.frames_expected);
+    dict->SetInteger("main-frames-ontime", main.frames_ontime);
   } else {
     dict->SetInteger("impl-frames-produced", impl.frames_produced);
     dict->SetInteger("impl-frames-expected", impl.frames_expected);
+    dict->SetInteger("impl-frames-ontime", impl.frames_ontime);
   }
   return dict;
 }
diff --git a/cc/metrics/frame_sequence_metrics.h b/cc/metrics/frame_sequence_metrics.h
index 17a8285..3725d1d 100644
--- a/cc/metrics/frame_sequence_metrics.h
+++ b/cc/metrics/frame_sequence_metrics.h
@@ -49,16 +49,27 @@
         const ThroughputData& main,
         ThreadType effective_thred);
 
-    // Returns the throughput in percent, a return value of base::nullopt
-    // indicates that no throughput metric is reported.
-    static base::Optional<int> ReportHistogram(FrameSequenceMetrics* metrics,
-                                               ThreadType thread_type,
-                                               int metric_index,
-                                               const ThroughputData& data);
+    static bool CanReportHistogram(FrameSequenceMetrics* metrics,
+                                   ThreadType thread_type,
+                                   const ThroughputData& data);
+
+    // Returns the dropped throughput in percent
+    static int ReportDroppedFramePercentHistogram(FrameSequenceMetrics* metrics,
+                                                  ThreadType thread_type,
+                                                  int metric_index,
+                                                  const ThroughputData& data);
+
+    // Returns the missed deadline throughput in percent
+    static int ReportMissedDeadlineFramePercentHistogram(
+        FrameSequenceMetrics* metrics,
+        ThreadType thread_type,
+        int metric_index,
+        const ThroughputData& data);
 
     void Merge(const ThroughputData& data) {
       frames_expected += data.frames_expected;
       frames_produced += data.frames_produced;
+      frames_ontime += data.frames_ontime;
 #if DCHECK_IS_ON()
       frames_processed += data.frames_processed;
       frames_received += data.frames_received;
@@ -72,6 +83,13 @@
                        static_cast<double>(frames_expected));
     }
 
+    int MissedDeadlineFramePercent() const {
+      if (frames_produced == 0)
+        return 0;
+      return std::ceil(100 * (frames_produced - frames_ontime) /
+                       static_cast<double>(frames_produced));
+    }
+
     // Tracks the number of frames that were expected to be shown during this
     // frame-sequence.
     uint32_t frames_expected = 0;
@@ -80,6 +98,10 @@
     // during this frame-sequence.
     uint32_t frames_produced = 0;
 
+    // Tracks the number of frames that were actually presented to the user
+    // that didn't miss the vsync deadline during this frame-sequence.
+    uint32_t frames_ontime = 0;
+
 #if DCHECK_IS_ON()
     // Tracks the number of frames that is either submitted or reported as no
     // damage.
diff --git a/cc/metrics/frame_sequence_metrics_unittest.cc b/cc/metrics/frame_sequence_metrics_unittest.cc
index 6f514d1..9189f571 100644
--- a/cc/metrics/frame_sequence_metrics_unittest.cc
+++ b/cc/metrics/frame_sequence_metrics_unittest.cc
@@ -19,12 +19,15 @@
                              nullptr);
   first.impl_throughput().frames_expected = 200u;
   first.impl_throughput().frames_produced = 190u;
+  first.impl_throughput().frames_ontime = 120u;
   first.aggregated_throughput().frames_expected = 170u;
   first.aggregated_throughput().frames_produced = 150u;
+  first.aggregated_throughput().frames_ontime = 100u;
 
   first.ReportMetrics();
   EXPECT_EQ(first.aggregated_throughput().frames_expected, 0u);
   EXPECT_EQ(first.aggregated_throughput().frames_produced, 0u);
+  EXPECT_EQ(first.aggregated_throughput().frames_ontime, 0u);
 }
 
 TEST(FrameSequenceMetricsTest, MergeMetrics) {
@@ -33,6 +36,7 @@
   FrameSequenceMetrics first(FrameSequenceTrackerType::kTouchScroll, nullptr);
   first.impl_throughput().frames_expected = 20;
   first.impl_throughput().frames_produced = 10;
+  first.impl_throughput().frames_ontime = 5;
   EXPECT_FALSE(first.HasEnoughDataForReporting());
 
   // Create a second metric with too few frames to report any metrics.
@@ -40,6 +44,7 @@
       FrameSequenceTrackerType::kTouchScroll, nullptr);
   second->impl_throughput().frames_expected = 90;
   second->impl_throughput().frames_produced = 60;
+  second->impl_throughput().frames_ontime = 50;
   EXPECT_FALSE(second->HasEnoughDataForReporting());
 
   // Merge the two metrics. The result should have enough frames to report
@@ -54,12 +59,14 @@
   first.SetScrollingThread(FrameSequenceMetrics::ThreadType::kCompositor);
   first.impl_throughput().frames_expected = 20;
   first.impl_throughput().frames_produced = 10;
+  first.impl_throughput().frames_ontime = 10;
 
   auto second = std::make_unique<FrameSequenceMetrics>(
       FrameSequenceTrackerType::kTouchScroll, nullptr);
   second->SetScrollingThread(FrameSequenceMetrics::ThreadType::kMain);
   second->main_throughput().frames_expected = 50;
   second->main_throughput().frames_produced = 10;
+  second->main_throughput().frames_ontime = 10;
 
   ASSERT_DEATH(first.Merge(std::move(second)), "");
 }
@@ -71,8 +78,10 @@
   FrameSequenceMetrics first(FrameSequenceTrackerType::kVideo, nullptr);
   first.impl_throughput().frames_expected = 120;
   first.impl_throughput().frames_produced = 80;
+  first.impl_throughput().frames_ontime = 80;
   first.main_throughput().frames_expected = 0;
   first.main_throughput().frames_produced = 0;
+  first.main_throughput().frames_ontime = 0;
   first.ReportMetrics();
   histograms.ExpectTotalCount("Graphics.Smoothness.FrameSequenceLength.Video",
                               1u);
@@ -86,8 +95,10 @@
   FrameSequenceMetrics first(FrameSequenceTrackerType::kTouchScroll, nullptr);
   first.impl_throughput().frames_expected = 120;
   first.impl_throughput().frames_produced = 80;
+  first.impl_throughput().frames_ontime = 60;
   first.main_throughput().frames_expected = 20;
   first.main_throughput().frames_produced = 10;
+  first.main_throughput().frames_ontime = 5;
   EXPECT_TRUE(first.HasEnoughDataForReporting());
   first.ReportMetrics();
 
@@ -98,6 +109,13 @@
       1u);
   histograms.ExpectTotalCount(
       "Graphics.Smoothness.PercentDroppedFrames.MainThread.TouchScroll", 0u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.CompositorThread."
+      "TouchScroll",
+      1u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.MainThread.TouchScroll",
+      0u);
 
   // There should still be data left over for the main-thread.
   EXPECT_TRUE(first.HasDataLeftForReporting());
@@ -106,7 +124,9 @@
       FrameSequenceTrackerType::kTouchScroll, nullptr);
   second->impl_throughput().frames_expected = 110;
   second->impl_throughput().frames_produced = 100;
+  second->impl_throughput().frames_ontime = 80;
   second->main_throughput().frames_expected = 90;
+  second->main_throughput().frames_ontime = 70;
   first.Merge(std::move(second));
   EXPECT_TRUE(first.HasEnoughDataForReporting());
   first.ReportMetrics();
@@ -115,6 +135,13 @@
       2u);
   histograms.ExpectTotalCount(
       "Graphics.Smoothness.PercentDroppedFrames.MainThread.TouchScroll", 1u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.CompositorThread."
+      "TouchScroll",
+      2u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.MainThread.TouchScroll",
+      1u);
   // All the metrics have now been reported. No data should be left over.
   EXPECT_FALSE(first.HasDataLeftForReporting());
 }
@@ -128,8 +155,10 @@
                              nullptr);
   first.impl_throughput().frames_expected = 120;
   first.impl_throughput().frames_produced = 80;
+  first.impl_throughput().frames_ontime = 70;
   first.main_throughput().frames_expected = 120;
   first.main_throughput().frames_produced = 80;
+  first.main_throughput().frames_ontime = 70;
   EXPECT_TRUE(first.HasEnoughDataForReporting());
   first.ReportMetrics();
 
@@ -146,18 +175,34 @@
       "Graphics.Smoothness.PercentDroppedFrames.SlowerThread."
       "CompositorAnimation",
       0u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.CompositorThread."
+      "CompositorAnimation",
+      1u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.MainThread."
+      "CompositorAnimation",
+      0u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.SlowerThread."
+      "CompositorAnimation",
+      0u);
 
   // Not reported, but the data should be reset.
   EXPECT_EQ(first.impl_throughput().frames_expected, 0u);
   EXPECT_EQ(first.impl_throughput().frames_produced, 0u);
+  EXPECT_EQ(first.impl_throughput().frames_ontime, 0u);
   EXPECT_EQ(first.main_throughput().frames_expected, 0u);
   EXPECT_EQ(first.main_throughput().frames_produced, 0u);
+  EXPECT_EQ(first.main_throughput().frames_ontime, 0u);
 
   FrameSequenceMetrics second(FrameSequenceTrackerType::kRAF, nullptr);
   second.impl_throughput().frames_expected = 120;
   second.impl_throughput().frames_produced = 80;
+  second.impl_throughput().frames_ontime = 70;
   second.main_throughput().frames_expected = 120;
   second.main_throughput().frames_produced = 80;
+  second.main_throughput().frames_ontime = 70;
   EXPECT_TRUE(second.HasEnoughDataForReporting());
   second.ReportMetrics();
 
@@ -169,6 +214,13 @@
       "Graphics.Smoothness.PercentDroppedFrames.MainThread.RAF", 1u);
   histograms.ExpectTotalCount(
       "Graphics.Smoothness.PercentDroppedFrames.SlowerThread.RAF", 0u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.CompositorThread.RAF",
+      0u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.MainThread.RAF", 1u);
+  histograms.ExpectTotalCount(
+      "Graphics.Smoothness.PercentMissedDeadlineFrames.SlowerThread.RAF", 0u);
 }
 
 TEST(FrameSequenceMetricsTest, ScrollingThreadMetricsReportedForInteractions) {
@@ -177,8 +229,10 @@
         FrameSequenceTrackerType::kTouchScroll, nullptr);
     metrics->impl_throughput().frames_expected = 100;
     metrics->impl_throughput().frames_produced = 80;
+    metrics->impl_throughput().frames_ontime = 70;
     metrics->main_throughput().frames_expected = 100;
     metrics->main_throughput().frames_produced = 60;
+    metrics->main_throughput().frames_ontime = 50;
     return metrics;
   };
 
diff --git a/cc/metrics/frame_sequence_tracker.cc b/cc/metrics/frame_sequence_tracker.cc
index 56019f9..9e851e3d 100644
--- a/cc/metrics/frame_sequence_tracker.cc
+++ b/cc/metrics/frame_sequence_tracker.cc
@@ -361,6 +361,9 @@
     DCHECK_GT(impl_throughput().frames_expected,
               impl_throughput().frames_produced)
         << TRACKER_DCHECK_MSG;
+    DCHECK_GE(impl_throughput().frames_produced,
+              impl_throughput().frames_ontime)
+        << TRACKER_DCHECK_MSG;
     --impl_throughput().frames_expected;
 #if DCHECK_IS_ON()
     ++impl_throughput().frames_processed;
@@ -437,9 +440,28 @@
 
   uint32_t impl_frames_produced = 0;
   uint32_t main_frames_produced = 0;
+  uint32_t impl_frames_ontime = 0;
+  uint32_t main_frames_ontime = 0;
+
+  const auto& vsync_interval =
+      (feedback.interval.is_zero() ? viz::BeginFrameArgs::DefaultInterval()
+                                   : feedback.interval) *
+      1.5;
+  DCHECK(!vsync_interval.is_zero()) << TRACKER_DCHECK_MSG;
+  base::TimeTicks safe_deadline_for_frame =
+      last_frame_presentation_timestamp_ + vsync_interval;
 
   const bool was_presented = !feedback.failed();
   if (was_presented && submitted_frame_since_last_presentation) {
+    if (!last_frame_presentation_timestamp_.is_null() &&
+        (safe_deadline_for_frame < feedback.timestamp)) {
+      DCHECK_LE(impl_throughput().frames_ontime,
+                impl_throughput().frames_produced)
+          << TRACKER_DCHECK_MSG;
+      ++impl_throughput().frames_ontime;
+      ++impl_frames_ontime;
+    }
+
     DCHECK_LT(impl_throughput().frames_produced,
               impl_throughput().frames_expected)
         << TRACKER_DCHECK_MSG;
@@ -474,6 +496,16 @@
       metrics()->ComputeJank(FrameSequenceMetrics::ThreadType::kMain,
                              feedback.timestamp, feedback.interval);
     }
+    if (main_frames_.size() < size_before_erase) {
+      if (!last_frame_presentation_timestamp_.is_null() &&
+          (safe_deadline_for_frame < feedback.timestamp)) {
+        DCHECK_LE(main_throughput().frames_ontime,
+                  main_throughput().frames_produced)
+            << TRACKER_DCHECK_MSG;
+        ++main_throughput().frames_ontime;
+        ++main_frames_ontime;
+      }
+    }
 
     if (impl_frames_produced > 0) {
       // If there is no main frame presented, then we need to see whether or not
@@ -492,6 +524,7 @@
           // frames produced so that we can apply that to aggregated throughput
           // if the main frame reports no-damage later on.
           impl_frames_produced_while_expecting_main_ += impl_frames_produced;
+          impl_frames_ontime_while_expecting_main_ += impl_frames_ontime;
         } else {
           // TODO(https://crbug.com/1066455): Determine why this DCHECK is
           // causing PageLoadMetricsBrowserTests to flake, and re-enable.
@@ -499,10 +532,14 @@
           //    << TRACKER_DCHECK_MSG;
           aggregated_throughput().frames_produced += impl_frames_produced;
           impl_frames_produced_while_expecting_main_ = 0;
+          aggregated_throughput().frames_ontime += impl_frames_ontime;
+          impl_frames_ontime_while_expecting_main_ = 0;
         }
       } else {
         aggregated_throughput().frames_produced += main_frames_produced;
         impl_frames_produced_while_expecting_main_ = 0;
+        aggregated_throughput().frames_ontime += main_frames_ontime;
+        impl_frames_ontime_while_expecting_main_ = 0;
         while (!expecting_main_when_submit_impl_.empty() &&
                !viz::FrameTokenGT(expecting_main_when_submit_impl_.front(),
                                   frame_token)) {
@@ -511,6 +548,8 @@
       }
     }
 
+    last_frame_presentation_timestamp_ = feedback.timestamp;
+
     if (checkerboarding_.last_frame_had_checkerboarding) {
       DCHECK(!checkerboarding_.last_frame_timestamp.is_null())
           << TRACKER_DCHECK_MSG;
@@ -608,6 +647,8 @@
   DCHECK_GT(main_throughput().frames_expected,
             main_throughput().frames_produced)
       << TRACKER_DCHECK_MSG;
+  DCHECK_GE(main_throughput().frames_produced, main_throughput().frames_ontime)
+      << TRACKER_DCHECK_MSG;
   last_no_main_damage_sequence_ = args.frame_id.sequence_number;
   --main_throughput().frames_expected;
   // Compute the number of actually expected compositor frames during this main
@@ -634,6 +675,9 @@
   aggregated_throughput().frames_produced +=
       impl_frames_produced_while_expecting_main_;
   impl_frames_produced_while_expecting_main_ = 0;
+  aggregated_throughput().frames_ontime +=
+      impl_frames_ontime_while_expecting_main_;
+  impl_frames_ontime_while_expecting_main_ = 0;
   expecting_main_when_submit_impl_.clear();
 }
 
diff --git a/cc/metrics/frame_sequence_tracker.h b/cc/metrics/frame_sequence_tracker.h
index 5d1d92e..3c59aea 100644
--- a/cc/metrics/frame_sequence_tracker.h
+++ b/cc/metrics/frame_sequence_tracker.h
@@ -214,6 +214,9 @@
   // scheduled to report histogram.
   base::TimeTicks first_frame_timestamp_;
 
+  // Tracks the presentation timestamp of the previous frame.
+  base::TimeTicks last_frame_presentation_timestamp_;
+
   // Keeps track of whether the impl-frame being processed did not have any
   // damage from the compositor (i.e. 'impl damage').
   bool frame_had_no_compositor_damage_ = false;
@@ -244,6 +247,7 @@
   // presented because if that main frame ends up with no-damage, then we should
   // count the impl frames that were produced in the meantime.
   uint32_t impl_frames_produced_while_expecting_main_ = 0;
+  uint32_t impl_frames_ontime_while_expecting_main_ = 0;
   // Each entry is a frame token, inserted at ReportSubmitFrame.
   base::circular_deque<uint32_t> expecting_main_when_submit_impl_;