blob: c86a5f4072199b15815753c5009f8db419d7d3de [file] [log] [blame]
// 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_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/strcat.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 {
namespace {
enum class PerScrollHistogramType {
// For Event.ScrollJank.DelayedFramesPercentage.PerScroll.* histograms.
kPercentage = 0,
// For Event.ScrollJank.MissedVsyncsMax.PerScroll.* histograms.
kMax = 1,
// For Event.ScrollJank.MissedVsyncsSum.PerScroll.* histograms.
kSum = 2,
};
// Histogram min, max and no. of buckets.
constexpr int kVsyncCountsMin = 1;
constexpr int kVsyncCountsMax = 50;
constexpr int kVsyncCountsBuckets = 25;
const char* GetPerScrollHistogramName(int num_frames,
PerScrollHistogramType type) {
DCHECK_GT(num_frames, 0);
if (type == PerScrollHistogramType::kPercentage) {
if (num_frames <= 16) {
return "Event.ScrollJank.DelayedFramesPercentage.PerScroll.Small";
} else if (num_frames <= 64) {
return "Event.ScrollJank.DelayedFramesPercentage.PerScroll.Medium";
} else {
return "Event.ScrollJank.DelayedFramesPercentage.PerScroll.Large";
}
} else if (type == PerScrollHistogramType::kMax) {
if (num_frames <= 16) {
return "Event.ScrollJank.MissedVsyncsMax.PerScroll.Small";
} else if (num_frames <= 64) {
return "Event.ScrollJank.MissedVsyncsMax.PerScroll.Medium";
} else {
return "Event.ScrollJank.MissedVsyncsMax.PerScroll.Large";
}
} else {
DCHECK_EQ(type, PerScrollHistogramType::kSum);
if (num_frames <= 16) {
return "Event.ScrollJank.MissedVsyncsSum.PerScroll.Small";
} else if (num_frames <= 64) {
return "Event.ScrollJank.MissedVsyncsSum.PerScroll.Medium";
} else {
return "Event.ScrollJank.MissedVsyncsSum.PerScroll.Large";
}
}
}
const char* GetPerVsyncScrollHistogramName(int num_vsyncs,
PerScrollHistogramType type) {
DCHECK_GT(num_vsyncs, 0);
if (type == PerScrollHistogramType::kPercentage) {
if (num_vsyncs <= 16) {
return "Event.ScrollJank.MissedVsyncsPercentage.PerScroll.Small";
} else if (num_vsyncs <= 64) {
return "Event.ScrollJank.MissedVsyncsPercentage.PerScroll.Medium";
} else {
return "Event.ScrollJank.MissedVsyncsPercentage.PerScroll.Large";
}
} else {
NOTREACHED();
return "";
}
}
} // namespace
ScrollJankDroppedFrameTracker::ScrollJankDroppedFrameTracker() {
// 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.
fixed_window_.num_presented_frames = -1;
experimental_vsync_fixed_window_.num_past_vsyncs = -1;
}
ScrollJankDroppedFrameTracker::~ScrollJankDroppedFrameTracker() {
if (per_scroll_.has_value()) {
// Per scroll metrics for a given scroll are emitted at the start of next
// scroll. Emittimg from here makes sure we don't loose the data for last
// scroll.
EmitPerScrollHistogramsAndResetCounters();
EmitPerScrollVsyncHistogramsAndResetCounters();
}
}
void ScrollJankDroppedFrameTracker::EmitPerScrollHistogramsAndResetCounters() {
// There should be at least one presented frame given the method is only
// called after we have a successful presentation.
if (per_scroll_->num_presented_frames == 0) {
// TODO(1464878): Debug cases where we can have 0 presented frames.
TRACE_EVENT_INSTANT("input", "NoPresentedFramesInScroll");
return;
}
// Emit non-bucketed histograms.
int delayed_frames_percentage =
(100 * per_scroll_->missed_frames) / per_scroll_->num_presented_frames;
UMA_HISTOGRAM_PERCENTAGE(kDelayedFramesPerScrollHistogram,
delayed_frames_percentage);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsMaxPerScrollHistogram,
per_scroll_->max_missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsSumPerScrollHistogram,
per_scroll_->missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
// Emit bucketed histogram.
base::UmaHistogramPercentage(
GetPerScrollHistogramName(per_scroll_->num_presented_frames,
PerScrollHistogramType::kPercentage),
delayed_frames_percentage);
base::UmaHistogramCustomCounts(
GetPerScrollHistogramName(per_scroll_->num_presented_frames,
PerScrollHistogramType::kMax),
per_scroll_->max_missed_vsyncs, kVsyncCountsMin, kVsyncCountsMax,
kVsyncCountsBuckets);
base::UmaHistogramCustomCounts(
GetPerScrollHistogramName(per_scroll_->num_presented_frames,
PerScrollHistogramType::kSum),
per_scroll_->missed_vsyncs, kVsyncCountsMin, kVsyncCountsMax,
kVsyncCountsBuckets);
per_scroll_->missed_frames = 0;
per_scroll_->missed_vsyncs = 0;
per_scroll_->num_presented_frames = 0;
per_scroll_->max_missed_vsyncs = 0;
}
void ScrollJankDroppedFrameTracker::
EmitPerScrollVsyncHistogramsAndResetCounters() {
// There should be at least one presented frame given the method is only
// called after we have a successful presentation.
if (experimental_per_scroll_vsync_->num_past_vsyncs == 0) {
return;
}
// Emit non-bucketed histograms.
int missed_vsyncs_percentage =
(100 * experimental_per_scroll_vsync_->missed_vsyncs) /
experimental_per_scroll_vsync_->num_past_vsyncs;
UMA_HISTOGRAM_PERCENTAGE(kMissedVsyncsPerScrollHistogram,
missed_vsyncs_percentage);
// Emit bucketed histogram.
base::UmaHistogramPercentage(
GetPerVsyncScrollHistogramName(
experimental_per_scroll_vsync_->num_past_vsyncs,
PerScrollHistogramType::kPercentage),
missed_vsyncs_percentage);
experimental_per_scroll_vsync_->missed_frames = 0;
experimental_per_scroll_vsync_->missed_vsyncs = 0;
experimental_per_scroll_vsync_->num_past_vsyncs = 0;
experimental_per_scroll_vsync_->max_missed_vsyncs = 0;
}
void ScrollJankDroppedFrameTracker::EmitPerWindowHistogramsAndResetCounters() {
DCHECK_EQ(fixed_window_.num_presented_frames, kHistogramEmitFrequency);
UMA_HISTOGRAM_PERCENTAGE(
kDelayedFramesWindowHistogram,
(100 * fixed_window_.missed_frames) / kHistogramEmitFrequency);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsSumInWindowHistogram,
fixed_window_.missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsMaxInWindowHistogram,
fixed_window_.max_missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
fixed_window_.missed_frames = 0;
fixed_window_.missed_vsyncs = 0;
fixed_window_.max_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.
fixed_window_.num_presented_frames = 0;
}
// TODO(b/306611560): Cleanup experimental per vsync metric or promote to
// default.
void ScrollJankDroppedFrameTracker::
EmitPerVsyncWindowHistogramsAndResetCounters() {
DCHECK_GE(experimental_vsync_fixed_window_.num_past_vsyncs,
kHistogramEmitFrequency);
UMA_HISTOGRAM_PERCENTAGE(
kMissedVsyncsWindowHistogram,
(100 * experimental_vsync_fixed_window_.missed_vsyncs) /
experimental_vsync_fixed_window_.num_past_vsyncs);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsSumInVsyncWindowHistogram,
experimental_vsync_fixed_window_.missed_vsyncs,
kVsyncCountsMin, kVsyncCountsMax,
kVsyncCountsBuckets);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsMaxInVsyncWindowHistogram,
fixed_window_.max_missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
experimental_vsync_fixed_window_.missed_vsyncs = 0;
experimental_vsync_fixed_window_.num_past_vsyncs = 0;
experimental_vsync_fixed_window_.max_missed_vsyncs = 0;
}
void ScrollJankDroppedFrameTracker::ReportLatestPresentationData(
ScrollUpdateEventMetrics& earliest_event,
base::TimeTicks last_input_generation_ts,
base::TimeTicks presentation_ts,
base::TimeDelta vsync_interval) {
base::TimeTicks first_input_generation_ts =
earliest_event.GetDispatchStageTimestamp(
EventMetrics::DispatchStage::kGenerated);
if ((last_input_generation_ts < first_input_generation_ts) ||
(presentation_ts <= last_input_generation_ts)) {
// TODO(crbug/1447358): Investigate when these edge cases can be triggered
// in field and web tests. We have already seen this triggered in field, and
// some web tests where an event with null(0) timestamp gets coalesced with
// a "normal" input.
return;
}
// 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;
}
// `per_scroll_` is initialized in OnScrollStarted when we see
// FIRST_GESTURE_SCROLL_UPDATE event. But in some rare scenarios we don't see
// the FIRST_GESTURE_SCROLL_UPDATE events on scroll start.
if (!per_scroll_.has_value()) {
per_scroll_ = JankData();
experimental_per_scroll_vsync_ = JankData();
}
// 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);
// Sometimes the vsync interval is not accurate and is slightly more
// than the actual signal arrival time, adding (vsync_interval / 2)
// here insures the result is always ceiled.
int curr_frame_total_vsyncs =
(presentation_ts - prev_presentation_ts_ + (vsync_interval / 2)) /
vsync_interval;
int curr_frame_missed_vsyncs = curr_frame_total_vsyncs - 1;
if (missed_frame && input_available) {
++fixed_window_.missed_frames;
++per_scroll_->missed_frames;
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsPerFrameHistogram,
curr_frame_missed_vsyncs, kVsyncCountsMin,
kVsyncCountsMax, kVsyncCountsBuckets);
fixed_window_.missed_vsyncs += curr_frame_missed_vsyncs;
per_scroll_->missed_vsyncs += curr_frame_missed_vsyncs;
// TODO(b/306611560): If experimental per scroll logic is promoted to
// default, then UKM reporting will need to be recorded under the same
// conditions.
if (scroll_jank_ukm_reporter_) {
scroll_jank_ukm_reporter_->IncrementDelayedFrameCount();
scroll_jank_ukm_reporter_->AddMissedVsyncs(curr_frame_missed_vsyncs);
}
if (curr_frame_missed_vsyncs > per_scroll_->max_missed_vsyncs) {
per_scroll_->max_missed_vsyncs = curr_frame_missed_vsyncs;
if (scroll_jank_ukm_reporter_) {
scroll_jank_ukm_reporter_->set_max_missed_vsyncs(
curr_frame_missed_vsyncs);
}
}
if (curr_frame_missed_vsyncs > fixed_window_.max_missed_vsyncs) {
fixed_window_.max_missed_vsyncs = curr_frame_missed_vsyncs;
}
TRACE_EVENT_INSTANT(
"input,input.scrolling", "MissedFrame", "per_scroll_->missed_frames",
per_scroll_->missed_frames, "per_scroll_->missed_vsyncs",
per_scroll_->missed_vsyncs, "vsync_interval", vsync_interval);
earliest_event.set_is_janky_scrolled_frame(true);
} else {
earliest_event.set_is_janky_scrolled_frame(false);
UMA_HISTOGRAM_CUSTOM_COUNTS(kMissedVsyncsPerFrameHistogram, 0,
kVsyncCountsMin, kVsyncCountsMax,
kVsyncCountsBuckets);
}
if (input_available) {
// Per 64 vsyncs
experimental_vsync_fixed_window_.missed_vsyncs += curr_frame_missed_vsyncs;
experimental_vsync_fixed_window_.num_past_vsyncs += curr_frame_total_vsyncs;
experimental_vsync_fixed_window_.max_missed_vsyncs =
std::max(experimental_vsync_fixed_window_.max_missed_vsyncs,
curr_frame_missed_vsyncs);
// Per scroll
experimental_per_scroll_vsync_->missed_vsyncs += curr_frame_missed_vsyncs;
experimental_per_scroll_vsync_->num_past_vsyncs += curr_frame_total_vsyncs;
if (scroll_jank_ukm_reporter_) {
scroll_jank_ukm_reporter_->AddVsyncs(curr_frame_total_vsyncs);
}
} else {
++experimental_vsync_fixed_window_.num_past_vsyncs;
++experimental_per_scroll_vsync_->num_past_vsyncs;
if (scroll_jank_ukm_reporter_) {
scroll_jank_ukm_reporter_->AddVsyncs(1);
}
}
++fixed_window_.num_presented_frames;
++per_scroll_->num_presented_frames;
if (scroll_jank_ukm_reporter_) {
scroll_jank_ukm_reporter_->IncrementFrameCount();
}
if (fixed_window_.num_presented_frames == kHistogramEmitFrequency) {
EmitPerWindowHistogramsAndResetCounters();
}
// TODO(b/306611560): Cleanup experimental per vsync metric or promote to
// default.
if (experimental_vsync_fixed_window_.num_past_vsyncs >=
kHistogramEmitFrequency) {
EmitPerVsyncWindowHistogramsAndResetCounters();
}
DCHECK_LT(fixed_window_.num_presented_frames, kHistogramEmitFrequency);
prev_presentation_ts_ = presentation_ts;
prev_last_input_generation_ts_ = last_input_generation_ts;
}
void ScrollJankDroppedFrameTracker::OnScrollStarted() {
if (per_scroll_.has_value()) {
EmitPerScrollHistogramsAndResetCounters();
EmitPerScrollVsyncHistogramsAndResetCounters();
} else {
per_scroll_ = JankData();
experimental_per_scroll_vsync_ = JankData();
}
}
} // namespace cc