blob: 9a63156fa8c60926f4081c6867fd46ac24eeb516 [file] [log] [blame]
// Copyright 2017 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 "media/mojo/services/video_decode_perf_history.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/field_trial_params.h"
#include "base/strings/stringprintf.h"
#include "media/base/bind_to_current_loop.h"
#include "media/base/key_systems.h"
#include "media/base/media_switches.h"
#include "media/base/video_codecs.h"
#include "media/capabilities/learning_helper.h"
#include "media/mojo/mojom/media_types.mojom.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
namespace media {
namespace {
const double kMaxSmoothDroppedFramesPercentParamDefault = .05;
} // namespace
const char VideoDecodePerfHistory::kMaxSmoothDroppedFramesPercentParamName[] =
"smooth_threshold";
const char
VideoDecodePerfHistory::kEmeMaxSmoothDroppedFramesPercentParamName[] =
"eme_smooth_threshold";
// static
double VideoDecodePerfHistory::GetMaxSmoothDroppedFramesPercent(bool is_eme) {
double threshold = base::GetFieldTrialParamByFeatureAsDouble(
kMediaCapabilitiesWithParameters, kMaxSmoothDroppedFramesPercentParamName,
kMaxSmoothDroppedFramesPercentParamDefault);
// For EME, the precedence of overrides is:
// 1. EME specific override, |k*Eme*MaxSmoothDroppedFramesPercentParamName
// 2. Non-EME override, |kMaxSmoothDroppedFramesPercentParamName|
// 3. |kMaxSmoothDroppedFramesPercentParamDefault|
if (is_eme) {
threshold = base::GetFieldTrialParamByFeatureAsDouble(
kMediaCapabilitiesWithParameters,
kEmeMaxSmoothDroppedFramesPercentParamName, threshold);
}
return threshold;
}
VideoDecodePerfHistory::VideoDecodePerfHistory(
std::unique_ptr<VideoDecodeStatsDB> db,
learning::FeatureProviderFactoryCB feature_factory_cb)
: db_(std::move(db)),
db_init_status_(UNINITIALIZED),
feature_factory_cb_(std::move(feature_factory_cb)) {
DVLOG(2) << __func__;
DCHECK(db_);
// If the local learning experiment is enabled, then also create
// |learning_helper_| to send data to it.
if (base::FeatureList::IsEnabled(kMediaLearningExperiment))
learning_helper_ = std::make_unique<LearningHelper>(feature_factory_cb_);
}
VideoDecodePerfHistory::~VideoDecodePerfHistory() {
DVLOG(2) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void VideoDecodePerfHistory::BindReceiver(
mojo::PendingReceiver<mojom::VideoDecodePerfHistory> receiver) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
receivers_.Add(this, std::move(receiver));
}
void VideoDecodePerfHistory::InitDatabase() {
DVLOG(2) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_init_status_ == PENDING)
return;
// DB should be initialized only once! We hand out references to the
// initialized DB via GetVideoDecodeStatsDB(). Dependents expect DB to remain
// initialized during their lifetime.
DCHECK_EQ(db_init_status_, UNINITIALIZED);
db_->Initialize(base::BindOnce(&VideoDecodePerfHistory::OnDatabaseInit,
weak_ptr_factory_.GetWeakPtr()));
db_init_status_ = PENDING;
}
void VideoDecodePerfHistory::OnDatabaseInit(bool success) {
DVLOG(2) << __func__ << " " << success;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(db_init_status_, PENDING);
db_init_status_ = success ? COMPLETE : FAILED;
// Post all the deferred API calls as if they're just now coming in.
for (auto& deferred_call : init_deferred_api_calls_) {
base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE,
std::move(deferred_call));
}
init_deferred_api_calls_.clear();
}
void VideoDecodePerfHistory::GetPerfInfo(mojom::PredictionFeaturesPtr features,
GetPerfInfoCallback got_info_cb) {
DVLOG(3) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_NE(features->profile, VIDEO_CODEC_PROFILE_UNKNOWN);
DCHECK_GT(features->frames_per_sec, 0);
DCHECK(features->video_size.width() > 0 && features->video_size.height() > 0);
if (db_init_status_ == FAILED) {
// Optimistically claim perf is both smooth and power efficient.
std::move(got_info_cb).Run(true, true);
return;
}
// Defer this request until the DB is initialized.
if (db_init_status_ != COMPLETE) {
init_deferred_api_calls_.push_back(base::BindOnce(
&VideoDecodePerfHistory::GetPerfInfo, weak_ptr_factory_.GetWeakPtr(),
std::move(features), std::move(got_info_cb)));
InitDatabase();
return;
}
VideoDecodeStatsDB::VideoDescKey video_key =
VideoDecodeStatsDB::VideoDescKey::MakeBucketedKey(
features->profile, features->video_size, features->frames_per_sec,
features->key_system, features->use_hw_secure_codecs);
db_->GetDecodeStats(
video_key, base::BindOnce(&VideoDecodePerfHistory::OnGotStatsForRequest,
weak_ptr_factory_.GetWeakPtr(), video_key,
std::move(got_info_cb)));
}
void VideoDecodePerfHistory::AssessStats(
const VideoDecodeStatsDB::VideoDescKey& key,
const VideoDecodeStatsDB::DecodeStatsEntry* stats,
bool* is_smooth,
bool* is_power_efficient) {
// TODO(chcunningham/mlamouri): Refactor database API to give us nearby
// stats whenever we don't have a perfect match. If higher
// resolutions/frame rates are known to be smooth, we can report this as
/// smooth. If lower resolutions/frames are known to be janky, we can assume
// this will be janky.
// No stats? Lets be optimistic.
if (!stats || stats->frames_decoded == 0) {
*is_power_efficient = true;
*is_smooth = true;
return;
}
double percent_dropped =
static_cast<double>(stats->frames_dropped) / stats->frames_decoded;
double percent_power_efficient =
static_cast<double>(stats->frames_power_efficient) /
stats->frames_decoded;
*is_power_efficient =
percent_power_efficient >= kMinPowerEfficientDecodedFramePercent;
*is_smooth = percent_dropped <=
GetMaxSmoothDroppedFramesPercent(!key.key_system.empty());
}
void VideoDecodePerfHistory::OnGotStatsForRequest(
const VideoDecodeStatsDB::VideoDescKey& video_key,
GetPerfInfoCallback got_info_cb,
bool database_success,
std::unique_ptr<VideoDecodeStatsDB::DecodeStatsEntry> stats) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(got_info_cb);
DCHECK_EQ(db_init_status_, COMPLETE);
bool is_power_efficient = false;
bool is_smooth = false;
double percent_dropped = 0;
double percent_power_efficient = 0;
AssessStats(video_key, stats.get(), &is_smooth, &is_power_efficient);
if (stats && stats->frames_decoded) {
DCHECK(database_success);
percent_dropped =
static_cast<double>(stats->frames_dropped) / stats->frames_decoded;
percent_power_efficient =
static_cast<double>(stats->frames_power_efficient) /
stats->frames_decoded;
}
DVLOG(3) << __func__
<< base::StringPrintf(
" profile:%s size:%s fps:%d --> ",
GetProfileName(video_key.codec_profile).c_str(),
video_key.size.ToString().c_str(), video_key.frame_rate)
<< (stats.get()
? base::StringPrintf(
"smooth:%d frames_decoded:%" PRIu64 " pcnt_dropped:%f"
" pcnt_power_efficent:%f",
is_smooth, stats->frames_decoded, percent_dropped,
percent_power_efficient)
: (database_success ? "no info" : "query FAILED"));
std::move(got_info_cb).Run(is_smooth, is_power_efficient);
}
VideoDecodePerfHistory::SaveCallback VideoDecodePerfHistory::GetSaveCallback() {
return base::BindRepeating(&VideoDecodePerfHistory::SavePerfRecord,
weak_ptr_factory_.GetWeakPtr());
}
void VideoDecodePerfHistory::SavePerfRecord(ukm::SourceId source_id,
learning::FeatureValue origin,
bool is_top_frame,
mojom::PredictionFeatures features,
mojom::PredictionTargets targets,
uint64_t player_id,
base::OnceClosure save_done_cb) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DVLOG(3)
<< __func__
<< base::StringPrintf(
" profile:%s size:%s fps:%d decoded:%d dropped:%d efficient:%d",
GetProfileName(features.profile).c_str(),
features.video_size.ToString().c_str(), features.frames_per_sec,
targets.frames_decoded, targets.frames_dropped,
targets.frames_power_efficient);
if (db_init_status_ == FAILED) {
DVLOG(3) << __func__ << " Can't save stats. No DB!";
return;
}
// Defer this request until the DB is initialized.
if (db_init_status_ != COMPLETE) {
init_deferred_api_calls_.push_back(base::BindOnce(
&VideoDecodePerfHistory::SavePerfRecord, weak_ptr_factory_.GetWeakPtr(),
source_id, origin, is_top_frame, std::move(features),
std::move(targets), player_id, std::move(save_done_cb)));
InitDatabase();
return;
}
VideoDecodeStatsDB::VideoDescKey video_key =
VideoDecodeStatsDB::VideoDescKey::MakeBucketedKey(
features.profile, features.video_size, features.frames_per_sec,
features.key_system, features.use_hw_secure_codecs);
VideoDecodeStatsDB::DecodeStatsEntry new_stats(
targets.frames_decoded, targets.frames_dropped,
targets.frames_power_efficient);
if (learning_helper_)
learning_helper_->AppendStats(video_key, origin, new_stats);
// Get past perf info and report UKM metrics before saving this record.
db_->GetDecodeStats(
video_key,
base::BindOnce(&VideoDecodePerfHistory::OnGotStatsForSave,
weak_ptr_factory_.GetWeakPtr(), source_id, is_top_frame,
player_id, video_key, new_stats, std::move(save_done_cb)));
}
void VideoDecodePerfHistory::OnGotStatsForSave(
ukm::SourceId source_id,
bool is_top_frame,
uint64_t player_id,
const VideoDecodeStatsDB::VideoDescKey& video_key,
const VideoDecodeStatsDB::DecodeStatsEntry& new_stats,
base::OnceClosure save_done_cb,
bool success,
std::unique_ptr<VideoDecodeStatsDB::DecodeStatsEntry> past_stats) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(db_init_status_, COMPLETE);
if (!success) {
DVLOG(3) << __func__ << " FAILED! Aborting save.";
if (save_done_cb)
std::move(save_done_cb).Run();
return;
}
ReportUkmMetrics(source_id, is_top_frame, player_id, video_key, new_stats,
past_stats.get());
db_->AppendDecodeStats(
video_key, new_stats,
base::BindOnce(&VideoDecodePerfHistory::OnSaveDone,
weak_ptr_factory_.GetWeakPtr(), std::move(save_done_cb)));
}
void VideoDecodePerfHistory::OnSaveDone(base::OnceClosure save_done_cb,
bool success) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// TODO(chcunningham): Monitor UMA. Experiment with re-initializing DB to
// remedy IO failures.
DVLOG(3) << __func__ << (success ? " succeeded" : " FAILED!");
// Don't bother to bubble success. Its not actionable for upper layers. Also,
// save_done_cb only used for test sequencing, where DB should always behave
// (or fail the test).
if (save_done_cb)
std::move(save_done_cb).Run();
}
void VideoDecodePerfHistory::ReportUkmMetrics(
ukm::SourceId source_id,
bool is_top_frame,
uint64_t player_id,
const VideoDecodeStatsDB::VideoDescKey& video_key,
const VideoDecodeStatsDB::DecodeStatsEntry& new_stats,
VideoDecodeStatsDB::DecodeStatsEntry* past_stats) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// UKM may be unavailable in content_shell or other non-chrome/ builds; it
// may also be unavailable if browser shutdown has started; so this may be a
// nullptr. If it's unavailable, UKM reporting will be skipped.
ukm::UkmRecorder* ukm_recorder = ukm::UkmRecorder::Get();
if (!ukm_recorder)
return;
ukm::builders::Media_VideoDecodePerfRecord builder(source_id);
builder.SetVideo_InTopFrame(is_top_frame);
builder.SetVideo_PlayerID(player_id);
builder.SetVideo_CodecProfile(video_key.codec_profile);
builder.SetVideo_FramesPerSecond(video_key.frame_rate);
builder.SetVideo_NaturalHeight(video_key.size.height());
builder.SetVideo_NaturalWidth(video_key.size.width());
if (!video_key.key_system.empty()) {
builder.SetVideo_EME_KeySystem(GetKeySystemIntForUKM(video_key.key_system));
builder.SetVideo_EME_UseHwSecureCodecs(video_key.use_hw_secure_codecs);
}
bool past_is_smooth = false;
bool past_is_efficient = false;
AssessStats(video_key, past_stats, &past_is_smooth, &past_is_efficient);
builder.SetPerf_ApiWouldClaimIsSmooth(past_is_smooth);
builder.SetPerf_ApiWouldClaimIsPowerEfficient(past_is_efficient);
if (past_stats) {
builder.SetPerf_PastVideoFramesDecoded(past_stats->frames_decoded);
builder.SetPerf_PastVideoFramesDropped(past_stats->frames_dropped);
builder.SetPerf_PastVideoFramesPowerEfficient(
past_stats->frames_power_efficient);
} else {
builder.SetPerf_PastVideoFramesDecoded(0);
builder.SetPerf_PastVideoFramesDropped(0);
builder.SetPerf_PastVideoFramesPowerEfficient(0);
}
bool new_is_smooth = false;
bool new_is_efficient = false;
AssessStats(video_key, &new_stats, &new_is_smooth, &new_is_efficient);
builder.SetPerf_RecordIsSmooth(new_is_smooth);
builder.SetPerf_RecordIsPowerEfficient(new_is_efficient);
builder.SetPerf_VideoFramesDecoded(new_stats.frames_decoded);
builder.SetPerf_VideoFramesDropped(new_stats.frames_dropped);
builder.SetPerf_VideoFramesPowerEfficient(new_stats.frames_power_efficient);
builder.Record(ukm_recorder);
}
void VideoDecodePerfHistory::ClearHistory(base::OnceClosure clear_done_cb) {
DVLOG(2) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If we have a learning helper, then replace it. This will erase any data
// that it currently has.
if (learning_helper_)
learning_helper_ = std::make_unique<LearningHelper>(feature_factory_cb_);
if (db_init_status_ == FAILED) {
DVLOG(3) << __func__ << " Can't clear history - No DB!";
std::move(clear_done_cb).Run();
return;
}
// Defer this request until the DB is initialized.
if (db_init_status_ != COMPLETE) {
init_deferred_api_calls_.push_back(base::BindOnce(
&VideoDecodePerfHistory::ClearHistory, weak_ptr_factory_.GetWeakPtr(),
std::move(clear_done_cb)));
InitDatabase();
return;
}
db_->ClearStats(base::BindOnce(&VideoDecodePerfHistory::OnClearedHistory,
weak_ptr_factory_.GetWeakPtr(),
std::move(clear_done_cb)));
}
void VideoDecodePerfHistory::OnClearedHistory(base::OnceClosure clear_done_cb) {
DVLOG(2) << __func__;
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::move(clear_done_cb).Run();
}
void VideoDecodePerfHistory::GetVideoDecodeStatsDB(GetCB get_db_cb) {
DVLOG(3) << __func__;
DCHECK(get_db_cb);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_init_status_ == FAILED) {
std::move(get_db_cb).Run(nullptr);
return;
}
// Defer this request until the DB is initialized.
if (db_init_status_ != COMPLETE) {
init_deferred_api_calls_.push_back(
base::BindOnce(&VideoDecodePerfHistory::GetVideoDecodeStatsDB,
weak_ptr_factory_.GetWeakPtr(), std::move(get_db_cb)));
InitDatabase();
return;
}
// DB is already initialized. BindToCurrentLoop to avoid reentrancy.
std::move(BindToCurrentLoop(std::move(get_db_cb))).Run(db_.get());
}
} // namespace media