blob: 69474ed30803f0d696f7abef04f6b338b5c1b2ed [file] [log] [blame]
// Copyright 2020 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/metrics/video_playback_roughness_reporter.h"
#include <algorithm>
#include "base/callback_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/ranges.h"
#include "base/numerics/safe_conversions.h"
#include "components/viz/common/quads/compositor_frame_metadata.h"
namespace {
constexpr int max_worst_windows_size() {
constexpr int size =
1 + cc::VideoPlaybackRoughnessReporter::kMaxWindowsBeforeSubmit *
(100 - cc::VideoPlaybackRoughnessReporter::kPercentileToSubmit) /
100;
static_assert(size > 1, "worst_windows_ is too small");
static_assert(size < 25, "worst_windows_ is too big");
return size;
}
} // namespace
namespace cc {
constexpr int VideoPlaybackRoughnessReporter::kMinWindowSize;
constexpr int VideoPlaybackRoughnessReporter::kMaxWindowSize;
constexpr int VideoPlaybackRoughnessReporter::kMaxWindowsBeforeSubmit;
constexpr int VideoPlaybackRoughnessReporter::kMinWindowsBeforeSubmit;
constexpr int VideoPlaybackRoughnessReporter::kPercentileToSubmit;
VideoPlaybackRoughnessReporter::VideoPlaybackRoughnessReporter(
ReportingCallback reporting_cb)
: reporting_cb_(reporting_cb) {}
VideoPlaybackRoughnessReporter::~VideoPlaybackRoughnessReporter() = default;
double VideoPlaybackRoughnessReporter::ConsecutiveFramesWindow::roughness()
const {
return root_mean_square_error.InMillisecondsF();
}
VideoPlaybackRoughnessReporter::FrameInfo::FrameInfo() = default;
VideoPlaybackRoughnessReporter::FrameInfo::FrameInfo(const FrameInfo&) =
default;
void VideoPlaybackRoughnessReporter::FrameSubmitted(
TokenType token,
const media::VideoFrame& frame,
base::TimeDelta render_interval) {
if (!frames_.empty() && viz::FrameTokenGT(frames_.back().token, token)) {
DCHECK(false) << "Frames submitted out of order.";
return;
}
FrameInfo info;
info.token = token;
info.decode_time = frame.metadata()->decode_end_time;
info.refresh_rate_hz = int{std::round(1.0 / render_interval.InSecondsF())};
info.size = frame.natural_size();
info.intended_duration = frame.metadata()->wallclock_frame_duration;
if (info.intended_duration) {
if (render_interval > info.intended_duration.value()) {
// In videos with FPS higher than display refresh rate we acknowledge
// the fact that some frames will be dropped upstream and frame's intended
// duration can't be less than refresh interval.
info.intended_duration = render_interval;
}
// Adjust frame window size to fit about 1 second of playback
const int win_size =
base::ClampRound(info.intended_duration.value().ToHz());
frames_window_size_ =
base::ClampToRange(win_size, kMinWindowSize, kMaxWindowSize);
}
frames_.push_back(info);
}
void VideoPlaybackRoughnessReporter::FramePresented(TokenType token,
base::TimeTicks timestamp,
bool reliable_timestamp) {
for (auto it = frames_.rbegin(); it != frames_.rend(); it++) {
FrameInfo& info = *it;
if (token == it->token) {
if (info.decode_time.has_value()) {
auto time_since_decode = timestamp - info.decode_time.value();
UMA_HISTOGRAM_TIMES("Media.VideoFrameSubmitter", time_since_decode);
}
if (reliable_timestamp)
info.presentation_time = timestamp;
break;
}
if (viz::FrameTokenGT(token, it->token))
break;
}
}
void VideoPlaybackRoughnessReporter::SubmitPlaybackRoughness() {
// 0-based index, how many times to step away from the begin().
int index_to_submit = windows_seen_ * (100 - kPercentileToSubmit) / 100;
if (index_to_submit < 0 ||
index_to_submit >= static_cast<int>(worst_windows_.size())) {
DCHECK(false);
return;
}
auto it = worst_windows_.begin() + index_to_submit;
Measurement measurement;
measurement.frames = it->size;
measurement.duration = it->intended_duration;
measurement.roughness = it->roughness();
measurement.freezing = max_single_frame_error_;
measurement.refresh_rate_hz = it->refresh_rate_hz;
measurement.frame_size = it->frame_size;
reporting_cb_.Run(measurement);
worst_windows_.clear();
windows_seen_ = 0;
max_single_frame_error_ = base::TimeDelta();
}
void VideoPlaybackRoughnessReporter::ReportWindow(
const ConsecutiveFramesWindow& win) {
worst_windows_.insert(win);
if (worst_windows_.size() > max_worst_windows_size())
worst_windows_.erase(std::prev(worst_windows_.end()));
windows_seen_++;
if (windows_seen_ >= kMaxWindowsBeforeSubmit)
SubmitPlaybackRoughness();
}
void VideoPlaybackRoughnessReporter::ProcessFrameWindow() {
if (static_cast<int>(frames_.size()) <= frames_window_size_) {
// There is no window to speak of, let's wait and process it later.
return;
}
// If possible populate duration for frames that don't have it yet.
auto cur_frame_it = frames_.begin();
auto next_frame_it = std::next(cur_frame_it);
for (; next_frame_it != frames_.end(); cur_frame_it++, next_frame_it++) {
FrameInfo& cur_frame = *cur_frame_it;
const FrameInfo& next_frame = *next_frame_it;
if (cur_frame.actual_duration.has_value())
continue;
if (!cur_frame.presentation_time.has_value() ||
!next_frame.presentation_time.has_value()) {
// We reached a frame that hasn't been presented yet, there is
// no way to keep processing the window.
break;
}
cur_frame.actual_duration = next_frame.presentation_time.value() -
cur_frame.presentation_time.value();
}
int items_to_discard = 0;
const int max_buffer_size = 2 * frames_window_size_;
// There is sufficient number of frames with populated |actual_duration|
// let's calculate window metrics and report it.
if (next_frame_it - frames_.begin() > frames_window_size_) {
ConsecutiveFramesWindow win;
bool observed_change_in_parameters = false;
double mean_square_error_ms2 = 0.0;
base::TimeDelta total_error;
auto& first_frame = frames_.front();
if (first_frame.presentation_time.has_value()) {
win.first_frame_time = first_frame.presentation_time.value();
win.refresh_rate_hz = first_frame.refresh_rate_hz;
win.frame_size = first_frame.size;
}
for (auto i = 0; i < frames_window_size_; i++) {
FrameInfo& frame = frames_[i];
base::TimeDelta error;
if (win.frame_size != frame.size ||
win.refresh_rate_hz != frame.refresh_rate_hz) {
observed_change_in_parameters = true;
break;
}
if (frame.actual_duration.has_value() &&
frame.intended_duration.has_value()) {
error = frame.actual_duration.value() - frame.intended_duration.value();
win.intended_duration += frame.intended_duration.value();
}
total_error += error;
max_single_frame_error_ =
std::max(max_single_frame_error_, error.magnitude());
mean_square_error_ms2 +=
total_error.InMillisecondsF() * total_error.InMillisecondsF();
}
win.size = frames_window_size_;
win.root_mean_square_error = base::TimeDelta::FromMillisecondsD(
std::sqrt(mean_square_error_ms2 / frames_window_size_));
if (observed_change_in_parameters) {
// There has been a change in the frame size or the screen refresh rate,
// whatever roughness stats were accumulated up to this point need to be
// reported or discarded, because there is no point in mixing together
// roughess for different resolutions or refresh rates.
if (windows_seen_ >= kMinWindowsBeforeSubmit) {
SubmitPlaybackRoughness();
} else {
worst_windows_.clear();
windows_seen_ = 0;
max_single_frame_error_ = base::TimeDelta();
}
} else {
ReportWindow(win);
}
// The frames in the window have been reported,
// no need to keep them around any longer.
items_to_discard = frames_window_size_;
} else if (static_cast<int>(frames_.size()) > max_buffer_size) {
// |frames_| grew too much, because apparently we're not getting consistent
// FramePresented() calls and no smoothness windows can be reported.
// Nevertheless, we can't allow |frames_| to grow too big, let's drop
// the oldest items beyond |max_buffer_size|;
items_to_discard = frames_.size() - max_buffer_size;
}
frames_.erase(frames_.begin(), frames_.begin() + items_to_discard);
}
void VideoPlaybackRoughnessReporter::Reset() {
if (windows_seen_ >= kMinWindowsBeforeSubmit)
SubmitPlaybackRoughness();
frames_.clear();
worst_windows_.clear();
windows_seen_ = 0;
max_single_frame_error_ = base::TimeDelta();
}
} // namespace cc