| // 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 |