diff --git a/base/BUILD.gn b/base/BUILD.gn index 1cbc063dae9fbb..0ac9233dbb8477 100644 --- a/base/BUILD.gn +++ b/base/BUILD.gn @@ -766,6 +766,8 @@ component("base") { "task_runner.cc", "task_runner.h", "task_runner_util.h", + "task_scheduler/priority_queue.cc", + "task_scheduler/priority_queue.h", "task_scheduler/scheduler_lock.h", "task_scheduler/scheduler_lock_impl.cc", "task_scheduler/scheduler_lock_impl.h", @@ -1815,9 +1817,11 @@ test("base_unittests") { "system_monitor/system_monitor_unittest.cc", "task/cancelable_task_tracker_unittest.cc", "task_runner_util_unittest.cc", + "task_scheduler/priority_queue_unittest.cc", "task_scheduler/scheduler_lock_unittest.cc", "task_scheduler/sequence_sort_key_unittest.cc", "task_scheduler/sequence_unittest.cc", + "task_scheduler/test_utils.h", "template_util_unittest.cc", "test/histogram_tester_unittest.cc", "test/icu_test_util.cc", diff --git a/base/base.gyp b/base/base.gyp index d7f3519de14c0e..25918035bcba6d 100644 --- a/base/base.gyp +++ b/base/base.gyp @@ -549,9 +549,11 @@ 'system_monitor/system_monitor_unittest.cc', 'task/cancelable_task_tracker_unittest.cc', 'task_runner_util_unittest.cc', + 'task_scheduler/priority_queue_unittest.cc', 'task_scheduler/scheduler_lock_unittest.cc', 'task_scheduler/sequence_sort_key_unittest.cc', 'task_scheduler/sequence_unittest.cc', + 'task_scheduler/test_utils.h', 'template_util_unittest.cc', 'test/histogram_tester_unittest.cc', 'test/test_pending_task_unittest.cc', diff --git a/base/base.gypi b/base/base.gypi index 5d7693f8ba1cbf..dac46e236fc868 100644 --- a/base/base.gypi +++ b/base/base.gypi @@ -636,6 +636,8 @@ 'task_runner.cc', 'task_runner.h', 'task_runner_util.h', + 'task_scheduler/priority_queue.cc', + 'task_scheduler/priority_queue.h', 'task_scheduler/scheduler_lock.h', 'task_scheduler/scheduler_lock_impl.cc', 'task_scheduler/scheduler_lock_impl.h', diff --git a/base/task_scheduler/priority_queue.cc b/base/task_scheduler/priority_queue.cc new file mode 100644 index 00000000000000..8eb4b862642b3f --- /dev/null +++ b/base/task_scheduler/priority_queue.cc @@ -0,0 +1,89 @@ +// 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 "base/task_scheduler/priority_queue.h" + +#include + +#include "base/logging.h" + +namespace base { +namespace internal { + +PriorityQueue::SequenceAndSortKey::SequenceAndSortKey() + : sort_key(TaskPriority::LOWEST, TimeTicks()) {} + +PriorityQueue::SequenceAndSortKey::SequenceAndSortKey( + scoped_refptr sequence, + const SequenceSortKey& sort_key) + : sequence(std::move(sequence)), sort_key(sort_key) {} + +PriorityQueue::SequenceAndSortKey::~SequenceAndSortKey() = default; + +PriorityQueue::Transaction::Transaction(PriorityQueue* outer_queue) + : auto_lock_(new AutoSchedulerLock(outer_queue->container_lock_)), + outer_queue_(outer_queue) { + DCHECK(CalledOnValidThread()); +} + +PriorityQueue::Transaction::~Transaction() { + DCHECK(CalledOnValidThread()); + + // Run the sequence insertion callback once for each Sequence that was + // inserted in the PriorityQueue during the lifetime of this Transaction. + // Perform this outside the scope of PriorityQueue's lock to avoid imposing an + // unnecessary lock dependency on |sequence_inserted_callback_|'s destination. + auto_lock_.reset(); + for (size_t i = 0; i < num_pushed_sequences_; ++i) + outer_queue_->sequence_inserted_callback_.Run(); +} + +void PriorityQueue::Transaction::Push( + scoped_ptr sequence_and_sort_key) { + DCHECK(CalledOnValidThread()); + DCHECK(!sequence_and_sort_key->is_null()); + + outer_queue_->container_.push(std::move(sequence_and_sort_key)); + ++num_pushed_sequences_; +} + +const PriorityQueue::SequenceAndSortKey& PriorityQueue::Transaction::Peek() + const { + DCHECK(CalledOnValidThread()); + + // TODO(fdoray): Add an IsEmpty() method to Transaction and require Peek() to + // be called on a non-empty PriorityQueue only. + if (outer_queue_->container_.empty()) + return outer_queue_->empty_sequence_and_sort_key_; + + return *outer_queue_->container_.top(); +} + +void PriorityQueue::Transaction::Pop() { + DCHECK(CalledOnValidThread()); + DCHECK(!outer_queue_->container_.empty()); + outer_queue_->container_.pop(); +} + +PriorityQueue::PriorityQueue(const Closure& sequence_inserted_callback) + : sequence_inserted_callback_(sequence_inserted_callback) { + DCHECK(!sequence_inserted_callback_.is_null()); +} + +PriorityQueue::PriorityQueue(const Closure& sequence_inserted_callback, + const PriorityQueue* predecessor_priority_queue) + : container_lock_(&predecessor_priority_queue->container_lock_), + sequence_inserted_callback_(sequence_inserted_callback) { + DCHECK(!sequence_inserted_callback_.is_null()); + DCHECK(predecessor_priority_queue); +} + +PriorityQueue::~PriorityQueue() = default; + +scoped_ptr PriorityQueue::BeginTransaction() { + return make_scoped_ptr(new Transaction(this)); +} + +} // namespace internal +} // namespace base diff --git a/base/task_scheduler/priority_queue.h b/base/task_scheduler/priority_queue.h new file mode 100644 index 00000000000000..e37dae92f33cbc --- /dev/null +++ b/base/task_scheduler/priority_queue.h @@ -0,0 +1,139 @@ +// 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. + +#ifndef BASE_TASK_SCHEDULER_PRIORITY_QUEUE_H_ +#define BASE_TASK_SCHEDULER_PRIORITY_QUEUE_H_ + +#include +#include + +#include "base/base_export.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/task_scheduler/scheduler_lock.h" +#include "base/task_scheduler/sequence.h" +#include "base/task_scheduler/sequence_sort_key.h" +#include "base/threading/non_thread_safe.h" + +namespace base { +namespace internal { + +// A PriorityQueue holds Sequences of Tasks. This class is thread-safe. +class BASE_EXPORT PriorityQueue { + public: + // An immutable struct combining a Sequence and the sort key that determines + // its position in a PriorityQueue. + struct BASE_EXPORT SequenceAndSortKey { + // Constructs a null SequenceAndSortKey. + SequenceAndSortKey(); + + // Constructs a SequenceAndSortKey with the given |sequence| and |sort_key|. + SequenceAndSortKey(scoped_refptr sequence, + const SequenceSortKey& sort_key); + + ~SequenceAndSortKey(); + + // Returns true if this is a null SequenceAndSortKey. + bool is_null() const { return !sequence; } + + const scoped_refptr sequence; + const SequenceSortKey sort_key; + }; + + // A Transaction can perform multiple operations atomically on a + // PriorityQueue. While a Transaction is alive, it is guaranteed that nothing + // else will access the PriorityQueue. + // + // A WorkerThread needs to be able to Peek sequences from both its + // PriorityQueues (single-threaded and shared) and then Pop the sequence with + // the highest priority. If the Peek and the Pop are done through the same + // Transaction, it is guaranteed that the PriorityQueue hasn't changed between + // the 2 operations. + class BASE_EXPORT Transaction : public NonThreadSafe { + public: + ~Transaction(); + + // Inserts |sequence_and_sort_key| in the PriorityQueue. + void Push(scoped_ptr sequence_and_sort_key); + + // Returns the SequenceAndSortKey with the highest priority or a null + // SequenceAndSortKey if the PriorityQueue is empty. The reference becomes + // invalid the next time that a Sequence is popped from the PriorityQueue. + const SequenceAndSortKey& Peek() const; + + // Removes the SequenceAndSortKey with the highest priority from the + // PriorityQueue. Cannot be called on an empty PriorityQueue. + void Pop(); + + private: + friend class PriorityQueue; + + explicit Transaction(PriorityQueue* outer_queue); + + // Holds the lock of |outer_queue_| for most of the lifetime of this + // Transaction. Using a scoped_ptr allows the destructor to release the lock + // before performing internal operations which have to be done outside of + // its scope. + scoped_ptr auto_lock_; + + PriorityQueue* const outer_queue_; + + // Number of times that Push() has been called on this Transaction. + size_t num_pushed_sequences_ = 0; + + DISALLOW_COPY_AND_ASSIGN(Transaction); + }; + + // |sequence_inserted_callback| is a non-null callback invoked when the + // Transaction is done for each Push that was performed with the Transaction. + explicit PriorityQueue(const Closure& sequence_inserted_callback); + + // |sequence_inserted_callback| is a non-null callback invoked when the + // Transaction is done for each Push that was performed with the Transaction. + // |predecessor_priority_queue| is a PriorityQueue for which a thread is + // allowed to have an active Transaction when it creates a Transaction for + // this PriorityQueue. + PriorityQueue(const Closure& sequence_inserted_callback, + const PriorityQueue* predecessor_priority_queue); + + ~PriorityQueue(); + + // Begins a Transaction. This method cannot be called on a thread which has an + // active Transaction unless the last Transaction created on the thread was + // for the allowed predecessor specified in the constructor of this + // PriorityQueue. + scoped_ptr BeginTransaction(); + + private: + struct SequenceAndSortKeyComparator { + bool operator()(const scoped_ptr& left, + const scoped_ptr& right) const { + return left->sort_key < right->sort_key; + } + }; + using ContainerType = + std::priority_queue, + std::vector>, + SequenceAndSortKeyComparator>; + + // Synchronizes access to |container_|. + SchedulerLock container_lock_; + + ContainerType container_; + + const Closure sequence_inserted_callback_; + + // A null SequenceAndSortKey returned by Peek() when the PriorityQueue is + // empty. + const SequenceAndSortKey empty_sequence_and_sort_key_; + + DISALLOW_COPY_AND_ASSIGN(PriorityQueue); +}; + +} // namespace internal +} // namespace base + +#endif // BASE_TASK_SCHEDULER_PRIORITY_QUEUE_H_ diff --git a/base/task_scheduler/priority_queue_unittest.cc b/base/task_scheduler/priority_queue_unittest.cc new file mode 100644 index 00000000000000..aef95f1e325449 --- /dev/null +++ b/base/task_scheduler/priority_queue_unittest.cc @@ -0,0 +1,245 @@ +// 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 "base/task_scheduler/priority_queue.h" + +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/synchronization/waitable_event.h" +#include "base/task_scheduler/sequence.h" +#include "base/task_scheduler/task.h" +#include "base/task_scheduler/task_traits.h" +#include "base/task_scheduler/test_utils.h" +#include "base/threading/platform_thread.h" +#include "base/threading/simple_thread.h" +#include "base/time/time.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace base { +namespace internal { + +namespace { + +class PriorityQueueCallbackMock { + public: + PriorityQueueCallbackMock() = default; + MOCK_METHOD0(SequenceInsertedInPriorityQueue, void()); + + private: + DISALLOW_COPY_AND_ASSIGN(PriorityQueueCallbackMock); +}; + +class ThreadBeginningTransaction : public SimpleThread { + public: + explicit ThreadBeginningTransaction(PriorityQueue* priority_queue) + : SimpleThread("ThreadBeginningTransaction"), + priority_queue_(priority_queue), + transaction_began_(true, false) {} + + // SimpleThread: + void Run() override { + scoped_ptr transaction = + priority_queue_->BeginTransaction(); + transaction_began_.Signal(); + } + + void ExpectTransactionDoesNotBegin() { + // After a few milliseconds, the call to BeginTransaction() should not have + // returned. + EXPECT_FALSE( + transaction_began_.TimedWait(TimeDelta::FromMilliseconds(250))); + } + + private: + PriorityQueue* const priority_queue_; + WaitableEvent transaction_began_; + + DISALLOW_COPY_AND_ASSIGN(ThreadBeginningTransaction); +}; + +void ExpectSequenceAndSortKeyEq( + const PriorityQueue::SequenceAndSortKey& expected, + const PriorityQueue::SequenceAndSortKey& actual) { + EXPECT_EQ(expected.sequence, actual.sequence); + EXPECT_EQ(expected.sort_key.priority, actual.sort_key.priority); + EXPECT_EQ(expected.sort_key.next_task_sequenced_time, + actual.sort_key.next_task_sequenced_time); +} + +#define EXPECT_SEQUENCE_AND_SORT_KEY_EQ(expected, actual) \ + do { \ + SCOPED_TRACE(""); \ + ExpectSequenceAndSortKeyEq(expected, actual); \ + } while (false) + +} // namespace + +TEST(TaskSchedulerPriorityQueueTest, PushPopPeek) { + // Create test sequences. + scoped_refptr sequence_a(new Sequence); + sequence_a->PushTask(make_scoped_ptr( + new Task(FROM_HERE, Closure(), + TaskTraits().WithPriority(TaskPriority::USER_VISIBLE)))); + SequenceSortKey sort_key_a = sequence_a->GetSortKey(); + + scoped_refptr sequence_b(new Sequence); + sequence_b->PushTask(make_scoped_ptr( + new Task(FROM_HERE, Closure(), + TaskTraits().WithPriority(TaskPriority::USER_BLOCKING)))); + SequenceSortKey sort_key_b = sequence_b->GetSortKey(); + + scoped_refptr sequence_c(new Sequence); + sequence_c->PushTask(make_scoped_ptr( + new Task(FROM_HERE, Closure(), + TaskTraits().WithPriority(TaskPriority::USER_BLOCKING)))); + SequenceSortKey sort_key_c = sequence_c->GetSortKey(); + + scoped_refptr sequence_d(new Sequence); + sequence_d->PushTask(make_scoped_ptr( + new Task(FROM_HERE, Closure(), + TaskTraits().WithPriority(TaskPriority::BACKGROUND)))); + SequenceSortKey sort_key_d = sequence_d->GetSortKey(); + + // Create a PriorityQueue and a Transaction. + testing::StrictMock mock; + PriorityQueue pq( + Bind(&PriorityQueueCallbackMock::SequenceInsertedInPriorityQueue, + Unretained(&mock))); + scoped_ptr transaction(pq.BeginTransaction()); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ(PriorityQueue::SequenceAndSortKey(), + transaction->Peek()); + + // Push |sequence_a| in the PriorityQueue. It becomes the sequence with the + // highest priority. + transaction->Push(make_scoped_ptr( + new PriorityQueue::SequenceAndSortKey(sequence_a, sort_key_a))); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_a, sort_key_a), + transaction->Peek()); + + // Push |sequence_b| in the PriorityQueue. It becomes the sequence with the + // highest priority. + transaction->Push(make_scoped_ptr( + new PriorityQueue::SequenceAndSortKey(sequence_b, sort_key_b))); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_b, sort_key_b), + transaction->Peek()); + + // Push |sequence_c| in the PriorityQueue. |sequence_b| is still the sequence + // with the highest priority. + transaction->Push(make_scoped_ptr( + new PriorityQueue::SequenceAndSortKey(sequence_c, sort_key_c))); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_b, sort_key_b), + transaction->Peek()); + + // Push |sequence_d| in the PriorityQueue. |sequence_b| is still the sequence + // with the highest priority. + transaction->Push(make_scoped_ptr( + new PriorityQueue::SequenceAndSortKey(sequence_d, sort_key_d))); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_b, sort_key_b), + transaction->Peek()); + + // Pop |sequence_b| from the PriorityQueue. |sequence_c| becomes the sequence + // with the highest priority. + transaction->Pop(); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_c, sort_key_c), + transaction->Peek()); + + // Pop |sequence_c| from the PriorityQueue. |sequence_a| becomes the sequence + // with the highest priority. + transaction->Pop(); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_a, sort_key_a), + transaction->Peek()); + + // Pop |sequence_a| from the PriorityQueue. |sequence_d| becomes the sequence + // with the highest priority. + transaction->Pop(); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ( + PriorityQueue::SequenceAndSortKey(sequence_d, sort_key_d), + transaction->Peek()); + + // Pop |sequence_d| from the PriorityQueue. It is now empty. + transaction->Pop(); + EXPECT_SEQUENCE_AND_SORT_KEY_EQ(PriorityQueue::SequenceAndSortKey(), + transaction->Peek()); + + // Expect 4 calls to mock.SequenceInsertedInPriorityQueue() when the + // Transaction is destroyed. + EXPECT_CALL(mock, SequenceInsertedInPriorityQueue()).Times(4); + transaction.reset(); +} + +// Check that creating Transactions on the same thread for 2 unrelated +// PriorityQueues causes a crash. +TEST(TaskSchedulerPriorityQueueTest, IllegalTwoTransactionsSameThread) { + PriorityQueue pq_a(Bind(&DoNothing)); + PriorityQueue pq_b(Bind(&DoNothing)); + + EXPECT_DCHECK_DEATH( + { + scoped_ptr transaction_a = + pq_a.BeginTransaction(); + scoped_ptr transaction_b = + pq_b.BeginTransaction(); + }, + ""); +} + +// Check that there is no crash when Transactions are created on the same thread +// for 2 PriorityQueues which have a predecessor relationship. +TEST(TaskSchedulerPriorityQueueTest, LegalTwoTransactionsSameThread) { + PriorityQueue pq_a(Bind(&DoNothing)); + PriorityQueue pq_b(Bind(&DoNothing), &pq_a); + + // This shouldn't crash. + scoped_ptr transaction_a = + pq_a.BeginTransaction(); + scoped_ptr transaction_b = + pq_b.BeginTransaction(); +} + +// Check that it is possible to begin multiple Transactions for the same +// PriorityQueue on different threads. The call to BeginTransaction() on the +// second thread should block until the Transaction has ended on the first +// thread. +TEST(TaskSchedulerPriorityQueueTest, TwoTransactionsTwoThreads) { + PriorityQueue pq(Bind(&DoNothing)); + + // Call BeginTransaction() on this thread and keep the Transaction alive. + scoped_ptr transaction = pq.BeginTransaction(); + + // Call BeginTransaction() on another thread. + ThreadBeginningTransaction thread_beginning_transaction(&pq); + thread_beginning_transaction.Start(); + + // After a few milliseconds, the call to BeginTransaction() on the other + // thread should not have returned. + thread_beginning_transaction.ExpectTransactionDoesNotBegin(); + + // End the Transaction on the current thread. + transaction.reset(); + + // The other thread should exit after its call to BeginTransaction() returns. + thread_beginning_transaction.Join(); +} + +TEST(TaskSchedulerPriorityQueueTest, SequenceAndSortKeyIsNull) { + EXPECT_TRUE(PriorityQueue::SequenceAndSortKey().is_null()); + + const PriorityQueue::SequenceAndSortKey non_null_sequence_andsort_key( + make_scoped_refptr(new Sequence), + SequenceSortKey(TaskPriority::USER_VISIBLE, TimeTicks())); + EXPECT_FALSE(non_null_sequence_andsort_key.is_null()); +} + +} // namespace internal +} // namespace base diff --git a/base/task_scheduler/scheduler_lock_unittest.cc b/base/task_scheduler/scheduler_lock_unittest.cc index 48b8b087a33e52..6267559d1ecb59 100644 --- a/base/task_scheduler/scheduler_lock_unittest.cc +++ b/base/task_scheduler/scheduler_lock_unittest.cc @@ -10,6 +10,7 @@ #include "base/macros.h" #include "base/rand_util.h" #include "base/synchronization/waitable_event.h" +#include "base/task_scheduler/test_utils.h" #include "base/threading/platform_thread.h" #include "base/threading/simple_thread.h" #include "testing/gtest/include/gtest/gtest.h" @@ -18,13 +19,6 @@ namespace base { namespace internal { namespace { -// Death tests misbehave on Android. -#if DCHECK_IS_ON() && defined(GTEST_HAS_DEATH_TEST) && !defined(OS_ANDROID) -#define EXPECT_DCHECK_DEATH(statement, regex) EXPECT_DEATH(statement, regex) -#else -#define EXPECT_DCHECK_DEATH(statement, regex) -#endif - // Adapted from base::Lock's BasicLockTestThread to make sure // Acquire()/Release() don't crash. class BasicLockTestThread : public SimpleThread { diff --git a/base/task_scheduler/test_utils.h b/base/task_scheduler/test_utils.h new file mode 100644 index 00000000000000..bafd09aa2a294a --- /dev/null +++ b/base/task_scheduler/test_utils.h @@ -0,0 +1,19 @@ +// 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. + +#ifndef BASE_TASK_SCHEDULER_TEST_UTILS_H_ +#define BASE_TASK_SCHEDULER_TEST_UTILS_H_ + +#include "base/logging.h" +#include "build/build_config.h" +#include "testing/gtest/include/gtest/gtest.h" + +// Death tests misbehave on Android. +#if DCHECK_IS_ON() && defined(GTEST_HAS_DEATH_TEST) && !defined(OS_ANDROID) +#define EXPECT_DCHECK_DEATH(statement, regex) EXPECT_DEATH(statement, regex) +#else +#define EXPECT_DCHECK_DEATH(statement, regex) +#endif + +#endif // BASE_TASK_SCHEDULER_TEST_UTILS_H_