blob: a1a932943b8e4a2884b7ddf559b39a5c452eca2f [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 <map>
#include <memory>
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "media/base/media_switches.h"
#include "media/base/test_data_util.h"
#include "media/base/video_codecs.h"
#include "media/capabilities/video_decode_stats.pb.h"
#include "media/capabilities/video_decode_stats_db_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
using leveldb_proto::test::FakeDB;
using testing::_;
using testing::Eq;
using testing::Pointee;
namespace media {
class VideoDecodeStatsDBImplTest : public ::testing::Test {
public:
using VideoDescKey = VideoDecodeStatsDB::VideoDescKey;
using DecodeStatsEntry = VideoDecodeStatsDB::DecodeStatsEntry;
VideoDecodeStatsDBImplTest()
: kStatsKeyVp9(VideoDescKey::MakeBucketedKey(VP9PROFILE_PROFILE3,
gfx::Size(1024, 768),
60,
"com.widevine.alpha",
true)),
kStatsKeyAvc(VideoDescKey::MakeBucketedKey(H264PROFILE_MIN,
gfx::Size(1024, 768),
60,
"",
false)) {
bool parsed_time = base::Time::FromString(
VideoDecodeStatsDBImpl::kDefaultWriteTime, &kDefaultWriteTime);
DCHECK(parsed_time);
// Fake DB simply wraps a std::map with the LevelDB interface. We own the
// map and will delete it in TearDown().
fake_db_map_ = std::make_unique<FakeDB<DecodeStatsProto>::EntryMap>();
// |stats_db_| will own this pointer, but we hold a reference to control
// its behavior.
fake_db_ = new FakeDB<DecodeStatsProto>(fake_db_map_.get());
// Wrap the fake proto DB with our interface.
stats_db_ = base::WrapUnique(new VideoDecodeStatsDBImpl(
std::unique_ptr<FakeDB<DecodeStatsProto>>(fake_db_)));
}
VideoDecodeStatsDBImplTest(const VideoDecodeStatsDBImplTest&) = delete;
VideoDecodeStatsDBImplTest& operator=(const VideoDecodeStatsDBImplTest&) =
delete;
~VideoDecodeStatsDBImplTest() override {
// Tests should always complete any pending operations
VerifyNoPendingOps();
}
void VerifyOnePendingOp(std::string op_name) {
EXPECT_EQ(stats_db_->pending_ops_.size(), 1u);
VideoDecodeStatsDBImpl::PendingOperation* pending_op =
stats_db_->pending_ops_.begin()->second.get();
EXPECT_EQ(pending_op->uma_str_, op_name);
}
void VerifyNoPendingOps() { EXPECT_TRUE(stats_db_->pending_ops_.empty()); }
int GetMaxFramesPerBuffer() {
return VideoDecodeStatsDBImpl::GetMaxFramesPerBuffer();
}
int GetMaxDaysToKeepStats() {
return VideoDecodeStatsDBImpl::GetMaxDaysToKeepStats();
}
bool GetEnableUnweightedEntries() {
return VideoDecodeStatsDBImpl::GetEnableUnweightedEntries();
}
static base::FieldTrialParams GetFieldTrialParams() {
return VideoDecodeStatsDBImpl::GetFieldTrialParams();
}
void SetDBClock(base::Clock* clock) {
stats_db_->set_wall_clock_for_test(clock);
}
void InitializeDB() {
stats_db_->Initialize(base::BindOnce(
&VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
EXPECT_CALL(*this, OnInitialize(true));
fake_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
testing::Mock::VerifyAndClearExpectations(this);
}
void AppendStats(const VideoDescKey& key, const DecodeStatsEntry& entry) {
EXPECT_CALL(*this, MockAppendDecodeStatsCb(true));
stats_db_->AppendDecodeStats(
key, entry,
base::BindOnce(&VideoDecodeStatsDBImplTest::MockAppendDecodeStatsCb,
base::Unretained(this)));
VerifyOnePendingOp("Read");
fake_db_->GetCallback(true);
VerifyOnePendingOp("Write");
fake_db_->UpdateCallback(true);
testing::Mock::VerifyAndClearExpectations(this);
}
void VerifyReadStats(const VideoDescKey& key,
const DecodeStatsEntry& expected) {
EXPECT_CALL(*this, MockGetDecodeStatsCb(true, Pointee(Eq(expected))));
stats_db_->GetDecodeStats(
key, base::BindOnce(&VideoDecodeStatsDBImplTest::GetDecodeStatsCb,
base::Unretained(this)));
VerifyOnePendingOp("Read");
fake_db_->GetCallback(true);
testing::Mock::VerifyAndClearExpectations(this);
}
void VerifyEmptyStats(const VideoDescKey& key) {
EXPECT_CALL(*this, MockGetDecodeStatsCb(true, nullptr));
stats_db_->GetDecodeStats(
key, base::BindOnce(&VideoDecodeStatsDBImplTest::GetDecodeStatsCb,
base::Unretained(this)));
VerifyOnePendingOp("Read");
fake_db_->GetCallback(true);
testing::Mock::VerifyAndClearExpectations(this);
}
// Unwraps move-only parameters to pass to the mock function.
void GetDecodeStatsCb(bool success, std::unique_ptr<DecodeStatsEntry> entry) {
MockGetDecodeStatsCb(success, entry.get());
}
void AppendToProtoDB(const VideoDescKey& key,
const DecodeStatsProto* const proto) {
base::RunLoop run_loop;
base::OnceCallback<void(bool)> update_done_cb = base::BindOnce(
[](base::RunLoop* run_loop, bool success) {
ASSERT_TRUE(success);
run_loop->Quit();
},
Unretained(&run_loop));
using DBType = leveldb_proto::ProtoDatabase<DecodeStatsProto>;
std::unique_ptr<DBType::KeyEntryVector> entries =
std::make_unique<DBType::KeyEntryVector>();
entries->emplace_back(key.Serialize(), *proto);
fake_db_->UpdateEntries(std::move(entries),
std::make_unique<leveldb_proto::KeyVector>(),
std::move(update_done_cb));
fake_db_->UpdateCallback(true);
run_loop.Run();
}
MOCK_METHOD1(OnInitialize, void(bool success));
MOCK_METHOD2(MockGetDecodeStatsCb,
void(bool success, DecodeStatsEntry* entry));
MOCK_METHOD1(MockAppendDecodeStatsCb, void(bool success));
MOCK_METHOD0(MockClearStatsCb, void());
protected:
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
const VideoDescKey kStatsKeyVp9;
const VideoDescKey kStatsKeyAvc;
// Const in practice, but not marked const for compatibility with
// base::Time::FromString.
base::Time kDefaultWriteTime;
// See documentation in SetUp()
std::unique_ptr<FakeDB<DecodeStatsProto>::EntryMap> fake_db_map_;
FakeDB<DecodeStatsProto>* fake_db_;
std::unique_ptr<VideoDecodeStatsDBImpl> stats_db_;
};
TEST_F(VideoDecodeStatsDBImplTest, InitializeFailed) {
stats_db_->Initialize(base::BindOnce(
&VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
EXPECT_CALL(*this, OnInitialize(false));
fake_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kError);
}
TEST_F(VideoDecodeStatsDBImplTest, InitializeTimedOut) {
// Queue up an Initialize.
stats_db_->Initialize(base::BindOnce(
&VideoDecodeStatsDBImplTest::OnInitialize, base::Unretained(this)));
VerifyOnePendingOp("Initialize");
// Move time forward enough to trigger timeout.
EXPECT_CALL(*this, OnInitialize(_)).Times(0);
task_environment_.FastForwardBy(base::Seconds(100));
task_environment_.RunUntilIdle();
// Verify we didn't get an init callback and task is no longer considered
// pending (because it timed out).
testing::Mock::VerifyAndClearExpectations(this);
VerifyNoPendingOps();
// Verify callback still works if init completes very late.
EXPECT_CALL(*this, OnInitialize(false));
fake_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kError);
}
TEST_F(VideoDecodeStatsDBImplTest, ReadExpectingNothing) {
InitializeDB();
VerifyEmptyStats(kStatsKeyVp9);
}
TEST_F(VideoDecodeStatsDBImplTest, WriteReadAndClear) {
InitializeDB();
// Append and read back some VP9 stats.
DecodeStatsEntry entry(1000, 2, 10);
AppendStats(kStatsKeyVp9, entry);
VerifyReadStats(kStatsKeyVp9, entry);
// Reading with the wrong key (different codec) should still return nothing.
VerifyEmptyStats(kStatsKeyAvc);
// Appending same VP9 stats should read back as 2x the initial entry.
AppendStats(kStatsKeyVp9, entry);
DecodeStatsEntry aggregate_entry(2000, 4, 20);
VerifyReadStats(kStatsKeyVp9, aggregate_entry);
// Clear all stats from the DB.
EXPECT_CALL(*this, MockClearStatsCb);
stats_db_->ClearStats(base::BindOnce(
&VideoDecodeStatsDBImplTest::MockClearStatsCb, base::Unretained(this)));
VerifyOnePendingOp("Clear");
fake_db_->UpdateCallback(true);
// Database is now empty. Expect null entry.
VerifyEmptyStats(kStatsKeyVp9);
}
TEST_F(VideoDecodeStatsDBImplTest, ConfigureMaxFramesPerBuffer) {
// Setup field trial to use a tiny window of 1 decoded frame.
base::test::ScopedFeatureList scoped_feature_list;
std::unique_ptr<base::FieldTrialList> field_trial_list;
int previous_max_frames_per_buffer = GetMaxFramesPerBuffer();
int new_max_frames_per_buffer = 1;
ASSERT_LT(new_max_frames_per_buffer, previous_max_frames_per_buffer);
// Override field trial.
base::FieldTrialParams params;
params[VideoDecodeStatsDBImpl::kMaxFramesPerBufferParamName] =
base::NumberToString(new_max_frames_per_buffer);
scoped_feature_list.InitAndEnableFeatureWithParameters(
media::kMediaCapabilitiesWithParameters, params);
EXPECT_EQ(GetFieldTrialParams(), params);
EXPECT_EQ(new_max_frames_per_buffer, GetMaxFramesPerBuffer());
// Now verify the configured window is used by writing a frame and then
// writing another.
InitializeDB();
// Append single frame which was, sadly, dropped and not efficient.
DecodeStatsEntry entry(1, 1, 0);
AppendStats(kStatsKeyVp9, entry);
VerifyReadStats(kStatsKeyVp9, entry);
// Appending another frame which is *not* dropped and *is* efficient.
// Verify that only this last entry is still in the buffer (no aggregation).
DecodeStatsEntry second_entry(1, 0, 1);
AppendStats(kStatsKeyVp9, second_entry);
VerifyReadStats(kStatsKeyVp9, second_entry);
}
TEST_F(VideoDecodeStatsDBImplTest, ConfigureExpireDays) {
base::test::ScopedFeatureList scoped_feature_list;
std::unique_ptr<base::FieldTrialList> field_trial_list;
int previous_max_days_to_keep_stats = GetMaxDaysToKeepStats();
int new_max_days_to_keep_stats = 4;
ASSERT_LT(new_max_days_to_keep_stats, previous_max_days_to_keep_stats);
// Override field trial.
base::FieldTrialParams params;
params[VideoDecodeStatsDBImpl::kMaxDaysToKeepStatsParamName] =
base::NumberToString(new_max_days_to_keep_stats);
scoped_feature_list.InitAndEnableFeatureWithParameters(
media::kMediaCapabilitiesWithParameters, params);
EXPECT_EQ(GetFieldTrialParams(), params);
EXPECT_EQ(new_max_days_to_keep_stats, GetMaxDaysToKeepStats());
InitializeDB();
// Inject a test clock and initialize with the current time.
base::SimpleTestClock clock;
SetDBClock(&clock);
clock.SetNow(base::Time::Now());
// Append and verify read-back.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
// Some simple math to avoid troubles of integer division.
int half_days_to_keep_stats = new_max_days_to_keep_stats / 2;
int remaining_days_to_keep_stats =
new_max_days_to_keep_stats - half_days_to_keep_stats;
// Advance time half way through grace period. Verify stats not expired.
clock.Advance(base::Days(half_days_to_keep_stats));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
// Advance time 1 day beyond grace period, verify stats are expired.
clock.Advance(base::Days((remaining_days_to_keep_stats) + 1));
VerifyEmptyStats(kStatsKeyVp9);
// Advance the clock 100 extra days. Verify stats still expired.
clock.Advance(base::Days(100));
VerifyEmptyStats(kStatsKeyVp9);
}
TEST_F(VideoDecodeStatsDBImplTest, FailedWrite) {
InitializeDB();
// Expect the callback to indicate success = false when the write fails.
EXPECT_CALL(*this, MockAppendDecodeStatsCb(false));
// Append stats, but fail the internal DB update.
stats_db_->AppendDecodeStats(
kStatsKeyVp9, DecodeStatsEntry(1000, 2, 10),
base::BindOnce(&VideoDecodeStatsDBImplTest::MockAppendDecodeStatsCb,
base::Unretained(this)));
fake_db_->GetCallback(true);
fake_db_->UpdateCallback(false);
}
TEST_F(VideoDecodeStatsDBImplTest, FillBufferInMixedIncrements) {
InitializeDB();
// Setup DB entry that half fills the buffer with 10% of frames dropped and
// 50% of frames power efficient.
const int kNumFramesEntryA = GetMaxFramesPerBuffer() / 2;
DecodeStatsEntry entryA(kNumFramesEntryA, std::round(0.1 * kNumFramesEntryA),
std::round(0.5 * kNumFramesEntryA));
const double kDropRateA =
static_cast<double>(entryA.frames_dropped) / entryA.frames_decoded;
const double kEfficientRateA =
static_cast<double>(entryA.frames_power_efficient) /
entryA.frames_decoded;
// Append entryA to half fill the buffer and verify read. Verify read.
AppendStats(kStatsKeyVp9, entryA);
VerifyReadStats(kStatsKeyVp9, entryA);
// Append same entryA again to completely fill the buffer. Verify read gives
// out aggregated stats (2x the initial entryA)
AppendStats(kStatsKeyVp9, entryA);
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(GetMaxFramesPerBuffer(),
std::round(kDropRateA * GetMaxFramesPerBuffer()),
std::round(kEfficientRateA * GetMaxFramesPerBuffer())));
// This row in the DB is now "full" (appended frames >= kMaxFramesPerBuffer)!
//
// Future appends will not increase the total count of decoded frames. The
// ratios of dropped and power efficient frames will be a weighted average.
// The weight of new appends is determined by how much of the buffer they
// fill, i.e.
//
// new_append_weight = min(1, new_append_frame_count / kMaxFramesPerBuffer);
// old_data_weight = 1 - new_append_weight;
//
// The calculation for dropped ratios (same for power efficient) then becomes:
//
// aggregate_drop_ratio = old_drop_ratio * old_drop_weight +
// new_drop_ratio * new_data_weight;
//
// See implementation for more details.
// Append same entryA a 3rd time. Verify we still only get the aggregated
// stats from above (2x entryA) because the buffer is full and the ratio of
// dropped and efficient frames is the same.
AppendStats(kStatsKeyVp9, entryA);
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(GetMaxFramesPerBuffer(),
std::round(kDropRateA * GetMaxFramesPerBuffer()),
std::round(kEfficientRateA * GetMaxFramesPerBuffer())));
// Append entryB that will fill just 10% of the buffer. The new entry has
// different rates of dropped and power efficient frames to help verify that
// it is given proper weight as it mixes with existing data in the buffer.
const int kNumFramesEntryB = std::round(.1 * GetMaxFramesPerBuffer());
DecodeStatsEntry entryB(kNumFramesEntryB, std::round(0.25 * kNumFramesEntryB),
std::round(1 * kNumFramesEntryB));
const double kDropRateB =
static_cast<double>(entryB.frames_dropped) / entryB.frames_decoded;
const double kEfficientRateB =
static_cast<double>(entryB.frames_power_efficient) /
entryB.frames_decoded;
AppendStats(kStatsKeyVp9, entryB);
// Verify that buffer is still full, but dropped and power efficient frame
// rates are now higher according to entryB's impact (10%) on the full buffer.
double mixed_drop_rate = .1 * kDropRateB + .9 * kDropRateA;
double mixed_effiency_rate = .1 * kEfficientRateB + .9 * kEfficientRateA;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(
GetMaxFramesPerBuffer(),
std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));
// After appending entryB again, verify aggregate ratios behave according to
// the formula above (appending repeated entryB brings ratios closer to those
// in entryB, further from entryA).
AppendStats(kStatsKeyVp9, entryB);
mixed_drop_rate = .1 * kDropRateB + .9 * mixed_drop_rate;
mixed_effiency_rate = .1 * kEfficientRateB + .9 * mixed_effiency_rate;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(
GetMaxFramesPerBuffer(),
std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));
// Appending entry*A* again, verify aggregate ratios behave according to
// the formula above (ratio's move back in direction of those in entryA).
// Since entryA fills half the buffer it gets a higher weight than entryB did
// above.
AppendStats(kStatsKeyVp9, entryA);
mixed_drop_rate = .5 * kDropRateA + .5 * mixed_drop_rate;
mixed_effiency_rate = .5 * kEfficientRateA + .5 * mixed_effiency_rate;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(
GetMaxFramesPerBuffer(),
std::round(GetMaxFramesPerBuffer() * mixed_drop_rate),
std::round(GetMaxFramesPerBuffer() * mixed_effiency_rate)));
// Now append entryC with a frame count of 2x the buffer max. Verify entryC
// gets 100% of the weight, erasing the mixed stats from earlier appends.
const int kNumFramesEntryC = 2 * GetMaxFramesPerBuffer();
DecodeStatsEntry entryC(kNumFramesEntryC, std::round(0.3 * kNumFramesEntryC),
std::round(0.25 * kNumFramesEntryC));
const double kDropRateC =
static_cast<double>(entryC.frames_dropped) / entryC.frames_decoded;
const double kEfficientRateC =
static_cast<double>(entryC.frames_power_efficient) /
entryC.frames_decoded;
AppendStats(kStatsKeyVp9, entryC);
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(GetMaxFramesPerBuffer(),
std::round(GetMaxFramesPerBuffer() * kDropRateC),
std::round(GetMaxFramesPerBuffer() * kEfficientRateC)));
}
// Overfilling an empty buffer triggers the codepath to compute weighted dropped
// and power efficient ratios under a circumstance where the existing counts are
// all zero. This test ensures that we don't do any dividing by zero with that
// empty data.
TEST_F(VideoDecodeStatsDBImplTest, OverfillEmptyBuffer) {
InitializeDB();
// Setup DB entry that overflows the buffer max (by 1) with 10% of frames
// dropped and 50% of frames power efficient.
const int kNumFramesOverfill = GetMaxFramesPerBuffer() + 1;
DecodeStatsEntry entryA(kNumFramesOverfill,
std::round(0.1 * kNumFramesOverfill),
std::round(0.5 * kNumFramesOverfill));
// Append entry to completely fill the buffer and verify read.
AppendStats(kStatsKeyVp9, entryA);
// Read-back stats should have same ratios, but scaled such that
// frames_decoded = GetMaxFramesPerBuffer().
DecodeStatsEntry readBackEntryA(GetMaxFramesPerBuffer(),
std::round(0.1 * GetMaxFramesPerBuffer()),
std::round(0.5 * GetMaxFramesPerBuffer()));
VerifyReadStats(kStatsKeyVp9, readBackEntryA);
// Append another entry that again overfills with different dropped and power
// efficient ratios. Verify that read-back only reflects latest entry.
DecodeStatsEntry entryB(kNumFramesOverfill,
std::round(0.2 * kNumFramesOverfill),
std::round(0.6 * kNumFramesOverfill));
AppendStats(kStatsKeyVp9, entryB);
// Read-back stats should have same ratios, but scaled such that
// frames_decoded = GetMaxFramesPerBuffer().
DecodeStatsEntry readBackEntryB(GetMaxFramesPerBuffer(),
std::round(0.2 * GetMaxFramesPerBuffer()),
std::round(0.6 * GetMaxFramesPerBuffer()));
VerifyReadStats(kStatsKeyVp9, readBackEntryB);
}
TEST_F(VideoDecodeStatsDBImplTest, NoWriteDateReadAndExpire) {
InitializeDB();
// Seed the fake proto DB with an old-style entry lacking a write date. This
// will cause the DB to use kDefaultWriteTime.
DecodeStatsProto proto_lacking_date;
proto_lacking_date.set_frames_decoded(100);
proto_lacking_date.set_frames_dropped(10);
proto_lacking_date.set_frames_power_efficient(1);
fake_db_map_->emplace(kStatsKeyVp9.Serialize(), proto_lacking_date);
// Set "now" to be *before* the default write date. This will be the common
// case when the proto update (adding last_write_date) first ships (i.e. we
// don't want to immediately expire all the existing data).
base::SimpleTestClock clock;
SetDBClock(&clock);
clock.SetNow(kDefaultWriteTime - base::Days(10));
// Verify the stats are readable (not expired).
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));
// Set "now" to be in the middle of the grace period. Verify stats are still
// readable (not expired).
clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() / 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));
// Set the clock 1 day beyond the expiry date. Verify stats are no longer
// readable due to expiration.
clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() + 1));
VerifyEmptyStats(kStatsKeyVp9);
// Write some stats to the entry. Verify we get back exactly what's written
// without summing with the expired stats.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(50, 5, 0));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(50, 5, 0));
}
TEST_F(VideoDecodeStatsDBImplTest, NoWriteDateAppendReadAndExpire) {
InitializeDB();
// Seed the fake proto DB with an old-style entry lacking a write date. This
// will cause the DB to use kDefaultWriteTime.
DecodeStatsProto proto_lacking_date;
proto_lacking_date.set_frames_decoded(100);
proto_lacking_date.set_frames_dropped(10);
proto_lacking_date.set_frames_power_efficient(1);
fake_db_map_->emplace(kStatsKeyVp9.Serialize(), proto_lacking_date);
// Set "now" to be *before* the default write date. This will be the common
// case when the proto update (adding last_write_date) first ships (i.e. we
// don't want to immediately expire all the existing data).
base::SimpleTestClock clock;
SetDBClock(&clock);
clock.SetNow(kDefaultWriteTime - base::Days(10));
// Verify the stats are readable (not expired).
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(100, 10, 1));
// Append some stats and verify the aggregate math is correct. This will
// update the last_write_date to the current clock time.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(300, 30, 3));
// Set "now" to be in the middle of the grace period. Verify stats are still
// readable (not expired).
clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() / 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(300, 30, 3));
// Set the clock 1 day beyond the expiry date. Verify stats are no longer
// readable due to expiration.
clock.SetNow(kDefaultWriteTime + base::Days(GetMaxDaysToKeepStats() + 1));
VerifyEmptyStats(kStatsKeyVp9);
}
TEST_F(VideoDecodeStatsDBImplTest, AppendAndExpire) {
InitializeDB();
// Inject a test clock and initialize with the current time.
base::SimpleTestClock clock;
SetDBClock(&clock);
clock.SetNow(base::Time::Now());
// Append and verify read-back.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
// Advance time half way through grace period. Verify stats not expired.
clock.Advance(base::Days(GetMaxDaysToKeepStats() / 2));
VerifyReadStats(kStatsKeyVp9, DecodeStatsEntry(200, 20, 2));
// Advance time 1 day beyond grace period, verify stats are expired.
clock.Advance(base::Days((GetMaxDaysToKeepStats() / 2) + 1));
VerifyEmptyStats(kStatsKeyVp9);
// Advance the clock 100 days. Verify stats still expired.
clock.Advance(base::Days(100));
VerifyEmptyStats(kStatsKeyVp9);
}
TEST_F(VideoDecodeStatsDBImplTest, EnableUnweightedEntries) {
base::test::ScopedFeatureList scoped_feature_list;
std::unique_ptr<base::FieldTrialList> field_trial_list;
// Default is false.
EXPECT_FALSE(GetEnableUnweightedEntries());
// Override field trial.
base::FieldTrialParams params;
params[VideoDecodeStatsDBImpl::kEnableUnweightedEntriesParamName] = "true";
scoped_feature_list.InitAndEnableFeatureWithParameters(
media::kMediaCapabilitiesWithParameters, params);
EXPECT_EQ(GetFieldTrialParams(), params);
// Confirm field trial overridden.
EXPECT_TRUE(GetMaxDaysToKeepStats());
InitializeDB();
// Append 200 frames with 10% dropped, 1% efficient.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(200, 0.10 * 200, 0.01 * 200));
// Use real doubles to keep track of these things to make sure the precision
// math for repeating decimals works out with whats done internally.
int num_appends = 1;
double unweighted_smoothness_avg = 0.10;
double unweighted_efficiency_avg = 0.01;
// NOTE, the members of DecodeStatsEntry have a different meaning when using
// unweighted DB entries. The denominator is 100,000 * the number of appends
// and the numerator is whatever value achieves the correct unweighted ratio
// for those appends. See detailed comment in
// VideoDecodeStatsDBImpl::OnGotDecodeStats();
const int kNumAppendScale = 100000;
int expected_denominator = kNumAppendScale * num_appends;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(expected_denominator,
unweighted_smoothness_avg * expected_denominator,
unweighted_efficiency_avg * expected_denominator));
// Append 20K frames with 5% dropped and 10% efficient.
AppendStats(kStatsKeyVp9,
DecodeStatsEntry(20000, 0.05 * 20000, 0.10 * 20000));
num_appends++;
unweighted_smoothness_avg = (0.10 + 0.05) / num_appends;
unweighted_efficiency_avg = (0.01 + 0.10) / num_appends;
// While new record had 100x more frames than the previous append, the ratios
// should be an unweighted average of the two records (7.5% dropped and
// 5.5% efficient).
expected_denominator = kNumAppendScale * num_appends;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(expected_denominator,
unweighted_smoothness_avg * expected_denominator,
unweighted_efficiency_avg * expected_denominator));
// Append 1M frames with 3.4567% dropped and 3.4567% efficient.
AppendStats(kStatsKeyVp9, DecodeStatsEntry(1000000, 0.012345 * 1000000,
0.034567 * 1000000));
num_appends++;
unweighted_smoothness_avg = (0.10 + 0.05 + 0.012345) / num_appends;
unweighted_efficiency_avg = (0.01 + 0.10 + 0.034567) / num_appends;
// Here, the ratios should still be averaged in the unweighted fashion, but
// truncated after the 3rd decimal place of the percentage (e.g. 1.234%
// or the 5th decimal place when represented as a fraction of 1 (0.01234)).
expected_denominator = kNumAppendScale * num_appends;
VerifyReadStats(
kStatsKeyVp9,
DecodeStatsEntry(expected_denominator,
unweighted_smoothness_avg * expected_denominator,
unweighted_efficiency_avg * expected_denominator));
}
TEST_F(VideoDecodeStatsDBImplTest, DiscardCorruptedDBData) {
InitializeDB();
// Inject a test clock and initialize with the current time.
base::SimpleTestClock clock;
SetDBClock(&clock);
clock.SetNow(base::Time::Now());
// Construct several distinct key values for storing/retrieving the corrupted
// data. The details of the keys are not important.
const auto keyA = VideoDescKey::MakeBucketedKey(
VP9PROFILE_PROFILE0, gfx::Size(1024, 768), 60, "", false);
const auto keyB = VideoDescKey::MakeBucketedKey(
VP9PROFILE_PROFILE1, gfx::Size(1024, 768), 60, "", false);
const auto keyC = VideoDescKey::MakeBucketedKey(
VP9PROFILE_PROFILE2, gfx::Size(1024, 768), 60, "", false);
const auto keyD = VideoDescKey::MakeBucketedKey(
VP9PROFILE_PROFILE3, gfx::Size(1024, 768), 60, "", false);
const auto keyE = VideoDescKey::MakeBucketedKey(
H264PROFILE_BASELINE, gfx::Size(1024, 768), 60, "", false);
const auto keyF = VideoDescKey::MakeBucketedKey(
H264PROFILE_MAIN, gfx::Size(1024, 768), 60, "", false);
const auto keyG = VideoDescKey::MakeBucketedKey(
H264PROFILE_EXTENDED, gfx::Size(1024, 768), 60, "", false);
// Start with a proto that represents a valid uncorrupted and unexpired entry.
DecodeStatsProto protoA;
protoA.set_frames_decoded(100);
protoA.set_frames_dropped(15);
protoA.set_frames_power_efficient(50);
protoA.set_last_write_date(clock.Now().ToJsTime());
protoA.set_unweighted_average_frames_dropped(15.0 / 100);
protoA.set_unweighted_average_frames_efficient(50.0 / 100);
protoA.set_num_unweighted_playbacks(1);
// Append it and read it back without issue.
AppendToProtoDB(keyA, &protoA);
VerifyReadStats(keyA, DecodeStatsEntry(100, 15, 50));
// Make the valid proto invalid with more dropped frames than decoded. Verify
// you can't read it back (filtered for corruption).
DecodeStatsProto protoB(protoA);
protoB.set_frames_dropped(150);
AppendToProtoDB(keyB, &protoB);
VerifyEmptyStats(keyB);
// Make an invalid proto with more power efficient frames than decoded. Verify
// you can't read it back (filtered for corruption).
DecodeStatsProto protoC(protoA);
protoC.set_frames_power_efficient(150);
AppendToProtoDB(keyC, &protoC);
VerifyEmptyStats(keyC);
// Make an invalid proto with an unweighted average dropped ratio > 1.
DecodeStatsProto protoD(protoA);
protoD.set_unweighted_average_frames_dropped(2.0);
AppendToProtoDB(keyD, &protoD);
VerifyEmptyStats(keyD);
// Make an invalid proto with an unweighted average efficient ratio > 1.
DecodeStatsProto protoE(protoA);
protoE.set_unweighted_average_frames_efficient(2.0);
AppendToProtoDB(keyE, &protoE);
VerifyEmptyStats(keyE);
// Make an invalid proto with a negative last write date.
DecodeStatsProto protoF(protoA);
protoF.set_last_write_date(-1.0);
AppendToProtoDB(keyF, &protoF);
VerifyEmptyStats(keyF);
// Make an invalid proto with a last write date in the future.
DecodeStatsProto protoG(protoA);
protoG.set_last_write_date((clock.Now() + base::Days(1)).ToJsTime());
AppendToProtoDB(keyG, &protoG);
VerifyEmptyStats(keyG);
}
} // namespace media