| // Copyright 2018 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 "content/browser/scheduler/responsiveness/watcher.h" |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/pending_task.h" |
| #include "base/run_loop.h" |
| #include "base/synchronization/lock.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "content/browser/scheduler/responsiveness/calculator.h" |
| #include "content/browser/scheduler/responsiveness/native_event_observer.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace content { |
| namespace responsiveness { |
| |
| namespace { |
| |
| struct TaskTiming { |
| base::TimeTicks queue_time; |
| base::TimeTicks execution_start_time; |
| base::TimeTicks execution_finish_time; |
| }; |
| |
| class FakeCalculator : public Calculator { |
| public: |
| void TaskOrEventFinishedOnUIThread( |
| base::TimeTicks queue_time, |
| base::TimeTicks execution_start_time, |
| base::TimeTicks execution_finish_time) override { |
| execution_times_ui_.push_back( |
| {queue_time, execution_start_time, execution_finish_time}); |
| } |
| |
| void TaskOrEventFinishedOnIOThread( |
| base::TimeTicks queue_time, |
| base::TimeTicks execution_start_time, |
| base::TimeTicks execution_finish_time) override { |
| base::AutoLock l(io_thread_lock_); |
| execution_times_io_.push_back( |
| {queue_time, execution_start_time, execution_finish_time}); |
| } |
| |
| int NumTasksOnUIThread() { |
| return static_cast<int>(execution_times_ui_.size()); |
| } |
| std::vector<TaskTiming>& ExecutionTimesUIThread() { |
| return execution_times_ui_; |
| } |
| int NumTasksOnIOThread() { |
| base::AutoLock l(io_thread_lock_); |
| return static_cast<int>(execution_times_io_.size()); |
| } |
| std::vector<TaskTiming>& ExecutionTimesIOThread() { |
| base::AutoLock l(io_thread_lock_); |
| return execution_times_io_; |
| } |
| |
| private: |
| std::vector<TaskTiming> execution_times_ui_; |
| base::Lock io_thread_lock_; |
| std::vector<TaskTiming> execution_times_io_; |
| }; |
| |
| class FakeMetricSource : public MetricSource { |
| public: |
| FakeMetricSource(Delegate* delegate, bool register_message_loop_observer) |
| : MetricSource(delegate), |
| register_message_loop_observer_(register_message_loop_observer) {} |
| ~FakeMetricSource() override {} |
| |
| void RegisterMessageLoopObserverUI() override { |
| if (register_message_loop_observer_) |
| MetricSource::RegisterMessageLoopObserverUI(); |
| } |
| void RegisterMessageLoopObserverIO() override { |
| if (register_message_loop_observer_) |
| MetricSource::RegisterMessageLoopObserverIO(); |
| } |
| |
| std::unique_ptr<NativeEventObserver> CreateNativeEventObserver() override { |
| return nullptr; |
| } |
| |
| private: |
| bool register_message_loop_observer_; |
| }; |
| |
| class FakeWatcher : public Watcher { |
| public: |
| std::unique_ptr<Calculator> CreateCalculator() override { |
| std::unique_ptr<FakeCalculator> calculator = |
| std::make_unique<FakeCalculator>(); |
| calculator_ = calculator.get(); |
| return calculator; |
| } |
| |
| std::unique_ptr<MetricSource> CreateMetricSource() override { |
| return std::make_unique<FakeMetricSource>(this, |
| register_message_loop_observer_); |
| } |
| |
| FakeWatcher(bool register_message_loop_observer) |
| : Watcher(), |
| register_message_loop_observer_(register_message_loop_observer) {} |
| |
| int NumTasksOnUIThread() { return calculator_->NumTasksOnUIThread(); } |
| std::vector<TaskTiming>& ExecutionTimesUIThread() { |
| return calculator_->ExecutionTimesUIThread(); |
| } |
| std::vector<TaskTiming>& ExecutionTimesIOThread() { |
| return calculator_->ExecutionTimesIOThread(); |
| } |
| int NumTasksOnIOThread() { return calculator_->NumTasksOnIOThread(); } |
| |
| private: |
| ~FakeWatcher() override {} |
| raw_ptr<FakeCalculator> calculator_ = nullptr; |
| bool register_message_loop_observer_ = false; |
| }; |
| |
| } // namespace |
| |
| class ResponsivenessWatcherTest : public testing::Test { |
| public: |
| void SetUp() override { |
| // Watcher's constructor posts a task to IO thread, which in the unit test |
| // is also this thread. Regardless, we need to let those tasks finish. |
| watcher_ = scoped_refptr<FakeWatcher>( |
| new FakeWatcher(/*register_message_loop_observer=*/false)); |
| watcher_->SetUp(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| void TearDown() override { |
| watcher_->Destroy(); |
| watcher_.reset(); |
| } |
| |
| protected: |
| // This member sets up BrowserThread::IO and BrowserThread::UI. It must be the |
| // first member, as other members may depend on these abstractions. |
| content::BrowserTaskEnvironment task_environment_{ |
| content::BrowserTaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| scoped_refptr<FakeWatcher> watcher_; |
| }; |
| |
| // Test that tasks are forwarded to calculator. |
| TEST_F(ResponsivenessWatcherTest, TaskForwarding) { |
| for (int i = 0; i < 3; ++i) { |
| base::PendingTask task(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| watcher_->WillRunTaskOnUIThread(&task, |
| /* was_blocked_or_low_priority= */ false); |
| watcher_->DidRunTaskOnUIThread(&task); |
| } |
| EXPECT_EQ(3, watcher_->NumTasksOnUIThread()); |
| EXPECT_EQ(0, watcher_->NumTasksOnIOThread()); |
| |
| for (int i = 0; i < 4; ++i) { |
| base::PendingTask task(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| watcher_->WillRunTaskOnIOThread(&task, |
| /* was_blocked_or_low_priority= */ false); |
| watcher_->DidRunTaskOnIOThread(&task); |
| } |
| EXPECT_EQ(3, watcher_->NumTasksOnUIThread()); |
| EXPECT_EQ(4, watcher_->NumTasksOnIOThread()); |
| } |
| |
| // Test that nested tasks are not forwarded to the calculator. |
| TEST_F(ResponsivenessWatcherTest, TaskNesting) { |
| base::PendingTask task1(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| base::PendingTask task2(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| base::PendingTask task3(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| |
| const base::TimeTicks task_1_execution_start_time = base::TimeTicks::Now(); |
| watcher_->WillRunTaskOnUIThread(&task1, |
| /* was_blocked_or_low_priority= */ false); |
| watcher_->WillRunTaskOnUIThread(&task2, |
| /* was_blocked_or_low_priority= */ false); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| const base::TimeTicks task_3_execution_start_time = base::TimeTicks::Now(); |
| EXPECT_EQ(task_1_execution_start_time + base::Milliseconds(1), |
| task_3_execution_start_time); |
| watcher_->WillRunTaskOnUIThread(&task3, |
| /* was_blocked_or_low_priority= */ false); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| const base::TimeTicks task_3_execution_finish_time = base::TimeTicks::Now(); |
| watcher_->DidRunTaskOnUIThread(&task3); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| watcher_->DidRunTaskOnUIThread(&task2); |
| watcher_->DidRunTaskOnUIThread(&task1); |
| |
| ASSERT_EQ(1, watcher_->NumTasksOnUIThread()); |
| |
| // The innermost task should be the one that is passed through, as it didn't |
| // cause reentrancy. |
| const TaskTiming task_timing = watcher_->ExecutionTimesUIThread()[0]; |
| EXPECT_EQ(task3.queue_time, task_timing.queue_time); |
| EXPECT_EQ(task_3_execution_start_time, task_timing.execution_start_time); |
| EXPECT_EQ(task_3_execution_finish_time, task_timing.execution_finish_time); |
| |
| EXPECT_EQ(0, watcher_->NumTasksOnIOThread()); |
| } |
| |
| // Test that native events use execution time instead of queue + execution time. |
| TEST_F(ResponsivenessWatcherTest, NativeEvents) { |
| const base::TimeTicks start_time = base::TimeTicks::Now(); |
| |
| void* opaque_identifier = reinterpret_cast<void*>(0x1234); |
| watcher_->WillRunEventOnUIThread(opaque_identifier); |
| |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| const base::TimeTicks finish_time = base::TimeTicks::Now(); |
| watcher_->DidRunEventOnUIThread(opaque_identifier); |
| |
| ASSERT_EQ(1, watcher_->NumTasksOnUIThread()); |
| const TaskTiming task_timing = watcher_->ExecutionTimesUIThread()[0]; |
| EXPECT_EQ(task_timing.queue_time, start_time); |
| EXPECT_EQ(task_timing.execution_start_time, start_time); |
| EXPECT_EQ(task_timing.execution_finish_time, finish_time); |
| |
| EXPECT_EQ(0, watcher_->NumTasksOnIOThread()); |
| } |
| |
| // Test that the queue duration of a blocked or low priority task is zero. |
| TEST_F(ResponsivenessWatcherTest, BlockedOrLowPriorityTask) { |
| base::PendingTask task(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks::Now(), |
| /*delayed_run_time=*/base::TimeTicks()); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| const base::TimeTicks execution_start_time = base::TimeTicks::Now(); |
| watcher_->WillRunTaskOnUIThread(&task, |
| /* was_blocked_or_low_priority= */ true); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| const base::TimeTicks execution_finish_time = base::TimeTicks::Now(); |
| watcher_->DidRunTaskOnUIThread(&task); |
| |
| ASSERT_EQ(1, watcher_->NumTasksOnUIThread()); |
| const TaskTiming task_timing = watcher_->ExecutionTimesUIThread()[0]; |
| // The queue time should be equal to the execution start time, to simulate |
| // that the task did not spend any time in a queue. This is the desired |
| // behavior for a task that was blocked or low priority. |
| EXPECT_EQ(task_timing.queue_time, execution_start_time); |
| EXPECT_EQ(task_timing.execution_start_time, execution_start_time); |
| EXPECT_EQ(task_timing.execution_finish_time, execution_finish_time); |
| |
| EXPECT_EQ(0, watcher_->NumTasksOnIOThread()); |
| } |
| |
| // Test that the queue duration of a delayed task is zero. |
| TEST_F(ResponsivenessWatcherTest, DelayedTask) { |
| base::PendingTask task(FROM_HERE, base::OnceClosure(), |
| /*queue_time=*/base::TimeTicks(), |
| /*delayed_run_time=*/base::TimeTicks::Now()); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| const base::TimeTicks execution_start_time = base::TimeTicks::Now(); |
| watcher_->WillRunTaskOnUIThread(&task, |
| /* was_blocked_or_low_priority= */ false); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| const base::TimeTicks execution_finish_time = base::TimeTicks::Now(); |
| watcher_->DidRunTaskOnUIThread(&task); |
| |
| ASSERT_EQ(1, watcher_->NumTasksOnUIThread()); |
| const TaskTiming task_timing = watcher_->ExecutionTimesUIThread()[0]; |
| // The queue time should be equal to the execution start time, to simulate |
| // that the task did not spend any time in a queue. This is the desired |
| // behavior for a delayed task. |
| EXPECT_EQ(task_timing.queue_time, execution_start_time); |
| EXPECT_EQ(task_timing.execution_start_time, execution_start_time); |
| EXPECT_EQ(task_timing.execution_finish_time, execution_finish_time); |
| |
| EXPECT_EQ(0, watcher_->NumTasksOnIOThread()); |
| } |
| |
| class ResponsivenessWatcherRealIOThreadTest : public testing::Test { |
| public: |
| ResponsivenessWatcherRealIOThreadTest() |
| : task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD) {} |
| |
| void SetUp() override { |
| // Watcher's constructor posts a task to IO thread. We need to let those |
| // tasks finish. |
| watcher_ = scoped_refptr<FakeWatcher>( |
| new FakeWatcher(/*register_message_loop_observer=*/true)); |
| watcher_->SetUp(); |
| task_environment_.RunIOThreadUntilIdle(); |
| } |
| |
| void TearDown() override { |
| watcher_->Destroy(); |
| watcher_.reset(); |
| |
| // Destroy a task onto the IO thread, which posts back to the UI thread |
| // to complete destruction. |
| task_environment_.RunIOThreadUntilIdle(); |
| task_environment_.RunUntilIdle(); |
| } |
| |
| protected: |
| // This member sets up BrowserThread::IO and BrowserThread::UI. It must be the |
| // first member, as other members may depend on these abstractions. |
| content::BrowserTaskEnvironment task_environment_; |
| |
| scoped_refptr<FakeWatcher> watcher_; |
| }; |
| |
| TEST_F(ResponsivenessWatcherRealIOThreadTest, MessageLoopObserver) { |
| // Post a do-nothing task onto the UI thread. |
| content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE, base::DoNothing()); |
| |
| // Post a do-nothing task onto the IO thread. |
| content::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, base::DoNothing()); |
| |
| // Post a task onto the IO thread that hops back to the UI thread. This |
| // guarantees that both of the do-nothing tasks have already been processed. |
| base::RunLoop run_loop; |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce( |
| [](base::OnceClosure quit_closure) { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, std::move(quit_closure)); |
| }, |
| run_loop.QuitClosure())); |
| run_loop.Run(); |
| |
| ASSERT_GE(watcher_->NumTasksOnUIThread(), 1); |
| const TaskTiming ui_thread_task_timing = |
| watcher_->ExecutionTimesUIThread()[0]; |
| EXPECT_FALSE(ui_thread_task_timing.queue_time.is_null()); |
| EXPECT_FALSE(ui_thread_task_timing.execution_start_time.is_null()); |
| EXPECT_FALSE(ui_thread_task_timing.execution_finish_time.is_null()); |
| |
| ASSERT_GE(watcher_->NumTasksOnIOThread(), 1); |
| const TaskTiming io_thread_task_timing = |
| watcher_->ExecutionTimesUIThread()[0]; |
| EXPECT_FALSE(io_thread_task_timing.queue_time.is_null()); |
| EXPECT_FALSE(io_thread_task_timing.execution_start_time.is_null()); |
| EXPECT_FALSE(io_thread_task_timing.execution_finish_time.is_null()); |
| } |
| |
| } // namespace responsiveness |
| } // namespace content |