| // Copyright 2025 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_v4_frame.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/memory/raw_ref.h" |
| #include "cc/metrics/event_metrics.h" |
| #include "cc/metrics/scroll_jank_v4_frame_stage.h" |
| #include "components/viz/common/frame_sinks/begin_frame_args.h" |
| #include "third_party/abseil-cpp/absl/container/inlined_vector.h" |
| |
| namespace cc { |
| |
| namespace { |
| |
| ScrollEventMetrics* AsScrollUpdateOrEnd(EventMetrics& event) { |
| switch (event.type()) { |
| case EventMetrics::EventType::kFirstGestureScrollUpdate: |
| case EventMetrics::EventType::kGestureScrollUpdate: |
| case EventMetrics::EventType::kInertialGestureScrollUpdate: |
| case EventMetrics::EventType::kGestureScrollEnd: |
| case EventMetrics::EventType::kInertialGestureScrollEnd: |
| return event.AsScroll(); |
| default: |
| return nullptr; |
| } |
| } |
| |
| // Information about frames associated with scroll updates and scroll ends in an |
| // `EventMetrics::List`. |
| struct FrameBounds { |
| // The minimum frame ID associated with any scroll update or scroll end in the |
| // list. |
| viz::BeginFrameId min_id; |
| |
| // The maximum frame ID associated with any scroll update or scroll end in the |
| // list. |
| viz::BeginFrameId max_id; |
| |
| // The minimum frame ID associated with a damaging scroll update or scroll end |
| // in the list. Empty if there are no damaging scroll updates/ends in the |
| // list. |
| std::optional<viz::BeginFrameId> min_damaging_id; |
| |
| // Begin frame args associated with an arbitrary scroll update or scroll end |
| // in the list. |
| base::raw_ref<const viz::BeginFrameArgs> some_args; |
| |
| bool IsDamaging(const viz::BeginFrameId& frame_id) const { |
| return min_damaging_id.has_value() && frame_id >= *min_damaging_id; |
| } |
| }; |
| |
| // Information about frames associated with scroll updates and scroll ends in |
| // `events_metrics`. The return value is empty if and only if `events_metrics` |
| // doesn't contain any scroll updates/ends. |
| std::optional<FrameBounds> GetFrameBounds( |
| const EventMetrics::List& events_metrics) { |
| std::optional<FrameBounds> result = std::nullopt; |
| for (const auto& event : events_metrics) { |
| ScrollEventMetrics* scroll_event = AsScrollUpdateOrEnd(*event); |
| if (!scroll_event) { |
| continue; |
| } |
| const viz::BeginFrameArgs& args = scroll_event->begin_frame_args(); |
| viz::BeginFrameId frame_id = args.frame_id; |
| bool is_damaging = [&] { |
| if (!scroll_event->caused_frame_update()) { |
| return false; |
| } |
| ScrollUpdateEventMetrics* scroll_update_event = |
| scroll_event->AsScrollUpdate(); |
| if (scroll_update_event == nullptr) { |
| return false; |
| } |
| return scroll_update_event->did_scroll(); |
| }(); |
| |
| if (result.has_value()) { |
| result->min_id = std::min(result->min_id, frame_id); |
| result->max_id = std::max(result->max_id, frame_id); |
| if (is_damaging) { |
| result->min_damaging_id = |
| result->min_damaging_id.has_value() |
| ? std::min(*result->min_damaging_id, frame_id) |
| : frame_id; |
| } |
| } else { |
| result = FrameBounds{ |
| .min_id = frame_id, |
| .max_id = frame_id, |
| .min_damaging_id = is_damaging |
| ? std::optional<viz::BeginFrameId>(frame_id) |
| : std::nullopt, |
| .some_args = |
| base::raw_ref<const viz::BeginFrameArgs>::from_ptr(&args), |
| }; |
| } |
| } |
| return result; |
| } |
| |
| } // namespace |
| |
| ScrollJankV4Frame::ScrollJankV4Frame( |
| base::raw_ref<const viz::BeginFrameArgs> args, |
| ScrollDamage damage, |
| ScrollJankV4FrameStage::List stages) |
| : args(args), damage(damage), stages(stages) {} |
| |
| ScrollJankV4Frame::ScrollJankV4Frame(const ScrollJankV4Frame& frame) = default; |
| |
| ScrollJankV4Frame::~ScrollJankV4Frame() = default; |
| |
| // static |
| ScrollJankV4Frame::Timeline ScrollJankV4Frame::CalculateTimeline( |
| const EventMetrics::List& events_metrics, |
| const viz::BeginFrameArgs& presented_args, |
| base::TimeTicks presentation_ts) { |
| ScrollJankV4Frame::Timeline result; |
| |
| // We start by figuring out the range of `viz::BeginFrameId`s in |
| // `events_metrics`. |
| std::optional<FrameBounds> frame_bounds = GetFrameBounds(events_metrics); |
| |
| // We special-case the handling of several common scenarios and edge cases. |
| // This allows us to avoid constructing a `std::map` of `std::vector`s on the |
| // heap unless we absolutely have to. |
| |
| // Case 1: `events_metrics` doesn't contain any scroll updates/ends, so the |
| // result is simply empty. |
| if (!frame_bounds.has_value()) { |
| return result; |
| } |
| |
| // Case 2: All scroll updates/ends are non-damaging and associated with the |
| // same frame. |
| if (!frame_bounds->min_damaging_id.has_value() && |
| frame_bounds->min_id == frame_bounds->max_id) { |
| DCHECK_EQ(frame_bounds->some_args->frame_id, frame_bounds->min_id); |
| result.emplace_back( |
| frame_bounds->some_args, NonDamagingFrame{}, |
| ScrollJankV4FrameStage::CalculateStages( |
| events_metrics, /* skip_non_damaging_events= */ false)); |
| return result; |
| } |
| |
| // Case 3: All frames are damaging, so all scroll updates/ends should be |
| // associated with the presented damaging frame. |
| if (frame_bounds->min_damaging_id.has_value() && |
| *frame_bounds->min_damaging_id == frame_bounds->min_id) { |
| result.emplace_back( |
| base::raw_ref<const viz::BeginFrameArgs>::from_ptr(&presented_args), |
| DamagingFrame{.presentation_ts = presentation_ts}, |
| ScrollJankV4FrameStage::CalculateStages( |
| events_metrics, /* skip_non_damaging_events= */ false)); |
| return result; |
| } |
| |
| // None of the special cases above apply, so we take the generic approach and |
| // re-construct the mapping from frames to scroll events. |
| struct ArgsAndEvents { |
| base::raw_ref<const viz::BeginFrameArgs> args; |
| bool is_damaging; |
| std::vector<ScrollEventMetrics*> events; |
| }; |
| std::map<viz::BeginFrameId, ArgsAndEvents> frame_id_to_args_and_events; |
| for (const auto& event : events_metrics) { |
| ScrollEventMetrics* scroll_event = AsScrollUpdateOrEnd(*event); |
| if (!scroll_event) { |
| continue; |
| } |
| const viz::BeginFrameArgs& original_args = scroll_event->begin_frame_args(); |
| bool is_damaging = frame_bounds->IsDamaging(original_args.frame_id); |
| // If the frame is damaging, the scroll event should be associated with the |
| // presented frame (because that's the first frame where the user could see |
| // the damage). If the frame is non-damaging, it should instead be |
| // associated with its original frame (because the user isn't able to tell |
| // when/whether it was presented). |
| const viz::BeginFrameArgs& effective_args = |
| is_damaging ? presented_args : original_args; |
| const viz::BeginFrameId& effective_frame_id = effective_args.frame_id; |
| // Find the position where `effective_frame_id` is or should be in the map. |
| auto it = frame_id_to_args_and_events.lower_bound(effective_frame_id); |
| if (it != frame_id_to_args_and_events.end() && |
| it->first == effective_frame_id) { |
| // If `effective_frame_id` already has an entry in the map, just append to |
| // the vector of events. |
| it->second.events.push_back(scroll_event); |
| } else { |
| // If `effective_frame_id` doesn't have an entry in the map yet, create a |
| // new entry with a reference to `effective_args` and a singleton vector. |
| frame_id_to_args_and_events.emplace_hint( |
| it, effective_frame_id, |
| ArgsAndEvents{ |
| .args = base::raw_ref<const viz::BeginFrameArgs>::from_ptr( |
| &effective_args), |
| .is_damaging = is_damaging, |
| .events = {scroll_event}, |
| }); |
| } |
| } |
| for (const auto& [frame_id, args_and_events] : frame_id_to_args_and_events) { |
| ScrollDamage damage = |
| args_and_events.is_damaging |
| ? ScrollDamage{DamagingFrame{.presentation_ts = presentation_ts}} |
| : ScrollDamage{NonDamagingFrame{}}; |
| result.emplace_back( |
| args_and_events.args, damage, |
| ScrollJankV4FrameStage::CalculateStages( |
| args_and_events.events, /* skip_non_damaging_events= */ false)); |
| } |
| return result; |
| } |
| |
| } // namespace cc |