Rework Graphics.Smoothness.Jank to use V3 signals

For PercentDroppedFrames we upgrading from V1 to V3 with more accurate
signals for the importance of the frames. V3 is now a guardrail metric.

We want to eliminate the old V1 path. However some metrics still use it.
Jank is one such metric. This change updated V3 to perform
the calculation.

Bug: 1256879

Change-Id: Idf3f9d1cd0ec677903b5642e9db5e2d5f75e3822
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4582756
Reviewed-by: Mingjing Zhang <mjzhang@chromium.org>
Reviewed-by: John Chen <johnchen@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1154887}
diff --git a/cc/metrics/compositor_frame_reporter.cc b/cc/metrics/compositor_frame_reporter.cc
index 9c0fb97..51bc237 100644
--- a/cc/metrics/compositor_frame_reporter.cc
+++ b/cc/metrics/compositor_frame_reporter.cc
@@ -1836,6 +1836,7 @@
   info.smooth_thread = smooth_thread;
   info.scroll_thread = scrolling_thread;
   info.has_missing_content = has_missing_content_;
+  info.sequence_number = args_.frame_id.sequence_number;
 
   if (frame_skip_reason_.has_value() &&
       frame_skip_reason() == FrameSkippedReason::kNoDamage) {
@@ -1858,15 +1859,7 @@
     info.main_thread_response = FrameInfo::MainThreadResponse::kIncluded;
   }
 
-  if (!stage_history_.empty()) {
-    const auto& stage = stage_history_.back();
-    if (stage.stage_type == StageType::kTotalLatency) {
-      DCHECK_EQ(frame_termination_time_ - args_.frame_time,
-                stage.end_time - stage.start_time);
-      info.total_latency = frame_termination_time_ - args_.frame_time;
-    }
-  }
-
+  info.termination_time = frame_termination_time_;
   return info;
 }
 
diff --git a/cc/metrics/frame_info.cc b/cc/metrics/frame_info.cc
index d7306e9..ce14b20 100644
--- a/cc/metrics/frame_info.cc
+++ b/cc/metrics/frame_info.cc
@@ -40,6 +40,10 @@
 
 }  // namespace
 
+FrameInfo::FrameInfo() = default;
+FrameInfo::FrameInfo(const FrameInfo& other) = default;
+FrameInfo::~FrameInfo() = default;
+
 bool FrameInfo::IsDroppedAffectingSmoothness() const {
   // If neither of the threads are expected to be smooth, then this frame cannot
   // affect smoothness.
@@ -84,6 +88,11 @@
 
     compositor_update_was_dropped =
         other.final_state == FrameFinalState::kDropped;
+
+    compositor_final_state = other.final_state;
+    compositor_termination_time = other.termination_time;
+    main_final_state = final_state;
+    main_termination_time = termination_time;
   } else {
     // |this| does not include main-thread updates. Therefore:
     //   - |other| must include main-thread updates.
@@ -93,6 +102,11 @@
 
     main_update_was_dropped = other.final_state == FrameFinalState::kDropped;
     compositor_update_was_dropped = final_state == FrameFinalState::kDropped;
+
+    compositor_final_state = final_state;
+    compositor_termination_time = termination_time;
+    main_final_state = other.final_state;
+    main_termination_time = other.termination_time;
   }
 
   was_merged = true;
@@ -131,7 +145,7 @@
     smooth_thread = SmoothThread::kSmoothNone;
   }
 
-  total_latency = std::max(total_latency, other.total_latency);
+  termination_time = std::max(termination_time, other.termination_time);
 
   // Validate the state after the merge.
   DCHECK(Validate());
@@ -202,4 +216,34 @@
   }
 }
 
+FrameInfo::FrameFinalState FrameInfo::GetFinalStateForThread(
+    SmoothEffectDrivingThread thread) const {
+  if (!was_merged) {
+    return final_state;
+  }
+  switch (thread) {
+    case SmoothEffectDrivingThread::kCompositor:
+      return compositor_final_state;
+    case SmoothEffectDrivingThread::kMain:
+      return main_final_state;
+    case SmoothEffectDrivingThread::kUnknown:
+      return final_state;
+  }
+}
+
+base::TimeTicks FrameInfo::GetTerminationTimeForThread(
+    SmoothEffectDrivingThread thread) const {
+  if (!was_merged) {
+    return termination_time;
+  }
+  switch (thread) {
+    case SmoothEffectDrivingThread::kCompositor:
+      return compositor_termination_time;
+    case SmoothEffectDrivingThread::kMain:
+      return main_termination_time;
+    case SmoothEffectDrivingThread::kUnknown:
+      return termination_time;
+  }
+}
+
 }  // namespace cc
diff --git a/cc/metrics/frame_info.h b/cc/metrics/frame_info.h
index 9ff45c882..486ad3e2 100644
--- a/cc/metrics/frame_info.h
+++ b/cc/metrics/frame_info.h
@@ -11,6 +11,10 @@
 namespace cc {
 
 struct CC_EXPORT FrameInfo {
+  FrameInfo();
+  FrameInfo(const FrameInfo& other);
+  ~FrameInfo();
+
   enum class FrameFinalState {
     kNoUpdateDesired,
     kDropped,
@@ -56,10 +60,15 @@
 
   bool has_missing_content = false;
 
-  // The total latency for the frame. If the frame had to be 'split' (i.e.
-  // compositor-thread update and main-thread updates were presented in separate
-  // frames), then this contains the maximum latency of the two updates.
-  base::TimeDelta total_latency;
+  // The time when the frame was terminated. If the frame had to be 'split'
+  // (i.e. compositor-thread update and main-thread updates were presented in
+  // separate frames,) then this contains the maximum time when the updates were
+  // terminated. See GetTerminationTimeForThread to get the value for each.
+  base::TimeTicks termination_time;
+
+  // The frame number associated to the viz::BeginFrameArgs that started this
+  // frame's production.
+  uint64_t sequence_number = 0u;
 
   bool IsDroppedAffectingSmoothness() const;
   void MergeWith(const FrameInfo& info);
@@ -74,10 +83,26 @@
 
   bool IsScrollPrioritizeFrameDropped() const;
 
+  // If this `was_merged` these return the value for `thread`, otherwise returns
+  // the default non-merged values.
+  FrameFinalState GetFinalStateForThread(
+      SmoothEffectDrivingThread thread) const;
+  base::TimeTicks GetTerminationTimeForThread(
+      SmoothEffectDrivingThread thread) const;
+
  private:
   bool was_merged = false;
   bool compositor_update_was_dropped = false;
   bool main_update_was_dropped = false;
+
+  // A frame that `was_merged` could have differing final states, and differing
+  // termination times. We track both so that each thread's jank can be
+  // calculated.
+  FrameFinalState compositor_final_state = FrameFinalState::kNoUpdateDesired;
+  FrameFinalState main_final_state = FrameFinalState::kNoUpdateDesired;
+
+  base::TimeTicks compositor_termination_time;
+  base::TimeTicks main_termination_time;
 };
 
 }  // namespace cc
diff --git a/cc/metrics/frame_sequence_metrics.cc b/cc/metrics/frame_sequence_metrics.cc
index e1a66a25..309dde8f 100644
--- a/cc/metrics/frame_sequence_metrics.cc
+++ b/cc/metrics/frame_sequence_metrics.cc
@@ -17,6 +17,7 @@
 #include "cc/metrics/frame_sequence_tracker.h"
 #include "cc/metrics/jank_metrics.h"
 #include "components/viz/common/frame_sinks/begin_frame_args.h"
+#include "third_party/abseil-cpp/absl/base/attributes.h"
 
 namespace cc {
 
@@ -57,6 +58,20 @@
 
 namespace {
 
+constexpr uint32_t kMaxNoUpdateFrameCount = 100;
+
+const char* GetJankThreadTypeName(FrameInfo::SmoothEffectDrivingThread type) {
+  switch (type) {
+    case FrameInfo::SmoothEffectDrivingThread::kCompositor:
+      return "Compositor";
+    case FrameInfo::SmoothEffectDrivingThread::kMain:
+      return "Main";
+    default:
+      NOTREACHED();
+      return "";
+  }
+}
+
 // Avoid reporting any throughput metric for sequences that do not have a
 // sufficient number of frames.
 constexpr int kMinFramesForThroughputMetric = 100;
@@ -86,6 +101,13 @@
        FrameSequenceTracker::GetFrameSequenceTrackerTypeName(type)});
 }
 
+std::string GetJankV3HistogramName(FrameSequenceTrackerType type,
+                                   const char* thread_name) {
+  return base::StrCat(
+      {"Graphics.Smoothness.Jank3.", thread_name, ".",
+       FrameSequenceTracker::GetFrameSequenceTrackerTypeName(type)});
+}
+
 std::string GetThroughputV3HistogramName(FrameSequenceTrackerType type,
                                          const char* thread_name) {
   return base::StrCat(
@@ -95,6 +117,9 @@
 
 }  // namespace
 
+FrameSequenceMetrics::V3::V3() = default;
+FrameSequenceMetrics::V3::~V3() = default;
+
 FrameSequenceMetrics::FrameSequenceMetrics(FrameSequenceTrackerType type)
     : type_(type) {
   SmoothEffectDrivingThread thread_type = GetEffectiveThread();
@@ -175,10 +200,15 @@
   v3_.frames_expected += metrics->v3_.frames_expected;
   v3_.frames_dropped += metrics->v3_.frames_dropped;
   v3_.frames_missing_content += metrics->v3_.frames_missing_content;
+  v3_.jank_count += metrics->v3_.jank_count;
+  v3_.no_update_count += metrics->v3_.no_update_count;
   if (v3_.last_begin_frame_args.frame_time <
       metrics->v3_.last_begin_frame_args.frame_time) {
     v3_.last_begin_frame_args = metrics->v3_.last_begin_frame_args;
+    v3_.last_frame = metrics->v3_.last_frame;
     v3_.last_presented_frame = metrics->v3_.last_presented_frame;
+    v3_.last_frame_delta = metrics->v3_.last_frame_delta;
+    v3_.no_update_duration = metrics->v3_.no_update_duration;
   }
 
   if (jank_reporter_)
@@ -205,10 +235,13 @@
 void FrameSequenceMetrics::AdoptTrace(FrameSequenceMetrics* adopt_from) {
   DCHECK(!trace_data_.trace_id);
   trace_data_.trace_id = adopt_from->trace_data_.trace_id;
+  trace_data_v3_.trace_id = adopt_from->trace_data_v3_.trace_id;
   adopt_from->trace_data_.trace_id = nullptr;
+  adopt_from->trace_data_v3_.trace_id = nullptr;
 }
 
-void FrameSequenceMetrics::AdvanceTrace(base::TimeTicks timestamp) {
+void FrameSequenceMetrics::AdvanceTrace(base::TimeTicks timestamp,
+                                        uint64_t sequence_number) {
   uint32_t expected = 0, dropped = 0;
   switch (GetEffectiveThread()) {
     case SmoothEffectDrivingThread::kCompositor:
@@ -226,7 +259,8 @@
     case SmoothEffectDrivingThread::kUnknown:
       NOTREACHED();
   }
-  trace_data_.Advance(timestamp, expected, dropped);
+  trace_data_.Advance(trace_data_.last_timestamp, timestamp, expected, dropped,
+                      sequence_number, "FrameSequenceTracker");
 }
 
 void FrameSequenceMetrics::ReportMetrics() {
@@ -235,6 +269,7 @@
 
   // Terminates |trace_data_| for all types of FrameSequenceTracker.
   trace_data_.Terminate();
+  trace_data_v3_.TerminateV3(v3_);
 
   if (type_ == FrameSequenceTrackerType::kCustom) {
     DCHECK(!custom_reporter_.is_null());
@@ -258,17 +293,23 @@
       ShouldReportForInteraction(type(), thread_type, thread_type);
 
   if (v3_.frames_expected >= kMinFramesForThroughputMetric) {
-    const int percent_missing_content = static_cast<int>(
-        100 * v3_.frames_missing_content / v3_.frames_expected);
-    int percent = v3_.frames_expected == 0
-                      ? 0
-                      : std::ceil(100. * v3_.frames_dropped /
-                                  static_cast<double>(v3_.frames_expected));
+    const int percent_missing_content =
+        std::ceil(100. * v3_.frames_missing_content /
+                  static_cast<double>(v3_.frames_expected));
+    const int percent =
+        v3_.frames_expected == 0
+            ? 0
+            : std::ceil(100. * v3_.frames_dropped /
+                        static_cast<double>(v3_.frames_expected));
+    const int percent_jank = std::ceil(
+        100. * v3_.jank_count / static_cast<double>(v3_.frames_expected));
 
     if (is_animation) {
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.Checkerboarding3.AllAnimations",
           percent_missing_content);
+      UMA_HISTOGRAM_PERCENTAGE("Graphics.Smoothness.Jank3.AllAnimations",
+                               percent_jank);
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.PercentDroppedFrames3.AllAnimations", percent);
     }
@@ -276,6 +317,8 @@
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.Checkerboarding3.AllInteractions",
           percent_missing_content);
+      UMA_HISTOGRAM_PERCENTAGE("Graphics.Smoothness.Jank3.AllInteractions",
+                               percent_jank);
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.PercentDroppedFrames3.AllInteractions", percent);
     }
@@ -283,6 +326,8 @@
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.Checkerboarding3.AllSequences",
           percent_missing_content);
+      UMA_HISTOGRAM_PERCENTAGE("Graphics.Smoothness.Jank3.AllSequences",
+                               percent_jank);
       UMA_HISTOGRAM_PERCENTAGE(
           "Graphics.Smoothness.PercentDroppedFrames3.AllSequences", percent);
     }
@@ -307,9 +352,20 @@
         base::LinearHistogram::FactoryGet(
             GetCheckerboardingV3HistogramName(type_), 1, 100, 101,
             base::HistogramBase::kUmaTargetedHistogramFlag));
+
+    const char* jank_thread_name = GetJankThreadTypeName(GetEffectiveThread());
+    STATIC_HISTOGRAM_POINTER_GROUP(
+        GetJankV3HistogramName(type_, jank_thread_name),
+        static_cast<int>(type_),
+        static_cast<int>(FrameSequenceTrackerType::kMaxType), Add(percent_jank),
+        base::LinearHistogram::FactoryGet(
+            GetJankV3HistogramName(type_, jank_thread_name), 1, 100, 101,
+            base::HistogramBase::kUmaTargetedHistogramFlag));
     v3_.frames_expected = 0u;
     v3_.frames_dropped = 0u;
     v3_.frames_missing_content = 0u;
+    v3_.no_update_count = 0u;
+    v3_.jank_count = 0u;
   }
 
   // Report the checkerboarding metrics.
@@ -448,49 +504,90 @@
   trace_id = nullptr;
 }
 
-void FrameSequenceMetrics::TraceData::Advance(base::TimeTicks new_timestamp,
+void FrameSequenceMetrics::TraceData::TerminateV3(const V3& v3) {
+  if (!enabled || !trace_id) {
+    return;
+  }
+  auto dict = std::make_unique<base::trace_event::TracedValue>();
+  dict->BeginDictionary("data");
+  dict->SetInteger("expected", v3.frames_expected);
+  dict->SetInteger("dropped", v3.frames_dropped);
+  dict->SetInteger("missing_content", v3.frames_missing_content);
+  dict->EndDictionary();
+  TRACE_EVENT_NESTABLE_ASYNC_END1("cc,benchmark", "FrameSequenceTrackerV3",
+                                  TRACE_ID_LOCAL(trace_id), "args",
+                                  std::move(dict));
+  trace_id = nullptr;
+}
+
+void FrameSequenceMetrics::TraceData::Advance(base::TimeTicks start_timestamp,
+                                              base::TimeTicks new_timestamp,
                                               uint32_t expected,
-                                              uint32_t dropped) {
+                                              uint32_t dropped,
+                                              uint64_t sequence_number,
+                                              const char* histogram_name) {
   if (!enabled)
     return;
   if (!trace_id) {
     trace_id = this;
     TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1(
-        "cc,benchmark", "FrameSequenceTracker", TRACE_ID_LOCAL(trace_id),
-        this->last_timestamp, "name",
+        "cc,benchmark", histogram_name, TRACE_ID_LOCAL(trace_id),
+        start_timestamp, "name",
         FrameSequenceTracker::GetFrameSequenceTrackerTypeName(metrics->type()));
   }
+
+  auto dict = std::make_unique<base::trace_event::TracedValue>();
+  dict->BeginDictionary("values");
+  dict->SetInteger("sequence_number", sequence_number);
+  dict->SetInteger("last_sequence", last_presented_sequence_number);
+  dict->SetInteger("expected", expected);
+  dict->SetInteger("dopped", dropped);
+  dict->EndDictionary();
+
   // Use different names, because otherwise the trace-viewer shows the slices in
   // the same color, and that makes it difficult to tell the traces apart from
   // each other.
   const char* trace_names[] = {"Frame", "Frame ", "Frame   "};
   TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP0(
       "cc,benchmark", trace_names[++this->frame_count % 3],
-      TRACE_ID_LOCAL(trace_id), this->last_timestamp);
-  TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP2(
+      TRACE_ID_LOCAL(trace_id), start_timestamp);
+  TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP1(
       "cc,benchmark", trace_names[this->frame_count % 3],
-      TRACE_ID_LOCAL(trace_id), new_timestamp, "expected", expected, "dropped",
-      dropped);
+      TRACE_ID_LOCAL(trace_id), new_timestamp, "data", std::move(dict));
+  this->last_presented_sequence_number = sequence_number;
   this->last_timestamp = new_timestamp;
 }
 
 void FrameSequenceMetrics::AddSortedFrame(const viz::BeginFrameArgs& args,
                                           const FrameInfo& frame_info) {
-  switch (GetEffectiveThread()) {
+  const auto effective_thread = GetEffectiveThread();
+  const auto last_presented_termination_time =
+      v3_.last_presented_frame.GetTerminationTimeForThread(effective_thread);
+  const auto termination_time =
+      frame_info.GetTerminationTimeForThread(effective_thread);
+  switch (effective_thread) {
     case SmoothEffectDrivingThread::kCompositor:
       if (frame_info.WasSmoothCompositorUpdateDropped()) {
         ++v3_.frames_dropped;
       }
-      CalculateCheckerboardingV3(frame_info);
       ++v3_.frames_expected;
+      CalculateCheckerboardingAndJankV3(
+          args, frame_info, frame_info.GetFinalStateForThread(effective_thread),
+          last_presented_termination_time, termination_time);
       break;
     case SmoothEffectDrivingThread::kMain:
       if (frame_info.WasSmoothMainUpdateExpected()) {
         if (frame_info.WasSmoothMainUpdateDropped()) {
           ++v3_.frames_dropped;
         }
-        CalculateCheckerboardingV3(frame_info);
         ++v3_.frames_expected;
+        CalculateCheckerboardingAndJankV3(
+            args, frame_info,
+            frame_info.GetFinalStateForThread(effective_thread),
+            last_presented_termination_time, termination_time);
+      } else {
+        IncrementJankIdleTimeV3(last_presented_termination_time,
+                                termination_time);
       }
       break;
     case SmoothEffectDrivingThread::kUnknown:
@@ -498,12 +595,20 @@
       break;
   }
   v3_.last_begin_frame_args = args;
+  v3_.last_frame = frame_info;
 }
 
-void FrameSequenceMetrics::CalculateCheckerboardingV3(
-    const FrameInfo& frame_info) {
-  switch (frame_info.final_state) {
+void FrameSequenceMetrics::CalculateCheckerboardingAndJankV3(
+    const viz::BeginFrameArgs& args,
+    const FrameInfo& frame_info,
+    FrameInfo::FrameFinalState final_state,
+    base::TimeTicks last_presented_termination_time,
+    base::TimeTicks termination_time) {
+  switch (final_state) {
     case FrameInfo::FrameFinalState::kNoUpdateDesired:
+      IncrementJankIdleTimeV3(last_presented_termination_time,
+                              termination_time);
+      ABSL_FALLTHROUGH_INTENDED;
     case FrameInfo::FrameFinalState::kDropped:
       if (v3_.last_presented_frame.has_missing_content) {
         ++v3_.frames_missing_content;
@@ -515,9 +620,105 @@
       if (frame_info.has_missing_content) {
         ++v3_.frames_missing_content;
       }
+
+      // The first frame of a sequence will have no previous timestamp. We don't
+      // calculate it for jank. However we start the tracing from when the
+      // sequence was started.
+      bool will_ignore_current_frame =
+          v3_.no_update_count >= kMaxNoUpdateFrameCount;
+      if (last_presented_termination_time.is_null()) {
+        last_presented_termination_time = trace_data_v3_.last_timestamp;
+        will_ignore_current_frame = true;
+      }
+
+      // TODO(crbug.com/1450940): A new FrameSequenceTracker, that has yet to
+      // process its first frame uses its creation time as starting point of
+      // nested traces. FrameSorter processes a FrameInfo when both threads are
+      // complete. It's possible for the smoothness thread component to have
+      // completed before this tracker started. We do not include them in the
+      // traces.
+      if (!last_presented_termination_time.is_null() &&
+          termination_time > last_presented_termination_time) {
+        trace_data_v3_.Advance(last_presented_termination_time,
+                               termination_time, v3_.frames_expected,
+                               v3_.frames_dropped, frame_info.sequence_number,
+                               "FrameSequenceTrackerV3");
+      }
+
+      const base::TimeDelta zero_delta = base::Milliseconds(0);
+      base::TimeDelta current_frame_delta =
+          will_ignore_current_frame
+              ? zero_delta
+              : termination_time - last_presented_termination_time -
+                    v3_.no_update_duration;
+      // Guard against the situation when the physical presentation interval is
+      // shorter than |no_update_duration|. For example, consider two
+      // BeginFrames A and B separated by 5 vsync cycles of no-updates (i.e.
+      // |no_update_duration| = 5 vsync cycles); the Presentation of A occurs 2
+      // vsync cycles after BeginFrame A, whereas Presentation B occurs in the
+      // same vsync cycle as BeginFrame B. In this situation, the physical
+      // presentation interval is shorter than 5 vsync cycles and will result
+      // in a negative |current_frame_delta|.
+      if (current_frame_delta < zero_delta) {
+        current_frame_delta = zero_delta;
+      }
+
+      // The presentation interval is typically a multiple of VSync intervals
+      // (i.e. 16.67ms, 33.33ms, 50ms ... on a 60Hz display) with small
+      // fluctuations. The 0.5 * |frame_interval| criterion is chosen so that
+      // the jank detection is robust to those fluctuations.
+      if (!v3_.last_frame_delta.is_zero() &&
+          current_frame_delta > v3_.last_frame_delta + 0.5 * args.interval) {
+        ++v3_.jank_count;
+        TraceJankV3(frame_info.sequence_number, last_presented_termination_time,
+                    termination_time);
+      }
+
+      v3_.last_frame_delta = current_frame_delta;
+      v3_.no_update_duration = base::TimeDelta();
+      v3_.no_update_count = 0;
       v3_.last_presented_frame = frame_info;
       break;
   }
 }
 
+void FrameSequenceMetrics::IncrementJankIdleTimeV3(
+    base::TimeTicks last_presented_termination_time,
+    base::TimeTicks termination_time) {
+  // If `frame_info.sequence_number` of N takes a long time to present, it can
+  // present after N-1 was either Dropped or NoUpdateDesired. We don't offset
+  // jank calculation for these frames.
+  if (last_presented_termination_time.is_null() ||
+      termination_time < last_presented_termination_time) {
+    return;
+  }
+
+  v3_.no_update_duration +=
+      termination_time -
+      v3_.last_frame.GetTerminationTimeForThread(GetEffectiveThread());
+  ++v3_.no_update_count;
+}
+
+void FrameSequenceMetrics::TraceJankV3(uint64_t sequence_number,
+                                       base::TimeTicks last_termination_time,
+                                       base::TimeTicks termination_time) {
+  if (!trace_data_v3_.enabled) {
+    return;
+  }
+  auto dict = std::make_unique<base::trace_event::TracedValue>();
+  dict->BeginDictionary("data");
+  dict->SetInteger("frame_sequence_number", sequence_number);
+  dict->SetInteger("last_presented_frame_sequence_number",
+                   v3_.last_presented_frame.sequence_number);
+  dict->SetString("thread-type", GetJankThreadTypeName(GetEffectiveThread()));
+  dict->SetString("tracker-type",
+                  FrameSequenceTracker::GetFrameSequenceTrackerTypeName(type_));
+  dict->EndDictionary();
+  TRACE_EVENT_NESTABLE_ASYNC_BEGIN_WITH_TIMESTAMP1(
+      "cc,benchmark", "JankV3", TRACE_ID_LOCAL(this), last_termination_time,
+      "data", std::move(dict));
+  TRACE_EVENT_NESTABLE_ASYNC_END_WITH_TIMESTAMP0(
+      "cc,benchmark", "JankV3", TRACE_ID_LOCAL(this), termination_time);
+}
+
 }  // namespace cc
diff --git a/cc/metrics/frame_sequence_metrics.h b/cc/metrics/frame_sequence_metrics.h
index 8613d1d7..118b515 100644
--- a/cc/metrics/frame_sequence_metrics.h
+++ b/cc/metrics/frame_sequence_metrics.h
@@ -142,7 +142,7 @@
   void ReportLeftoverData();
 
   void AdoptTrace(FrameSequenceMetrics* adopt_from);
-  void AdvanceTrace(base::TimeTicks timestamp);
+  void AdvanceTrace(base::TimeTicks timestamp, uint64_t sequence_number);
 
   void ComputeJank(FrameInfo::SmoothEffectDrivingThread thread_type,
                    uint32_t frame_token,
@@ -160,34 +160,61 @@
       base::TimeDelta frame_interval);
 
  private:
-  void CalculateCheckerboardingV3(const FrameInfo& frame_info);
+  // FrameInfo is a merger of two threads' frame production. We should only look
+  // at the `final_state`, `last_presented_termination_time` and
+  // `termination_time` for the GetEffectiveThread.
+  void CalculateCheckerboardingAndJankV3(
+      const viz::BeginFrameArgs& args,
+      const FrameInfo& frame_info,
+      FrameInfo::FrameFinalState final_state,
+      base::TimeTicks last_presented_termination_time,
+      base::TimeTicks termination_time);
+  void IncrementJankIdleTimeV3(base::TimeTicks last_presented_termination_time,
+                               base::TimeTicks termination_time);
+  void TraceJankV3(uint64_t sequence_number,
+                   base::TimeTicks last_termination_time,
+                   base::TimeTicks termination_time);
 
   const FrameSequenceTrackerType type_;
 
+  // Track state for measuring the various Graphics.Smoothness V3 metrics.
+  struct V3 {
+    V3();
+    ~V3();
+    uint32_t frames_expected = 0;
+    uint32_t frames_dropped = 0;
+    uint32_t frames_missing_content = 0;
+    uint32_t no_update_count = 0;
+    uint32_t jank_count = 0;
+    viz::BeginFrameArgs last_begin_frame_args;
+    FrameInfo last_frame;
+    FrameInfo last_presented_frame;
+    base::TimeDelta last_frame_delta;
+    base::TimeDelta no_update_duration;
+  } v3_;
+
   // Tracks some data to generate useful trace events.
   struct TraceData {
     explicit TraceData(FrameSequenceMetrics* metrics);
     ~TraceData();
     raw_ptr<FrameSequenceMetrics> metrics;
+    uint64_t last_presented_sequence_number = 0u;
     base::TimeTicks last_timestamp = base::TimeTicks::Now();
     int frame_count = 0;
     bool enabled = false;
     raw_ptr<void> trace_id = nullptr;
 
-    void Advance(base::TimeTicks new_timestamp,
+    void Advance(base::TimeTicks start_timestamp,
+                 base::TimeTicks new_timestamp,
                  uint32_t expected,
-                 uint32_t dropped);
+                 uint32_t dropped,
+                 uint64_t sequence_number,
+                 const char* histogram_name);
     void Terminate();
+    void TerminateV3(const V3& v3);
   } trace_data_{this};
 
-  // Track state for measuring the PercentDroppedFrames v3 metrics.
-  struct {
-    uint32_t frames_expected = 0;
-    uint32_t frames_dropped = 0;
-    uint32_t frames_missing_content = 0;
-    viz::BeginFrameArgs last_begin_frame_args;
-    FrameInfo last_presented_frame;
-  } v3_;
+  TraceData trace_data_v3_{this};
 
   ThroughputData impl_throughput_;
   ThroughputData main_throughput_;
diff --git a/cc/metrics/frame_sequence_tracker.cc b/cc/metrics/frame_sequence_tracker.cc
index 648d1d9d..d1ff6ce 100644
--- a/cc/metrics/frame_sequence_tracker.cc
+++ b/cc/metrics/frame_sequence_tracker.cc
@@ -447,7 +447,7 @@
         << TRACKER_DCHECK_MSG;
     ++impl_throughput().frames_produced;
     if (metrics()->GetEffectiveThread() == ThreadType::kCompositor) {
-      metrics()->AdvanceTrace(feedback.timestamp);
+      metrics()->AdvanceTrace(feedback.timestamp, frame_token);
     }
 
     metrics()->ComputeJank(FrameInfo::SmoothEffectDrivingThread::kCompositor,
@@ -468,7 +468,7 @@
           << TRACKER_DCHECK_MSG;
       ++main_throughput().frames_produced;
       if (metrics()->GetEffectiveThread() == ThreadType::kMain) {
-        metrics()->AdvanceTrace(feedback.timestamp);
+        metrics()->AdvanceTrace(feedback.timestamp, frame_token);
       }
 
       metrics()->ComputeJank(FrameInfo::SmoothEffectDrivingThread::kMain,
diff --git a/tools/metrics/histograms/metadata/compositing/histograms.xml b/tools/metrics/histograms/metadata/compositing/histograms.xml
index 941704a3..e20dd9b 100644
--- a/tools/metrics/histograms/metadata/compositing/histograms.xml
+++ b/tools/metrics/histograms/metadata/compositing/histograms.xml
@@ -1229,6 +1229,55 @@
   </token>
 </histogram>
 
+<histogram name="Graphics.Smoothness.Jank3.AllSequences" units="%"
+    expires_after="2024-06-05">
+  <owner>jonross@chromium.org</owner>
+  <owner>mjzhang@chromium.org</owner>
+  <owner>graphics-dev@chromium.org</owner>
+  <summary>
+    Tracks the percent of janks for a sequence by aggregating all its animations
+    and interactions, where a sequence is a series of frames produced which
+    contain at least one of the animations/interactions. Such animations and
+    interactions encompass: Compositor-thread animations; Main-thread
+    animations; JS animations; scrolling; and pinch-gestures.
+
+    Jank is measured by tracking the number of abrupt increases in frame
+    presentation interval, divided by the total number of frames expected to be
+    produced and displayed. The lower this number is, the less the smoothness
+    varies over time.
+
+    Note that this metric is reported only when there are sufficient number of
+    frames (&gt;= 100). If there are sequences with fewer frames, then these are
+    aggregated until there are enough frames to produce the metric.
+  </summary>
+</histogram>
+
+<histogram name="Graphics.Smoothness.Jank3.{Thread}{Sequence}" units="%"
+    expires_after="2024-06-05">
+  <owner>jonross@chromium.org</owner>
+  <owner>mjzhang@chromium.org</owner>
+  <owner>graphics-dev@chromium.org</owner>
+  <summary>
+    Tracks the percent of janks for a sequence by aggregating all {Sequence},
+    where a sequence is a series of frames produced which contain at least one
+    of the aforementioned effects.
+
+    Jank is measured by tracking the number of abrupt increases in frame
+    presentation interval, divided by the total number of frames expected to be
+    produced and displayed. The lower this number is, the less the smoothness
+    varies over time.
+
+    Note that this metric is reported only when there are sufficient number of
+    frames (&gt;= 100). If there are sequences with fewer frames, then these are
+    aggregated until there are enough frames to produce the metric.
+  </summary>
+  <token key="Thread">
+    <variant name="CompositorThread."/>
+    <variant name="MainThread."/>
+  </token>
+  <token key="Sequence" variants="SmoothnessSequence"/>
+</histogram>
+
 <histogram name="Graphics.Smoothness.MaxPercentDroppedFrames_1sWindow"
     units="%" expires_after="2023-11-12">
   <owner>jonross@chromium.org</owner>
diff --git a/tools/perf/benchmarks/rendering.py b/tools/perf/benchmarks/rendering.py
index 2e0b4ff..782f3db 100644
--- a/tools/perf/benchmarks/rendering.py
+++ b/tools/perf/benchmarks/rendering.py
@@ -29,6 +29,9 @@
     'Graphics.Smoothness.Jank.AllAnimations',
     'Graphics.Smoothness.Jank.AllInteractions',
     'Graphics.Smoothness.Jank.AllSequences',
+    'Graphics.Smoothness.Jank3.AllAnimations',
+    'Graphics.Smoothness.Jank3.AllInteractions',
+    'Graphics.Smoothness.Jank3.AllSequences',
     'Graphics.Smoothness.PercentDroppedFrames3.AllAnimations',
     'Graphics.Smoothness.PercentDroppedFrames3.AllInteractions',
     'Graphics.Smoothness.PercentDroppedFrames3.AllSequences',
diff --git a/tools/perf/contrib/system_health_scroll_jank/system_health_scroll_jank.py b/tools/perf/contrib/system_health_scroll_jank/system_health_scroll_jank.py
index c14719d..fe809dc 100644
--- a/tools/perf/contrib/system_health_scroll_jank/system_health_scroll_jank.py
+++ b/tools/perf/contrib/system_health_scroll_jank/system_health_scroll_jank.py
@@ -26,6 +26,8 @@
     'Graphics.Smoothness.Checkerboarding3.WheelScroll',
     'Graphics.Smoothness.Jank.Compositor.TouchScroll',
     'Graphics.Smoothness.Jank.Main.TouchScroll',
+    'Graphics.Smoothness.Jank3.Compositor.TouchScroll',
+    'Graphics.Smoothness.Jank3.Main.TouchScroll',
     'Graphics.Smoothness.PercentDroppedFrames3.AllAnimations',
     'Graphics.Smoothness.PercentDroppedFrames3.AllInteractions',
     'Graphics.Smoothness.PercentDroppedFrames3.AllSequences',