blob: 75b4f9a874215c21db05f414f2c8f6cdc2a59b16 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/viz/service/display/frame_interval_matchers.h"
#include <algorithm>
#include <utility>
#include <variant>
#include "base/strings/stringprintf.h"
#include "base/trace_event/typed_macros.h"
#include "components/viz/common/quads/frame_interval_inputs.h"
#include "media/filters/video_cadence_estimator.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
namespace viz {
namespace {
// Matches case where only has content of type `type` is updating and they all
// have the same content frame interval. Then return that interval.
std::optional<FrameIntervalMatcher::Result> MatchContentIntervalType(
const FrameIntervalMatcher::Inputs& matcher_inputs,
ContentFrameIntervalType type,
FrameIntervalMatcher::ResultIntervalType interval_type) {
std::optional<base::TimeDelta> content_interval;
for (const auto& [frame_sink_id, inputs] : matcher_inputs.inputs_map) {
// Skip frame sinks that are old.
if ((matcher_inputs.aggregated_frame_time - inputs.frame_time) >
matcher_inputs.settings->ignore_frame_sink_timeout) {
continue;
}
// Don't match if viz client has other updates.
if (!inputs.has_only_content_frame_interval_updates) {
return std::nullopt;
}
for (const ContentFrameIntervalInfo& content_frame_interval_info :
inputs.content_interval_info) {
// Skip if there are other types of content.
if (content_frame_interval_info.type != type) {
return std::nullopt;
}
// Don't match if content interval are not all the same.
if (content_interval && !FrameIntervalMatcher::AreAlmostEqual(
content_interval.value(),
content_frame_interval_info.frame_interval,
matcher_inputs.settings->epsilon)) {
return std::nullopt;
}
content_interval = inputs.content_interval_info.front().frame_interval;
}
}
// Fail to match if no content type matches.
if (!content_interval) {
return std::nullopt;
}
base::TimeDelta interval = std::visit(
absl::Overload(
[&](const std::monostate& monostate) {
// If no intervals settings are given, then just return the content
// interval.
return content_interval.value();
},
[&](const FrameIntervalMatcher::FixedIntervalSettings&
fixed_interval_settings) {
// Pick the best interval from supported intervals using
// `HasSimpleCadence`.
std::optional<base::TimeDelta> best_interval;
for (auto supported_interval :
fixed_interval_settings.supported_intervals) {
bool simple_cadence =
media::VideoCadenceEstimator::HasSimpleCadence(
supported_interval, content_interval.value(),
matcher_inputs.settings->max_time_until_next_glitch);
if (simple_cadence &&
(!best_interval || supported_interval > best_interval)) {
best_interval = supported_interval;
}
}
return best_interval.value_or(
fixed_interval_settings.default_interval);
},
[&](const FrameIntervalMatcher::ContinuousRangeSettings&
continuous_range_settings) {
// Pick the best interval within the continuous range, such that the
// chosen value has a perfect integer cadence relative to the
// target.
base::TimeDelta range_min = continuous_range_settings.min_interval;
base::TimeDelta range_max = continuous_range_settings.max_interval;
// If the target is below the range minimum (too fast), determine
// the minimum cadence necessary to reach the minimum interval.
if (content_interval.value() < range_min) {
int cadence = std::ceil(range_min / content_interval.value());
base::TimeDelta cadence_interval =
cadence * content_interval.value();
// Use the calculated cadence if it didn't overshoot the range
// maximum. Otherwise use the range minimum as the closest
// fallback.
return cadence_interval <= range_max ? cadence_interval
: range_min;
}
// If the target is above the range maximum (too slow), determine
// the minimum cadence necessary to reach the maximum interval.
if (content_interval.value() > range_max) {
// Use inverse cadence (i.e. 1/2, 1/3, 1/4, ... of the content
// interval.)
int cadence = std::ceil(content_interval.value() / range_max);
base::TimeDelta cadence_interval =
content_interval.value() / cadence;
// Use the calculated cadence if it didn't undershoot the range
// minimum. Otherwise use the range maximum as the closest
// fallback.
return cadence_interval >= range_min ? cadence_interval
: range_max;
}
// Content falls within the supported range and can be used
// directly.
return content_interval.value();
}),
matcher_inputs.settings->interval_settings);
return FrameIntervalMatcher::ResultInterval{interval, interval_type};
}
} // namespace
bool FrameIntervalMatcher::ResultInterval::operator==(
const ResultInterval& other) const = default;
FrameIntervalMatcher::FixedIntervalSettings::FixedIntervalSettings() = default;
FrameIntervalMatcher::FixedIntervalSettings::FixedIntervalSettings(
const FixedIntervalSettings&) = default;
FrameIntervalMatcher::FixedIntervalSettings::~FixedIntervalSettings() = default;
FrameIntervalMatcher::ContinuousRangeSettings::ContinuousRangeSettings() =
default;
FrameIntervalMatcher::ContinuousRangeSettings::ContinuousRangeSettings(
const ContinuousRangeSettings&) = default;
FrameIntervalMatcher::ContinuousRangeSettings::~ContinuousRangeSettings() =
default;
FrameIntervalMatcher::Settings::Settings() = default;
FrameIntervalMatcher::Settings::~Settings() = default;
FrameIntervalMatcher::Settings::Settings(const Settings& other) = default;
FrameIntervalMatcher::Settings& FrameIntervalMatcher::Settings::operator=(
const Settings& other) = default;
FrameIntervalMatcher::Settings::Settings(Settings&& other) = default;
FrameIntervalMatcher::Settings& FrameIntervalMatcher::Settings::operator=(
Settings&& other) = default;
FrameIntervalMatcher::Inputs::Inputs(const Settings& settings,
uint64_t frame_id)
: settings(settings), frame_id(frame_id) {}
FrameIntervalMatcher::Inputs::~Inputs() = default;
FrameIntervalMatcher::Inputs::Inputs(const Inputs& other) = default;
FrameIntervalMatcher::Inputs& FrameIntervalMatcher::Inputs::operator=(
const Inputs& other) = default;
void FrameIntervalMatcher::Inputs::WriteIntoTrace(
perfetto::TracedValue trace_context) const {
auto dict = std::move(trace_context).WriteDictionary();
for (const auto& [frame_sink_id, interval_inputs] : inputs_map) {
// frame_sink_dict needs to be enclosed in a dedicated code block so that it
// would go out of scope and be destructed before content_info_dict is
// created. See https://crbug.com/371227621.
{
auto frame_sink_dict =
dict.AddDictionary(perfetto::DynamicString(frame_sink_id.ToString()));
frame_sink_dict.Add("time_diff_us",
(aggregated_frame_time - interval_inputs.frame_time)
.InMicroseconds());
frame_sink_dict.Add("has_input", interval_inputs.has_input);
frame_sink_dict.Add(
"only_content",
interval_inputs.has_only_content_frame_interval_updates);
}
int index = 0;
for (const ContentFrameIntervalInfo& content_info :
interval_inputs.content_interval_info) {
auto content_info_dict = dict.AddDictionary(perfetto::DynamicString(
base::StringPrintf("content_info_%d", index)));
content_info_dict.Add(
"type", ContentFrameIntervalTypeToString(content_info.type));
content_info_dict.Add("interval_us",
content_info.frame_interval.InMicroseconds());
content_info_dict.Add("duplicate_count", content_info.duplicate_count);
index++;
}
}
}
// static
std::string FrameIntervalMatcher::ResultToString(const Result& result) {
return std::visit(
absl::Overload(
[](FrameIntervalClass frame_interval_class) -> std::string {
switch (frame_interval_class) {
case FrameIntervalClass::kBoost:
return "kBoost";
case FrameIntervalClass::kDefault:
return "kDefault";
}
},
[](ResultInterval interval) {
return base::StringPrintf("%" PRId64 "us type:%d",
interval.interval.InMicroseconds(),
static_cast<int>(interval.type));
}),
result);
}
// static
std::string FrameIntervalMatcher::MatcherTypeToString(
FrameIntervalMatcherType type) {
switch (type) {
case FrameIntervalMatcherType::kNone:
return "None";
case FrameIntervalMatcherType::kInputBoost:
return "InputBoost";
case FrameIntervalMatcherType::kSlowScrollThrottle:
return "SlowScrollThrottle";
case FrameIntervalMatcherType::kOnlyVideo:
return "OnlyVideo";
case FrameIntervalMatcherType::kVideoConference:
return "VideoConference";
case FrameIntervalMatcherType::kOnlyAnimatingImage:
return "kOnlyAnimatingImage";
case FrameIntervalMatcherType::kUserInputBoost:
return "UserInputBoost";
case FrameIntervalMatcherType::kOnlyScrollBarFadeOut:
return "OnlyScrollBarFadeOut";
}
}
// static
bool FrameIntervalMatcher::AreAlmostEqual(base::TimeDelta a,
base::TimeDelta b,
base::TimeDelta epsilon) {
if (a.is_min() || b.is_min() || a.is_max() || b.is_max()) {
return a == b;
}
return (a - b).magnitude() <= epsilon;
}
FrameIntervalMatcher::FrameIntervalMatcher(FrameIntervalMatcherType type)
: type_(type) {}
#define DefineSimpleMatcherConstructorDestructor(ClassName, MatcherType) \
ClassName::ClassName() \
: FrameIntervalMatcher(FrameIntervalMatcherType::MatcherType) {} \
ClassName::~ClassName() = default
// If there's any input, return kBoost or the highest supported frame interval.
DefineSimpleMatcherConstructorDestructor(InputBoostMatcher, kInputBoost);
std::optional<FrameIntervalMatcher::Result> InputBoostMatcher::Match(
const Inputs& matcher_inputs) {
for (const auto& [frame_sink_id, inputs] : matcher_inputs.inputs_map) {
if (inputs.has_input &&
(matcher_inputs.aggregated_frame_time - inputs.frame_time) <
matcher_inputs.settings->ignore_frame_sink_timeout) {
return std::visit(
absl::Overload(
[](const std::monostate& monostate) -> Result {
return FrameIntervalClass::kBoost;
},
[](const FixedIntervalSettings& fixed_interval_settings)
-> Result {
return ResultInterval{
*fixed_interval_settings.supported_intervals.begin(),
ResultIntervalType::kAtLeast};
},
[](const ContinuousRangeSettings& continuous_range_settings)
-> Result {
return ResultInterval{continuous_range_settings.min_interval,
ResultIntervalType::kAtLeast};
}),
matcher_inputs.settings->interval_settings);
}
}
return std::nullopt;
}
// Matches when there are only videos of same frame interval updating.
// Returns the video frame interval or the ideal supported interval (if
// supplied).
DefineSimpleMatcherConstructorDestructor(OnlyVideoMatcher, kOnlyVideo);
std::optional<FrameIntervalMatcher::Result> OnlyVideoMatcher::Match(
const Inputs& matcher_inputs) {
return MatchContentIntervalType(matcher_inputs,
ContentFrameIntervalType::kVideo,
ResultIntervalType::kExact);
}
// Matches video conference case by using heuristic of 2 or more videos.
// Videos do not need to have the same frame interval. If supported intervals
// are supplied, then pick the biggest interval that is smaller than the video's
// interval, which may not be the ideal interval.
DefineSimpleMatcherConstructorDestructor(VideoConferenceMatcher,
kVideoConference);
std::optional<FrameIntervalMatcher::Result> VideoConferenceMatcher::Match(
const Inputs& matcher_inputs) {
size_t num_videos = 0;
std::optional<base::TimeDelta> min_interval;
for (const auto& [frame_sink_id, inputs] : matcher_inputs.inputs_map) {
for (const ContentFrameIntervalInfo& content_frame_interval_info :
inputs.content_interval_info) {
// Ignore non-video.
if (content_frame_interval_info.type !=
ContentFrameIntervalType::kVideo) {
continue;
}
// Skip video that hasn't updated 5 times its interval. These videos are
// probably paused. Not using `ignore_frame_sink_timeout` since these can
// be larger in practice.
if ((matcher_inputs.aggregated_frame_time - inputs.frame_time) >
5 * content_frame_interval_info.frame_interval) {
continue;
}
if (!min_interval ||
min_interval.value() > content_frame_interval_info.frame_interval) {
min_interval = content_frame_interval_info.frame_interval;
}
num_videos += 1u + content_frame_interval_info.duplicate_count;
}
}
if (num_videos < 2u) {
return std::nullopt;
}
base::TimeDelta interval = std::visit(
absl::Overload(
[&](const std::monostate& monostate) { return min_interval.value(); },
[&](const FixedIntervalSettings& fixed_interval_settings) {
// Pick closest supported interval amongst discrete list.
base::TimeDelta closest_supported_interval;
base::TimeDelta min_delta = base::TimeDelta::Max();
for (auto supported_interval :
fixed_interval_settings.supported_intervals) {
base::TimeDelta delta = min_interval.value() - supported_interval;
if ((AreAlmostEqual(min_interval.value(), supported_interval,
matcher_inputs.settings->epsilon) ||
delta.is_positive()) &&
delta.magnitude() < min_delta) {
closest_supported_interval = supported_interval;
min_delta = delta.magnitude();
}
}
return closest_supported_interval;
},
[&](const ContinuousRangeSettings& continuous_range_settings) {
// Pick closest supported interval within continuous range.
return std::clamp(min_interval.value(),
continuous_range_settings.min_interval,
continuous_range_settings.max_interval);
}),
matcher_inputs.settings->interval_settings);
return ResultInterval{interval};
}
DefineSimpleMatcherConstructorDestructor(OnlyAnimatingImageMatcher,
kOnlyAnimatingImage);
std::optional<FrameIntervalMatcher::Result> OnlyAnimatingImageMatcher::Match(
const Inputs& matcher_inputs) {
return MatchContentIntervalType(matcher_inputs,
ContentFrameIntervalType::kAnimatingImage,
ResultIntervalType::kExact);
}
DefineSimpleMatcherConstructorDestructor(OnlyScrollBarFadeOutAnimationMatcher,
kOnlyScrollBarFadeOut);
std::optional<FrameIntervalMatcher::Result>
OnlyScrollBarFadeOutAnimationMatcher::Match(const Inputs& matcher_inputs) {
return MatchContentIntervalType(
matcher_inputs, ContentFrameIntervalType::kScrollBarFadeOutAnimation,
ResultIntervalType::kAtLeast);
}
DefineSimpleMatcherConstructorDestructor(UserInputBoostMatcher,
kUserInputBoost);
std::optional<FrameIntervalMatcher::Result> UserInputBoostMatcher::Match(
const Inputs& matcher_inputs) {
for (const auto& [frame_sink_id, inputs] : matcher_inputs.inputs_map) {
if (inputs.has_user_input &&
(matcher_inputs.aggregated_frame_time - inputs.frame_time) <
matcher_inputs.settings->ignore_frame_sink_timeout) {
return std::visit(
absl::Overload(
[](const std::monostate& monostate) -> Result {
return FrameIntervalClass::kBoost;
},
[](const FixedIntervalSettings& fixed_interval_settings)
-> Result {
return ResultInterval{
*fixed_interval_settings.supported_intervals.begin(),
ResultIntervalType::kAtLeast};
},
[](const ContinuousRangeSettings& continuous_range_settings)
-> Result {
return ResultInterval{continuous_range_settings.min_interval,
ResultIntervalType::kAtLeast};
}),
matcher_inputs.settings->interval_settings);
}
}
return std::nullopt;
}
SlowScrollThrottleMatcher::SlowScrollThrottleMatcher(float device_scale_factor)
: FrameIntervalMatcher(FrameIntervalMatcherType::kSlowScrollThrottle),
device_scale_factor_(device_scale_factor) {}
SlowScrollThrottleMatcher::~SlowScrollThrottleMatcher() = default;
std::optional<FrameIntervalMatcher::Result> SlowScrollThrottleMatcher::Match(
const Inputs& matcher_inputs) {
CHECK(std::holds_alternative<std::monostate>(
matcher_inputs.settings->interval_settings));
float scroll_speed = 0.f;
bool ignored_extra_update = false;
for (const auto& [frame_sink_id, inputs] : matcher_inputs.inputs_map) {
// Skip frame sinks that are old.
if (matcher_inputs.aggregated_frame_time - inputs.frame_time >=
matcher_inputs.settings->ignore_frame_sink_timeout) {
continue;
}
bool has_non_scroll_update =
!inputs.has_only_content_frame_interval_updates ||
std::any_of(
inputs.content_interval_info.begin(),
inputs.content_interval_info.end(),
[](const ContentFrameIntervalInfo& content_frame_interval_info) {
return content_frame_interval_info.type !=
ContentFrameIntervalType::kCompositorScroll;
});
// Scroll can occasionally have new tiles rastered or new content recorded
// in a commit. Ignore these one off frames that has updates beyond just
// scroll. Otherwise, do not match.
if (has_non_scroll_update &&
(matcher_inputs.frame_id - last_frame_id_matched_without_extra_update_ >
1)) {
return std::nullopt;
}
if (has_non_scroll_update) {
ignored_extra_update = true;
}
if (inputs.major_scroll_speed_in_pixels_per_second > scroll_speed) {
scroll_speed = inputs.major_scroll_speed_in_pixels_per_second;
}
}
// No scroll.
if (scroll_speed <= 0) {
return std::nullopt;
}
if (!ignored_extra_update) {
last_frame_id_matched_without_extra_update_ = matcher_inputs.frame_id;
}
float speed_dps = scroll_speed / device_scale_factor_;
// The hard-coded values are copied from AOSP View.convertVelocityToFrameRate.
if (speed_dps > 300) {
return FrameIntervalClass::kBoost;
} else if (speed_dps > 125) {
return ResultInterval{base::Hertz(80), ResultIntervalType::kAtLeast};
} else {
return ResultInterval{base::Hertz(60), ResultIntervalType::kAtLeast};
}
}
} // namespace viz