| Index: content/renderer/media/audio_renderer_sink_cache_unittest.cc
|
| diff --git a/content/renderer/media/audio_renderer_sink_cache_unittest.cc b/content/renderer/media/audio_renderer_sink_cache_unittest.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..78eddecd8225db77904f485fab4e58a8debecdff
|
| --- /dev/null
|
| +++ b/content/renderer/media/audio_renderer_sink_cache_unittest.cc
|
| @@ -0,0 +1,372 @@
|
| +// Copyright 2016 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 <array>
|
| +
|
| +#include "base/bind.h"
|
| +#include "base/bind_helpers.h"
|
| +#include "base/logging.h"
|
| +#include "base/test/test_simple_task_runner.h"
|
| +#include "content/renderer/media/audio_renderer_sink_cache_impl.h"
|
| +#include "media/audio/audio_device_description.h"
|
| +#include "media/base/audio_parameters.h"
|
| +#include "media/base/mock_audio_renderer_sink.h"
|
| +#include "media/base/test_helpers.h"
|
| +#include "testing/gmock/include/gmock/gmock.h"
|
| +#include "testing/gtest/include/gtest/gtest.h"
|
| +#include "url/gurl.h"
|
| +
|
| +namespace content {
|
| +
|
| +namespace {
|
| +const char* const kDefaultDeviceId =
|
| + media::AudioDeviceDescription::kDefaultDeviceId;
|
| +const char kAnotherDeviceId[] = "another-device-id";
|
| +const int kRenderFrameId = 124;
|
| +const int kDeleteTimeoutMs = 1000;
|
| +} // namespace
|
| +
|
| +class AudioRendererSinkCacheTest : public testing::Test {
|
| + public:
|
| + AudioRendererSinkCacheTest()
|
| + : cache_(new AudioRendererSinkCacheImpl(
|
| + message_loop_.task_runner(),
|
| + base::Bind(&AudioRendererSinkCacheTest::CreateSink,
|
| + base::Unretained(this)),
|
| + base::TimeDelta::FromMilliseconds(kDeleteTimeoutMs))) {}
|
| +
|
| + void GetSink(int render_frame_id,
|
| + const std::string& device_id,
|
| + const url::Origin& security_origin,
|
| + media::AudioRendererSink** sink) {
|
| + *sink = cache_->GetSink(render_frame_id, device_id, security_origin).get();
|
| + }
|
| +
|
| + void GetRandomSinkInfo(int frame) {
|
| + // Get info and check if memory is not corrupted.
|
| + EXPECT_EQ(kDefaultDeviceId,
|
| + cache_->GetSinkInfo(frame, 0, kDefaultDeviceId, url::Origin())
|
| + .device_id());
|
| + }
|
| +
|
| + void GetRandomSink(int frame, base::TimeDelta sleep_timeout) {
|
| + scoped_refptr<media::AudioRendererSink> sink =
|
| + cache_->GetSink(frame, kDefaultDeviceId, url::Origin()).get();
|
| + ExpectToStop(sink.get());
|
| + base::PlatformThread::Sleep(sleep_timeout);
|
| + cache_->ReleaseSink(sink.get());
|
| + sink->Stop(); // Call a method to make the object is not corrupted.
|
| + }
|
| +
|
| + protected:
|
| + int sink_count() {
|
| + DCHECK(message_loop_.task_runner()->BelongsToCurrentThread());
|
| + return cache_->GetCacheSizeForTesting();
|
| + }
|
| +
|
| + scoped_refptr<media::AudioRendererSink> CreateSink(
|
| + int render_frame_id,
|
| + int session_id,
|
| + const std::string& device_id,
|
| + const url::Origin& security_origin) {
|
| + return new media::MockAudioRendererSink(device_id,
|
| + media::OUTPUT_DEVICE_STATUS_OK);
|
| + }
|
| +
|
| + void ExpectToStop(media::AudioRendererSink* sink) {
|
| + // Sink must be stoped before deletion.
|
| + EXPECT_CALL(*static_cast<media::MockAudioRendererSink*>(sink), Stop())
|
| + .Times(1);
|
| + }
|
| +
|
| + void ExpectNotToStop(media::AudioRendererSink* sink) {
|
| + // The sink must be stoped before deletion.
|
| + EXPECT_CALL(*static_cast<media::MockAudioRendererSink*>(sink), Stop())
|
| + .Times(0);
|
| + }
|
| +
|
| + // Posts the task to the specified thread and runs current message loop until
|
| + // the task is completed.
|
| + void PostAndRunUntilDone(const base::Thread& thread,
|
| + const base::Closure& task) {
|
| + media::WaitableMessageLoopEvent event;
|
| + thread.task_runner()->PostTaskAndReply(FROM_HERE, task, event.GetClosure());
|
| + // Runs the loop and waits for the thread to call event's closure.
|
| + event.RunAndWait();
|
| + }
|
| +
|
| + void WaitOnAnotherThread(const base::Thread& thread, int timeout_ms) {
|
| + PostAndRunUntilDone(
|
| + thread, base::Bind(base::IgnoreResult(&base::PlatformThread::Sleep),
|
| + base::TimeDelta::FromMilliseconds(timeout_ms)));
|
| + }
|
| +
|
| + base::MessageLoop message_loop_;
|
| + std::unique_ptr<AudioRendererSinkCacheImpl> cache_;
|
| +
|
| + private:
|
| + DISALLOW_COPY_AND_ASSIGN(AudioRendererSinkCacheTest);
|
| +};
|
| +
|
| +// Verify that normal get/release sink sequence works.
|
| +TEST_F(AudioRendererSinkCacheTest, GetReleaseSink) {
|
| + // Verify that a new sink is successfully created.
|
| + EXPECT_EQ(0, sink_count());
|
| + scoped_refptr<media::AudioRendererSink> sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + ExpectNotToStop(sink.get()); // Cache should not stop sinks marked as used.
|
| + EXPECT_EQ(kDefaultDeviceId, sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Verify that another sink with the same key is successfully created
|
| + scoped_refptr<media::AudioRendererSink> another_sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + ExpectNotToStop(another_sink.get());
|
| + EXPECT_EQ(kDefaultDeviceId, another_sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(2, sink_count());
|
| + EXPECT_NE(sink, another_sink);
|
| +
|
| + // Verify that another sink with a different kay is successfully created.
|
| + scoped_refptr<media::AudioRendererSink> yet_another_sink =
|
| + cache_->GetSink(kRenderFrameId, kAnotherDeviceId, url::Origin()).get();
|
| + ExpectNotToStop(yet_another_sink.get());
|
| + EXPECT_EQ(kAnotherDeviceId,
|
| + yet_another_sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(3, sink_count());
|
| + EXPECT_NE(sink, yet_another_sink);
|
| + EXPECT_NE(another_sink, yet_another_sink);
|
| +
|
| + // Verify that the first sink is successfully deleted.
|
| + cache_->ReleaseSink(sink.get());
|
| + EXPECT_EQ(2, sink_count());
|
| + sink = nullptr;
|
| +
|
| + // Make sure we deleted the right sink, and the memory for the rest is not
|
| + // corrupted.
|
| + EXPECT_EQ(kDefaultDeviceId, another_sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(kAnotherDeviceId,
|
| + yet_another_sink->GetOutputDeviceInfo().device_id());
|
| +
|
| + // Verify that the second sink is successfully deleted.
|
| + cache_->ReleaseSink(another_sink.get());
|
| + EXPECT_EQ(1, sink_count());
|
| + EXPECT_EQ(kAnotherDeviceId,
|
| + yet_another_sink->GetOutputDeviceInfo().device_id());
|
| +
|
| + cache_->ReleaseSink(yet_another_sink.get());
|
| + EXPECT_EQ(0, sink_count());
|
| +}
|
| +
|
| +// Verify that the sink created with GetSinkInfo() is reused when possible.
|
| +TEST_F(AudioRendererSinkCacheTest, GetDeviceInfo) {
|
| + EXPECT_EQ(0, sink_count());
|
| + media::OutputDeviceInfo device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // The info on the same device is requested, so no new sink is created.
|
| + media::OutputDeviceInfo one_more_device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count());
|
| + EXPECT_EQ(device_info.device_id(), one_more_device_info.device_id());
|
| +
|
| + // Aquire the sink that was created on GetSinkInfo().
|
| + scoped_refptr<media::AudioRendererSink> sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + EXPECT_EQ(1, sink_count());
|
| + EXPECT_EQ(device_info.device_id(), sink->GetOutputDeviceInfo().device_id());
|
| +
|
| + // Now the sink is in used, but we can still get the device info out of it, no
|
| + // new sink is created.
|
| + one_more_device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count());
|
| + EXPECT_EQ(device_info.device_id(), one_more_device_info.device_id());
|
| +
|
| + // Request sink for the same device. The first sink is in use, so a new one
|
| + // should be created.
|
| + scoped_refptr<media::AudioRendererSink> another_sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + EXPECT_EQ(2, sink_count());
|
| + EXPECT_EQ(device_info.device_id(),
|
| + another_sink->GetOutputDeviceInfo().device_id());
|
| +}
|
| +
|
| +// Verify that the sink created with GetSinkInfo() is deleted if unused.
|
| +// The test produces 2 "Uninteresting mock" warnings for
|
| +// MockAudioRendererSink::Stop().
|
| +TEST_F(AudioRendererSinkCacheTest, GarbageCollection) {
|
| + EXPECT_EQ(0, sink_count());
|
| + media::OutputDeviceInfo device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + media::OutputDeviceInfo another_device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kAnotherDeviceId, url::Origin());
|
| + EXPECT_EQ(2, sink_count());
|
| +
|
| + base::Thread thread("timeout_thread");
|
| + thread.Start();
|
| +
|
| + // 100 ms more than garbage collection timeout.
|
| + WaitOnAnotherThread(thread, kDeleteTimeoutMs + 100);
|
| +
|
| + // All the sinks should be garbage-collected by now.
|
| + EXPECT_EQ(0, sink_count());
|
| +}
|
| +
|
| +// Verify that the sink created with GetSinkInfo() is not deleted if used within
|
| +// the timeout.
|
| +TEST_F(AudioRendererSinkCacheTest, NoGarbageCollectionForUsedSink) {
|
| + EXPECT_EQ(0, sink_count());
|
| + media::OutputDeviceInfo device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + base::Thread thread("timeout_thread");
|
| + thread.Start();
|
| +
|
| + // Wait significantly less than grabage collection timeout.
|
| + int wait_a_bit = 100;
|
| + DCHECK_GT(kDeleteTimeoutMs, wait_a_bit * 2);
|
| + WaitOnAnotherThread(thread, wait_a_bit);
|
| +
|
| + // Sink is not deleted yet.
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Request it:
|
| + scoped_refptr<media::AudioRendererSink> sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + EXPECT_EQ(kDefaultDeviceId, sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Wait more to hit garbage collection timeout.
|
| + WaitOnAnotherThread(thread, kDeleteTimeoutMs);
|
| +
|
| + // The sink is still in place.
|
| + EXPECT_EQ(1, sink_count());
|
| +}
|
| +
|
| +// Verify that cache works fine if a sink scheduled for delettion is aquired and
|
| +// released before deletion timeout elapses.
|
| +// The test produces one "Uninteresting mock" warning for
|
| +// MockAudioRendererSink::Stop().
|
| +TEST_F(AudioRendererSinkCacheTest, ReleaseSinkBeforeScheduledDeletion) {
|
| + EXPECT_EQ(0, sink_count());
|
| +
|
| + media::OutputDeviceInfo device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kDefaultDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count()); // This sink is scheduled for deletion now.
|
| +
|
| + // Request it:
|
| + scoped_refptr<media::AudioRendererSink> sink =
|
| + cache_->GetSink(kRenderFrameId, kDefaultDeviceId, url::Origin()).get();
|
| + ExpectNotToStop(sink.get());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Release it:
|
| + cache_->ReleaseSink(sink.get());
|
| + EXPECT_EQ(0, sink_count());
|
| +
|
| + media::OutputDeviceInfo another_device_info =
|
| + cache_->GetSinkInfo(kRenderFrameId, 0, kAnotherDeviceId, url::Origin());
|
| + EXPECT_EQ(1, sink_count()); // This sink is scheduled for deletion now.
|
| +
|
| + base::Thread thread("timeout_thread");
|
| + thread.Start();
|
| +
|
| + // 100 ms more than garbage collection timeout.
|
| + WaitOnAnotherThread(thread, kDeleteTimeoutMs + 100);
|
| +
|
| + // Nothing crashed and the second sink deleted on schedule.
|
| + EXPECT_EQ(0, sink_count());
|
| +}
|
| +
|
| +// Check that a sink created on one thread in response to GetSinkInfo can be
|
| +// used on another thread.
|
| +TEST_F(AudioRendererSinkCacheTest, MultithreadedAccess) {
|
| + EXPECT_EQ(0, sink_count());
|
| +
|
| + base::Thread thread1("thread1");
|
| + thread1.Start();
|
| +
|
| + base::Thread thread2("thread2");
|
| + thread2.Start();
|
| +
|
| + // Request device information on the first thread.
|
| + PostAndRunUntilDone(
|
| + thread1,
|
| + base::Bind(base::IgnoreResult(&AudioRendererSinkCacheImpl::GetSinkInfo),
|
| + base::Unretained(cache_.get()), kRenderFrameId, 0,
|
| + kDefaultDeviceId, url::Origin()));
|
| +
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Request the sink on the second thread.
|
| + media::AudioRendererSink* sink;
|
| +
|
| + PostAndRunUntilDone(
|
| + thread2,
|
| + base::Bind(&AudioRendererSinkCacheTest::GetSink, base::Unretained(this),
|
| + kRenderFrameId, kDefaultDeviceId, url::Origin(), &sink));
|
| +
|
| + EXPECT_EQ(kDefaultDeviceId, sink->GetOutputDeviceInfo().device_id());
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Request device information on the first thread again.
|
| + PostAndRunUntilDone(
|
| + thread1,
|
| + base::Bind(base::IgnoreResult(&AudioRendererSinkCacheImpl::GetSinkInfo),
|
| + base::Unretained(cache_.get()), kRenderFrameId, 0,
|
| + kDefaultDeviceId, url::Origin()));
|
| + EXPECT_EQ(1, sink_count());
|
| +
|
| + // Release the sink on the second thread.
|
| + PostAndRunUntilDone(thread2,
|
| + base::Bind(&AudioRendererSinkCache::ReleaseSink,
|
| + base::Unretained(cache_.get()), sink));
|
| +
|
| + EXPECT_EQ(0, sink_count());
|
| +}
|
| +
|
| +// Intensive parallell access to the cache. Produces a ton of "Uninteresting
|
| +// mock" warnings for Stop() calls - this is fine.
|
| +TEST_F(AudioRendererSinkCacheTest, SmokeTest) {
|
| + const int kExperimentSize = 1000;
|
| + const int kSinkCount = 10;
|
| + const int kThreadCount = 3;
|
| +
|
| + // Sleep no more than (kDeleteTimeoutMs * 3) in total per thread.
|
| + const base::TimeDelta kSleepTimeout =
|
| + base::TimeDelta::FromMilliseconds(kDeleteTimeoutMs * 3 / kExperimentSize);
|
| +
|
| + srand(42); // Does not matter.
|
| +
|
| + std::array<std::unique_ptr<base::Thread>, kThreadCount> threads;
|
| + for (int i = 0; i < kThreadCount; ++i) {
|
| + threads[i].reset(new base::Thread(std::to_string(i)));
|
| + threads[i]->Start();
|
| + }
|
| +
|
| + for (int i = 0; i < kExperimentSize; ++i) {
|
| + for (auto& thread : threads) {
|
| + thread->task_runner()->PostTask(
|
| + FROM_HERE, base::Bind(&AudioRendererSinkCacheTest::GetRandomSinkInfo,
|
| + base::Unretained(this), rand() % kSinkCount));
|
| + thread->task_runner()->PostTask(
|
| + FROM_HERE, base::Bind(&AudioRendererSinkCacheTest::GetRandomSink,
|
| + base::Unretained(this), rand() % kSinkCount,
|
| + kSleepTimeout));
|
| + }
|
| + }
|
| +
|
| + // Wait for completion of all the tasks posted to at least one thread.
|
| + media::WaitableMessageLoopEvent loop_event;
|
| + threads[kThreadCount - 1]->task_runner()->PostTaskAndReply(
|
| + FROM_HERE, base::Bind(&base::DoNothing), loop_event.GetClosure());
|
| + // Runs the loop and waits for the thread to call event's closure.
|
| + loop_event.RunAndWait();
|
| +}
|
| +
|
| +} // namespace content
|
|
|