| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cc/trees/frame_sequence_tracker.h" |
| |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/strcat.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/trace_event/traced_value.h" |
| #include "components/viz/common/frame_sinks/begin_frame_args.h" |
| #include "components/viz/common/quads/compositor_frame_metadata.h" |
| #include "ui/gfx/presentation_feedback.h" |
| |
| namespace cc { |
| |
| namespace { |
| |
| enum class ThreadType { |
| kMain, |
| kCompositor, |
| }; |
| |
| constexpr const char* const kBuiltinSequences[] = { |
| [FrameSequenceTrackerType::kCompositorAnimation] = "CompositorAnimation", |
| [FrameSequenceTrackerType::kMainThreadAnimation] = "MainThreadAnimation", |
| [FrameSequenceTrackerType::kPinchZoom] = "PinchZoom", |
| [FrameSequenceTrackerType::kRAF] = "RAF", |
| [FrameSequenceTrackerType::kTouchScroll] = "TouchScroll", |
| [FrameSequenceTrackerType::kWheelScroll] = "WheelScroll", |
| }; |
| |
| constexpr int kBuiltinSequenceNum = base::size(kBuiltinSequences); |
| constexpr int kMaximumHistogramIndex = 2 * kBuiltinSequenceNum; |
| |
| int GetIndexForMetric(ThreadType thread_type, FrameSequenceTrackerType type) { |
| return thread_type == ThreadType::kMain |
| ? static_cast<int>(type) |
| : static_cast<int>(type + kBuiltinSequenceNum); |
| } |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // FrameSequenceTrackerCollection |
| |
| FrameSequenceTrackerCollection::FrameSequenceTrackerCollection() {} |
| FrameSequenceTrackerCollection::~FrameSequenceTrackerCollection() { |
| removal_trackers_.clear(); |
| DCHECK(frame_trackers_.empty()); |
| } |
| |
| std::unique_ptr<FrameSequenceTracker> |
| FrameSequenceTrackerCollection::CreateTracker(FrameSequenceTrackerType type) { |
| // The collection always outlives the trackers. So using Unretained() here is |
| // safe. |
| auto tracker = base::WrapUnique(new FrameSequenceTracker( |
| type, base::BindOnce(&FrameSequenceTrackerCollection::RemoveFrameTracker, |
| base::Unretained(this)))); |
| AddFrameTracker(tracker.get()); |
| return tracker; |
| } |
| |
| void FrameSequenceTrackerCollection::ScheduleRemoval( |
| std::unique_ptr<FrameSequenceTracker> tracker) { |
| if (!tracker) |
| return; |
| tracker->ScheduleTerminate(); |
| removal_trackers_.push_back(std::move(tracker)); |
| } |
| |
| void FrameSequenceTrackerCollection::ClearAll() { |
| removal_trackers_.clear(); |
| DCHECK(frame_trackers_.empty()); |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyBeginImplFrame( |
| const viz::BeginFrameArgs& args) { |
| for (auto* tracker : frame_trackers_) { |
| tracker->ReportBeginImplFrame(args); |
| } |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyBeginMainFrame( |
| const viz::BeginFrameArgs& args) { |
| for (auto* tracker : frame_trackers_) { |
| tracker->ReportBeginMainFrame(args); |
| } |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyImplFrameCausedNoDamage( |
| const viz::BeginFrameAck& ack) { |
| for (auto* tracker : frame_trackers_) { |
| tracker->ReportImplFrameCausedNoDamage(ack); |
| } |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyMainFrameCausedNoDamage( |
| const viz::BeginFrameArgs& args) { |
| for (auto* tracker : frame_trackers_) { |
| tracker->ReportMainFrameCausedNoDamage(args); |
| } |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyPauseFrameProduction() { |
| for (auto* tracker : frame_trackers_) |
| tracker->PauseFrameProduction(); |
| } |
| |
| void FrameSequenceTrackerCollection::NotifySubmitFrame( |
| uint32_t frame_token, |
| const viz::BeginFrameAck& ack, |
| const viz::BeginFrameArgs& origin_args) { |
| for (auto* tracker : frame_trackers_) { |
| tracker->ReportSubmitFrame(frame_token, ack, origin_args); |
| } |
| } |
| |
| void FrameSequenceTrackerCollection::NotifyFramePresented( |
| uint32_t frame_token, |
| const gfx::PresentationFeedback& feedback) { |
| for (auto* tracker : frame_trackers_) |
| tracker->ReportFramePresented(frame_token, feedback); |
| |
| // Destroy the trackers that are ready to be terminated. |
| base::EraseIf( |
| removal_trackers_, |
| [](const std::unique_ptr<FrameSequenceTracker>& tracker) { |
| return tracker->termination_status() == |
| FrameSequenceTracker::TerminationStatus::kReadyForTermination; |
| }); |
| } |
| |
| void FrameSequenceTrackerCollection::AddFrameTracker( |
| FrameSequenceTracker* tracker) { |
| frame_trackers_.push_back(tracker); |
| } |
| |
| void FrameSequenceTrackerCollection::RemoveFrameTracker( |
| FrameSequenceTracker* tracker) { |
| base::Erase(frame_trackers_, tracker); |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // FrameSequenceTracker |
| |
| FrameSequenceTracker::FrameSequenceTracker( |
| FrameSequenceTrackerType type, |
| base::OnceCallback<void(FrameSequenceTracker*)> destroy_callback) |
| : type_(type), destroy_callback_(std::move(destroy_callback)) { |
| DCHECK_LT(type_, FrameSequenceTrackerType::kMaxType); |
| TRACE_EVENT_ASYNC_BEGIN1("cc,benchmark", "FrameSequenceTracker", this, "name", |
| TRACE_STR_COPY(kBuiltinSequences[type_])); |
| } |
| |
| FrameSequenceTracker::~FrameSequenceTracker() { |
| DCHECK_LE(impl_throughput_.frames_produced, impl_throughput_.frames_expected); |
| DCHECK_LE(main_throughput_.frames_produced, main_throughput_.frames_expected); |
| DCHECK_LE(main_throughput_.frames_produced, impl_throughput_.frames_produced); |
| TRACE_EVENT_ASYNC_END1( |
| "cc,benchmark", "FrameSequenceTracker", this, "args", |
| ThroughputData::ToTracedValue(impl_throughput_, main_throughput_)); |
| ThroughputData::ReportHistogram( |
| type_, "CompositorThread", |
| GetIndexForMetric(ThreadType::kCompositor, type_), impl_throughput_); |
| ThroughputData::ReportHistogram(type_, "MainThread", |
| GetIndexForMetric(ThreadType::kMain, type_), |
| main_throughput_); |
| std::move(destroy_callback_).Run(this); |
| } |
| |
| void FrameSequenceTracker::ReportBeginImplFrame( |
| const viz::BeginFrameArgs& args) { |
| if (termination_status_ != TerminationStatus::kActive) |
| return; |
| |
| if (ShouldIgnoreBeginFrameSource(args.source_id)) |
| return; |
| |
| UpdateTrackedFrameData(&begin_impl_frame_data_, args.source_id, |
| args.sequence_number); |
| impl_throughput_.frames_expected += |
| begin_impl_frame_data_.previous_sequence_delta; |
| } |
| |
| void FrameSequenceTracker::ReportBeginMainFrame( |
| const viz::BeginFrameArgs& args) { |
| if (termination_status_ != TerminationStatus::kActive) |
| return; |
| |
| if (ShouldIgnoreBeginFrameSource(args.source_id)) |
| return; |
| |
| UpdateTrackedFrameData(&begin_main_frame_data_, args.source_id, |
| args.sequence_number); |
| if (first_received_main_sequence_ == 0) |
| first_received_main_sequence_ = args.sequence_number; |
| main_throughput_.frames_expected += |
| begin_main_frame_data_.previous_sequence_delta; |
| } |
| |
| void FrameSequenceTracker::ReportSubmitFrame( |
| uint32_t frame_token, |
| const viz::BeginFrameAck& ack, |
| const viz::BeginFrameArgs& origin_args) { |
| if (termination_status_ != TerminationStatus::kActive) |
| return; |
| |
| if (ShouldIgnoreBeginFrameSource(ack.source_id)) |
| return; |
| |
| if (begin_impl_frame_data_.previous_sequence == 0 || |
| ack.sequence_number < begin_impl_frame_data_.previous_sequence) { |
| return; |
| } |
| |
| if (first_submitted_frame_ == 0) |
| first_submitted_frame_ = frame_token; |
| last_submitted_frame_ = frame_token; |
| |
| if (!ShouldIgnoreBeginFrameSource(origin_args.source_id) && |
| first_received_main_sequence_ && |
| origin_args.sequence_number >= first_received_main_sequence_) { |
| if (last_submitted_main_sequence_ == 0 || |
| origin_args.sequence_number > last_submitted_main_sequence_) { |
| last_submitted_main_sequence_ = origin_args.sequence_number; |
| main_frames_.push_back(frame_token); |
| DCHECK_GE(main_throughput_.frames_expected, main_frames_.size()); |
| } |
| } |
| } |
| |
| void FrameSequenceTracker::ReportFramePresented( |
| uint32_t frame_token, |
| const gfx::PresentationFeedback& feedback) { |
| const bool frame_token_acks_last_frame = |
| frame_token == last_submitted_frame_ || |
| viz::FrameTokenGT(frame_token, last_submitted_frame_); |
| |
| // Update termination status if this is scheduled for termination, and it is |
| // not waiting for any frames, or it has received the presentation-feedback |
| // for the latest frame it is tracking. |
| if (termination_status_ == TerminationStatus::kScheduledForTermination && |
| (last_submitted_frame_ == 0 || frame_token_acks_last_frame)) { |
| termination_status_ = TerminationStatus::kReadyForTermination; |
| } |
| |
| if (first_submitted_frame_ == 0 || |
| viz::FrameTokenGT(first_submitted_frame_, frame_token)) { |
| // We are getting presentation feedback for frames that were submitted |
| // before this sequence started. So ignore these. |
| return; |
| } |
| |
| const bool was_presented = !feedback.timestamp.is_null(); |
| if (was_presented && last_submitted_frame_) { |
| DCHECK_LT(impl_throughput_.frames_produced, |
| impl_throughput_.frames_expected); |
| ++impl_throughput_.frames_produced; |
| |
| if (frame_token_acks_last_frame) |
| last_submitted_frame_ = 0; |
| } |
| |
| while (!main_frames_.empty() && |
| !viz::FrameTokenGT(main_frames_.front(), frame_token)) { |
| if (was_presented && main_frames_.front() == frame_token) { |
| DCHECK_LT(main_throughput_.frames_produced, |
| main_throughput_.frames_expected); |
| ++main_throughput_.frames_produced; |
| } |
| main_frames_.pop_front(); |
| } |
| } |
| |
| void FrameSequenceTracker::ReportImplFrameCausedNoDamage( |
| const viz::BeginFrameAck& ack) { |
| if (termination_status_ != TerminationStatus::kActive) |
| return; |
| |
| if (ShouldIgnoreBeginFrameSource(ack.source_id)) |
| return; |
| |
| // It is possible that this is called before a begin-impl-frame has been |
| // dispatched for this frame-sequence. In such cases, ignore this call. |
| if (begin_impl_frame_data_.previous_sequence == 0 || |
| ack.sequence_number < begin_impl_frame_data_.previous_sequence) { |
| return; |
| } |
| DCHECK_GT(impl_throughput_.frames_expected, 0u); |
| DCHECK_GT(impl_throughput_.frames_expected, impl_throughput_.frames_produced); |
| --impl_throughput_.frames_expected; |
| |
| if (begin_impl_frame_data_.previous_sequence == ack.sequence_number) |
| begin_impl_frame_data_.previous_sequence = 0; |
| } |
| |
| void FrameSequenceTracker::ReportMainFrameCausedNoDamage( |
| const viz::BeginFrameArgs& args) { |
| if (termination_status_ != TerminationStatus::kActive) |
| return; |
| |
| if (ShouldIgnoreBeginFrameSource(args.source_id)) |
| return; |
| |
| // It is possible that this is called before a begin-main-frame has been |
| // dispatched for this frame-sequence. In such cases, ignore this call. |
| if (begin_main_frame_data_.previous_sequence == 0 || |
| args.sequence_number < begin_main_frame_data_.previous_sequence) { |
| return; |
| } |
| |
| DCHECK_GT(main_throughput_.frames_expected, 0u); |
| DCHECK_GT(main_throughput_.frames_expected, main_throughput_.frames_produced); |
| --main_throughput_.frames_expected; |
| DCHECK_GE(main_throughput_.frames_expected, main_frames_.size()); |
| |
| if (begin_main_frame_data_.previous_sequence == args.sequence_number) |
| begin_main_frame_data_.previous_sequence = 0; |
| } |
| |
| void FrameSequenceTracker::PauseFrameProduction() { |
| // Reset the states, so that the tracker ignores the vsyncs until the next |
| // received begin-frame. |
| begin_impl_frame_data_ = {0, 0, 0}; |
| begin_main_frame_data_ = {0, 0, 0}; |
| } |
| |
| void FrameSequenceTracker::UpdateTrackedFrameData(TrackedFrameData* frame_data, |
| uint64_t source_id, |
| uint64_t sequence_number) { |
| if (frame_data->previous_sequence && |
| frame_data->previous_source == source_id) { |
| uint8_t current_latency = sequence_number - frame_data->previous_sequence; |
| frame_data->previous_sequence_delta = current_latency; |
| } else { |
| frame_data->previous_sequence_delta = 1; |
| } |
| frame_data->previous_source = source_id; |
| frame_data->previous_sequence = sequence_number; |
| } |
| |
| bool FrameSequenceTracker::ShouldIgnoreBeginFrameSource( |
| uint64_t source_id) const { |
| if (begin_impl_frame_data_.previous_source == 0) |
| return false; |
| return source_id != begin_impl_frame_data_.previous_source; |
| } |
| |
| std::unique_ptr<base::trace_event::TracedValue> |
| FrameSequenceTracker::ThroughputData::ToTracedValue( |
| const ThroughputData& impl, |
| const ThroughputData& main) { |
| auto dict = std::make_unique<base::trace_event::TracedValue>(); |
| dict->SetInteger("impl-frames-produced", impl.frames_produced); |
| dict->SetInteger("impl-frames-expected", impl.frames_expected); |
| dict->SetInteger("main-frames-produced", main.frames_produced); |
| dict->SetInteger("main-frames-expected", main.frames_expected); |
| return dict; |
| } |
| |
| void FrameSequenceTracker::ThroughputData::ReportHistogram( |
| FrameSequenceTrackerType sequence_type, |
| const char* thread_name, |
| int metric_index, |
| const ThroughputData& data) { |
| DCHECK_LT(sequence_type, FrameSequenceTrackerType::kMaxType); |
| |
| UMA_HISTOGRAM_COUNTS_1000("Graphics.Smoothness.FrameSequenceLength", |
| data.frames_expected); |
| |
| // Avoid reporting any throughput metric for sequences that had a small amount |
| // of frames. |
| constexpr int kMinFramesForThroughputMetric = 4; |
| if (data.frames_expected < kMinFramesForThroughputMetric) |
| return; |
| |
| const std::string name = |
| base::StrCat({"Graphics.Smoothness.Throughput.", thread_name, ".", |
| kBuiltinSequences[sequence_type]}); |
| const int percent = |
| static_cast<int>(100 * data.frames_produced / data.frames_expected); |
| STATIC_HISTOGRAM_POINTER_GROUP( |
| name, metric_index, kMaximumHistogramIndex, Add(percent), |
| base::LinearHistogram::FactoryGet( |
| name, 1, 100, 101, base::HistogramBase::kUmaTargetedHistogramFlag)); |
| } |
| |
| } // namespace cc |