| /* | |
| * Copyright 2019 The WebRTC Project Authors. All rights reserved. | |
| * | |
| * Use of this source code is governed by a BSD-style license | |
| * that can be found in the LICENSE file in the root of the source | |
| * tree. An additional intellectual property rights grant can be found | |
| * in the file PATENTS. All contributing project authors may | |
| * be found in the AUTHORS file in the root of the source tree. | |
| */ | |
| #include "api/task_queue/task_queue_test.h" | |
| #ifdef BUILD_EXPERIMENTAL_TASK_QUEUE_COROUTINE_TESTS | |
| #include <coroutine> | |
| #endif | |
| #include <cstdint> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #if defined(WEBRTC_WIN) | |
| #include <windows.h> | |
| #endif | |
| #include "absl/cleanup/cleanup.h" | |
| #ifdef BUILD_EXPERIMENTAL_TASK_QUEUE_COROUTINE_TESTS | |
| #include "absl/functional/any_invocable.h" | |
| #endif | |
| #include "absl/strings/string_view.h" | |
| #include "api/make_ref_counted.h" | |
| #include "api/ref_count.h" | |
| #if defined(WEBRTC_WIN) | |
| #include "api/task_queue/default_task_queue_factory.h" | |
| #endif | |
| #include "api/task_queue/task_queue_base.h" | |
| #include "api/task_queue/task_queue_factory.h" | |
| #include "api/units/time_delta.h" | |
| #include "rtc_base/event.h" | |
| #include "rtc_base/ref_counter.h" | |
| #include "rtc_base/time_utils.h" | |
| #include "test/gmock.h" | |
| #include "test/gtest.h" | |
| namespace webrtc { | |
| namespace { | |
| using ::testing::ElementsAre; | |
| // Avoids a dependency to system_wrappers. | |
| void SleepFor(TimeDelta duration) { | |
| ScopedAllowBaseSyncPrimitivesForTesting allow; | |
| Event event; | |
| event.Wait(duration); | |
| } | |
| std::unique_ptr<TaskQueueBase, TaskQueueDeleter> CreateTaskQueue( | |
| const std::unique_ptr<TaskQueueFactory>& factory, | |
| absl::string_view task_queue_name, | |
| TaskQueueFactory::Priority priority = TaskQueueFactory::Priority::kNormal) { | |
| return factory->CreateTaskQueue(task_queue_name, priority); | |
| } | |
| TEST_P(TaskQueueTest, Construct) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = CreateTaskQueue(factory, "Construct"); | |
| EXPECT_FALSE(queue->IsCurrent()); | |
| } | |
| TEST_P(TaskQueueTest, PostAndCheckCurrent) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto queue = CreateTaskQueue(factory, "PostAndCheckCurrent"); | |
| // We're not running a task, so `queue` shouldn't be current. | |
| // Note that because Thread also supports the TQ interface and | |
| // TestMainImpl::Init wraps the main test thread (bugs.webrtc.org/9714), that | |
| // means that TaskQueueBase::Current() will still return a valid value. | |
| EXPECT_FALSE(queue->IsCurrent()); | |
| queue->PostTask([&event, &queue] { | |
| EXPECT_TRUE(queue->IsCurrent()); | |
| event.Set(); | |
| }); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| // Verifies that a task can post a new task from within the task | |
| // and that the new one eventually runs. | |
| TEST_P(TaskQueueTest, TaskCanPostContinuationTask) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = CreateTaskQueue(factory, "TaskCanPostContinuationTask"); | |
| std::vector<std::string> events; | |
| Event done; | |
| queue->PostTask([&events, &done, queue = queue.get()] { | |
| events.push_back("Start"); | |
| queue->PostTask([&events, &done] { | |
| events.push_back("Continue"); | |
| done.Set(); | |
| }); | |
| events.push_back("FirstDone"); | |
| }); | |
| EXPECT_TRUE(done.Wait(TimeDelta::Seconds(1))); | |
| EXPECT_THAT(events, ElementsAre("Start", "FirstDone", "Continue")); | |
| } | |
| TEST_P(TaskQueueTest, PostCustomTask) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event ran; | |
| auto queue = CreateTaskQueue(factory, "PostCustomImplementation"); | |
| class CustomTask { | |
| public: | |
| explicit CustomTask(Event* ran) : ran_(ran) {} | |
| void operator()() { ran_->Set(); } | |
| private: | |
| Event* const ran_; | |
| } my_task(&ran); | |
| queue->PostTask(my_task); | |
| EXPECT_TRUE(ran.Wait(TimeDelta::Seconds(1))); | |
| } | |
| TEST_P(TaskQueueTest, PostDelayedZero) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto queue = CreateTaskQueue(factory, "PostDelayedZero"); | |
| queue->PostDelayedTask([&event] { event.Set(); }, TimeDelta::Zero()); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| TEST_P(TaskQueueTest, PostFromQueue) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto queue = CreateTaskQueue(factory, "PostFromQueue"); | |
| queue->PostTask( | |
| [&event, &queue] { queue->PostTask([&event] { event.Set(); }); }); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| TEST_P(TaskQueueTest, PostDelayed) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto queue = CreateTaskQueue(factory, "PostDelayed", | |
| TaskQueueFactory::Priority::kHigh); | |
| int64_t start = TimeMillis(); | |
| queue->PostDelayedTask( | |
| [&event, &queue] { | |
| EXPECT_TRUE(queue->IsCurrent()); | |
| event.Set(); | |
| }, | |
| TimeDelta::Millis(100)); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| int64_t end = TimeMillis(); | |
| // These tests are a little relaxed due to how "powerful" our test bots can | |
| // be. Most recently we've seen windows bots fire the callback after 94-99ms, | |
| // which is why we have a little bit of leeway backwards as well. | |
| EXPECT_GE(end - start, 90u); | |
| EXPECT_NEAR(end - start, 190u, 100u); // Accept 90-290. | |
| } | |
| TEST_P(TaskQueueTest, PostMultipleDelayed) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = CreateTaskQueue(factory, "PostMultipleDelayed"); | |
| std::vector<Event> events(100); | |
| for (int i = 0; i < 100; ++i) { | |
| Event* event = &events[i]; | |
| queue->PostDelayedTask( | |
| [event, &queue] { | |
| EXPECT_TRUE(queue->IsCurrent()); | |
| event->Set(); | |
| }, | |
| TimeDelta::Millis(i)); | |
| } | |
| for (Event& e : events) | |
| EXPECT_TRUE(e.Wait(TimeDelta::Seconds(1))); | |
| } | |
| TEST_P(TaskQueueTest, PostDelayedAfterDestruct) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event run; | |
| Event deleted; | |
| auto queue = CreateTaskQueue(factory, "PostDelayedAfterDestruct"); | |
| absl::Cleanup cleanup = [&deleted] { deleted.Set(); }; | |
| queue->PostDelayedTask([&run, cleanup = std::move(cleanup)] { run.Set(); }, | |
| TimeDelta::Millis(100)); | |
| // Destroy the queue. | |
| queue = nullptr; | |
| // Task might outlive the TaskQueue, but still should be deleted. | |
| EXPECT_TRUE(deleted.Wait(TimeDelta::Seconds(1))); | |
| EXPECT_FALSE(run.Wait(TimeDelta::Zero())); // and should not run. | |
| } | |
| TEST_P(TaskQueueTest, PostDelayedHighPrecisionAfterDestruct) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event run; | |
| Event deleted; | |
| auto queue = | |
| CreateTaskQueue(factory, "PostDelayedHighPrecisionAfterDestruct"); | |
| absl::Cleanup cleanup = [&deleted] { deleted.Set(); }; | |
| queue->PostDelayedHighPrecisionTask( | |
| [&run, cleanup = std::move(cleanup)] { run.Set(); }, | |
| TimeDelta::Millis(100)); | |
| // Destroy the queue. | |
| queue = nullptr; | |
| // Task might outlive the TaskQueue, but still should be deleted. | |
| EXPECT_TRUE(deleted.Wait(TimeDelta::Seconds(1))); | |
| EXPECT_FALSE(run.Wait(TimeDelta::Zero())); // and should not run. | |
| } | |
| TEST_P(TaskQueueTest, PostedUnexecutedClosureDestroyedOnTaskQueue) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = | |
| CreateTaskQueue(factory, "PostedUnexecutedClosureDestroyedOnTaskQueue"); | |
| TaskQueueBase* queue_ptr = queue.get(); | |
| queue->PostTask([] { SleepFor(TimeDelta::Millis(100)); }); | |
| // Give the task queue a chance to start executing the first lambda. | |
| SleepFor(TimeDelta::Millis(10)); | |
| Event finished; | |
| // Then ensure the next lambda (which is likely not executing yet) is | |
| // destroyed in the task queue context when the queue is deleted. | |
| auto cleanup = absl::Cleanup([queue_ptr, &finished] { | |
| EXPECT_EQ(queue_ptr, TaskQueueBase::Current()); | |
| finished.Set(); | |
| }); | |
| queue->PostTask([cleanup = std::move(cleanup)] {}); | |
| queue = nullptr; | |
| finished.Wait(TimeDelta::Seconds(1)); | |
| } | |
| TEST_P(TaskQueueTest, PostedClosureDestroyedOnTaskQueue) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = CreateTaskQueue(factory, "PostedClosureDestroyedOnTaskQueue"); | |
| TaskQueueBase* queue_ptr = queue.get(); | |
| Event finished; | |
| auto cleanup = absl::Cleanup([queue_ptr, &finished] { | |
| EXPECT_EQ(queue_ptr, TaskQueueBase::Current()); | |
| finished.Set(); | |
| }); | |
| // The cleanup task may or may not have had time to execute when the task | |
| // queue is destroyed. Regardless, the task should be destroyed on the | |
| // queue. | |
| queue->PostTask([cleanup = std::move(cleanup)] {}); | |
| queue = nullptr; | |
| finished.Wait(TimeDelta::Seconds(1)); | |
| } | |
| TEST_P(TaskQueueTest, PostedExecutedClosureDestroyedOnTaskQueue) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| auto queue = | |
| CreateTaskQueue(factory, "PostedExecutedClosureDestroyedOnTaskQueue"); | |
| TaskQueueBase* queue_ptr = queue.get(); | |
| // Ensure an executed lambda is destroyed on the task queue. | |
| Event finished; | |
| queue->PostTask([cleanup = absl::Cleanup([queue_ptr, &finished] { | |
| EXPECT_EQ(queue_ptr, TaskQueueBase::Current()); | |
| finished.Set(); | |
| })] {}); | |
| finished.Wait(TimeDelta::Seconds(1)); | |
| } | |
| TEST_P(TaskQueueTest, PostAndReuse) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto post_queue = CreateTaskQueue(factory, "PostQueue"); | |
| auto reply_queue = CreateTaskQueue(factory, "ReplyQueue"); | |
| int call_count = 0; | |
| class ReusedTask { | |
| public: | |
| ReusedTask(int* counter, TaskQueueBase* reply_queue, Event* event) | |
| : counter_(*counter), reply_queue_(reply_queue), event_(*event) { | |
| EXPECT_EQ(counter_, 0); | |
| } | |
| ReusedTask(ReusedTask&&) = default; | |
| ReusedTask& operator=(ReusedTask&&) = delete; | |
| void operator()() && { | |
| if (++counter_ == 1) { | |
| reply_queue_->PostTask(std::move(*this)); | |
| // At this point, the object is in the moved-from state. | |
| } else { | |
| EXPECT_EQ(counter_, 2); | |
| EXPECT_TRUE(reply_queue_->IsCurrent()); | |
| event_.Set(); | |
| } | |
| } | |
| private: | |
| int& counter_; | |
| TaskQueueBase* const reply_queue_; | |
| Event& event_; | |
| }; | |
| ReusedTask task(&call_count, reply_queue.get(), &event); | |
| post_queue->PostTask(std::move(task)); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| TEST_P(TaskQueueTest, PostALot) { | |
| // Waits until DecrementCount called `count` times. Thread safe. | |
| class BlockingCounter { | |
| public: | |
| explicit BlockingCounter(int initial_count) : count_(initial_count) {} | |
| void DecrementCount() { | |
| if (count_.DecRef() == RefCountReleaseStatus::kDroppedLastRef) { | |
| event_.Set(); | |
| } | |
| } | |
| bool Wait(TimeDelta give_up_after) { return event_.Wait(give_up_after); } | |
| private: | |
| webrtc_impl::RefCounter count_; | |
| Event event_; | |
| }; | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| static constexpr int kTaskCount = 0xffff; | |
| Event posting_done; | |
| BlockingCounter all_destroyed(kTaskCount); | |
| int tasks_executed = 0; | |
| auto task_queue = CreateTaskQueue(factory, "PostALot"); | |
| task_queue->PostTask([&] { | |
| // Post tasks from the queue to guarantee that the 1st task won't be | |
| // executed before the last one is posted. | |
| for (int i = 0; i < kTaskCount; ++i) { | |
| absl::Cleanup cleanup = [&] { all_destroyed.DecrementCount(); }; | |
| task_queue->PostTask([&tasks_executed, cleanup = std::move(cleanup)] { | |
| ++tasks_executed; | |
| }); | |
| } | |
| posting_done.Set(); | |
| }); | |
| // Before destroying the task queue wait until all child tasks are posted. | |
| posting_done.Wait(Event::kForever); | |
| // Destroy the task queue. | |
| task_queue = nullptr; | |
| // Expect all tasks are destroyed eventually. In some task queue | |
| // implementations that might happen on a different thread after task queue is | |
| // destroyed. | |
| EXPECT_TRUE(all_destroyed.Wait(TimeDelta::Minutes(1))); | |
| EXPECT_LE(tasks_executed, kTaskCount); | |
| } | |
| // Test posting two tasks that have shared state not protected by a | |
| // lock. The TaskQueue should guarantee memory read-write order and | |
| // FIFO task execution order, so the second task should always see the | |
| // changes that were made by the first task. | |
| // | |
| // If the TaskQueue doesn't properly synchronize the execution of | |
| // tasks, there will be a data race, which is undefined behavior. The | |
| // EXPECT calls may randomly catch this, but to make the most of this | |
| // unit test, run it under TSan or some other tool that is able to | |
| // directly detect data races. | |
| TEST_P(TaskQueueTest, PostTwoWithSharedUnprotectedState) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| struct SharedState { | |
| // First task will set this value to 1 and second will assert it. | |
| int state = 0; | |
| } state; | |
| auto queue = CreateTaskQueue(factory, "PostTwoWithSharedUnprotectedState"); | |
| Event done; | |
| queue->PostTask([&state, &queue, &done] { | |
| // Post tasks from queue to guarantee, that 1st task won't be | |
| // executed before the second one will be posted. | |
| queue->PostTask([&state] { state.state = 1; }); | |
| queue->PostTask([&state, &done] { | |
| EXPECT_EQ(state.state, 1); | |
| done.Set(); | |
| }); | |
| // Check, that state changing tasks didn't start yet. | |
| EXPECT_EQ(state.state, 0); | |
| }); | |
| EXPECT_TRUE(done.Wait(TimeDelta::Seconds(1))); | |
| } | |
| #if !defined(WEBRTC_CHROMIUM_BUILD) && defined(WEBRTC_WIN) | |
| void CALLBACK ApcProc(ULONG_PTR data) { | |
| reinterpret_cast<Event*>(data)->Set(); | |
| } | |
| // This works for TaskQueueWin, but not for the Thread backed | |
| // implementation or TaskQueueStdlib. | |
| // Change this to `TEST_P(TaskQueueTest, QueueUserAPC)` when all | |
| // implementations support this and use GetParam() instead of | |
| // CreateDefaultTaskQueueFactory(). | |
| TEST(TaskQueueTest, QueueUserAPC) { | |
| std::unique_ptr<TaskQueueFactory> factory = | |
| CreateDefaultTaskQueueFactory(nullptr); | |
| auto queue = CreateTaskQueue(factory, "ApcCompat"); | |
| Event done; | |
| queue->PostTask([&done] { | |
| QueueUserAPC(&ApcProc, GetCurrentThread(), | |
| reinterpret_cast<ULONG_PTR>(&done)); | |
| }); | |
| EXPECT_TRUE(done.Wait(TimeDelta::Seconds(1))); | |
| } | |
| #endif // !defined(WEBRTC_CHROMIUM_BUILD) && defined(WEBRTC_WIN) | |
| #ifdef BUILD_EXPERIMENTAL_TASK_QUEUE_COROUTINE_TESTS | |
| struct TaskQueueCoroutine { | |
| TaskQueueBase* const tq; | |
| bool await_ready() const noexcept { return tq->IsCurrent(); } | |
| void await_suspend(std::coroutine_handle<> h) const { | |
| tq->PostTask([h = std::move(h)]() { h.resume(); }); | |
| } | |
| void await_resume() const noexcept {} | |
| }; | |
| struct TaskQueueCoTask { | |
| struct promise_type { | |
| TaskQueueCoTask get_return_object() { return {}; } | |
| std::suspend_never initial_suspend() { return {}; } | |
| std::suspend_never final_suspend() noexcept { return {}; } | |
| void return_void() {} | |
| void unhandled_exception() { FAIL() << "exceptional!"; } | |
| }; | |
| }; | |
| // A test implementation of a coroutine on top of task queues. | |
| // This method is called on one TQ, original, switches over to the | |
| // `hop_to` task queue to do some work and then switches back to | |
| // the original task queue again and invoke the `done_cb` callback. | |
| TaskQueueCoTask HoppingCoroutine(absl::AnyInvocable<void() &&> done_cb, | |
| TaskQueueBase* hop_to) { | |
| TaskQueueBase* original = TaskQueueBase::Current(); | |
| // Switch to the `hop_to` task queue. | |
| co_await TaskQueueCoroutine{hop_to}; | |
| EXPECT_FALSE(original->IsCurrent()); | |
| EXPECT_TRUE(hop_to->IsCurrent()); | |
| // Switch back to the original task queue. | |
| co_await TaskQueueCoroutine{original}; | |
| EXPECT_TRUE(original->IsCurrent()); | |
| EXPECT_FALSE(hop_to->IsCurrent()); | |
| // Try switching again to the same, original, task queue. This tests not | |
| // continuing on without any switching. | |
| co_await TaskQueueCoroutine{original}; | |
| EXPECT_TRUE(original->IsCurrent()); | |
| EXPECT_FALSE(hop_to->IsCurrent()); | |
| std::move(done_cb)(); | |
| } | |
| // This coroutine pretends to be asynchronous, but is actually synchronous. The | |
| // function does incorporate `co_await` but since the async task is already | |
| // meant for the current TQ, `co_await` should actually execute immediately | |
| // without involving any asynchronous tasks. | |
| TaskQueueCoTask ActuallySynchronous(absl::AnyInvocable<void() &&> done_cb) { | |
| TaskQueueBase* current_tq = TaskQueueBase::Current(); | |
| co_await TaskQueueCoroutine{current_tq}; | |
| std::move(done_cb)(); | |
| } | |
| TEST_P(TaskQueueTest, Coroutine) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto queue1 = CreateTaskQueue(factory, "CoroutineTQ1"); | |
| auto queue2 = CreateTaskQueue(factory, "CoroutineTQ2"); | |
| queue1->PostTask([&event, &queue2] { | |
| struct Called { | |
| bool callback_called = false; | |
| }; | |
| auto called = make_ref_counted<Called>(); | |
| HoppingCoroutine( | |
| [&event, called]() { | |
| called->callback_called = true; | |
| event.Set(); | |
| }, | |
| queue2.get()); | |
| // The couroutine will execute a few async tasks before invoking the | |
| // callback. | |
| EXPECT_FALSE(called->callback_called); | |
| EXPECT_FALSE(called->HasOneRef()); | |
| }); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| // Test that when calling a coroutine that can perform work synchronously, does | |
| // so without posting a task. This is to test compatibility with existing | |
| // patterns where two conceptual task queues (or threads), are configured to | |
| // refer to the same underlying task queue. Typically this is done via calls to | |
| // Thread::BlockingCall() but also in proxy code (`pc/proxy.h`) where explicit | |
| // checks are done for what TQ is active and depending on that either invoke the | |
| // method directly or use a combination of PostTask()+Event::Wait(). This test | |
| // covers the synchronous aspect of that pattern whereas the `Coroutine` test | |
| // above covers the async part. | |
| TEST_P(TaskQueueTest, CoroutineSynchronous) { | |
| std::unique_ptr<TaskQueueFactory> factory = GetParam()(nullptr); | |
| Event event; | |
| auto q = CreateTaskQueue(factory, "TQ"); | |
| q->PostTask([&event] { | |
| bool done = false; | |
| // Don't use co_await, just call the function directly. This is to | |
| // make sure that if any async operations were actually queued then | |
| // we have not given them a chance to execute by using `co_wait`. | |
| // Since we know how `ActuallySynchronous` is implemented, we can | |
| // do that to verify that the full function ran without the | |
| // `co_wait` operation inside the coroutine, forking execution. | |
| ActuallySynchronous([&]() { done = true; }); | |
| // Even though `ActuallySynchronous` is a coroutine it is intentionally | |
| // implemented to switch to the current TQ, which should happen without any | |
| // async operations. | |
| EXPECT_TRUE(done); | |
| event.Set(); | |
| }); | |
| EXPECT_TRUE(event.Wait(TimeDelta::Seconds(1))); | |
| } | |
| #endif // BUILD_EXPERIMENTAL_TASK_QUEUE_COROUTINE_TESTS | |
| // TaskQueueTest is a set of tests for any implementation of the TaskQueueBase. | |
| // Tests are instantiated next to the concrete implementation(s). | |
| // https://github.com/google/googletest/blob/master/googletest/docs/advanced.md#creating-value-parameterized-abstract-tests | |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(TaskQueueTest); | |
| } // namespace | |
| } // namespace webrtc |