fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 1 | // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "base/test/scoped_task_environment.h" |
| 6 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 7 | #include "base/bind_helpers.h" |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 8 | #include "base/lazy_instance.h" |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 9 | #include "base/logging.h" |
Jeremy Roman | e6533c7 | 2018-03-05 17:44:46 | [diff] [blame] | 10 | #include "base/memory/ptr_util.h" |
Alexander Timin | 4f9c35c | 2018-11-01 20:15:20 | [diff] [blame] | 11 | #include "base/message_loop/message_loop.h" |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 12 | #include "base/run_loop.h" |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 13 | #include "base/synchronization/condition_variable.h" |
| 14 | #include "base/synchronization/lock.h" |
Gabriel Charette | 44db142 | 2018-08-06 11:19:33 | [diff] [blame] | 15 | #include "base/task/post_task.h" |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 16 | #include "base/task/sequence_manager/sequence_manager_impl.h" |
| 17 | #include "base/task/sequence_manager/time_domain.h" |
Gabriel Charette | 44db142 | 2018-08-06 11:19:33 | [diff] [blame] | 18 | #include "base/task/task_scheduler/task_scheduler.h" |
Gabriel Charette | 04b138f | 2018-08-06 00:03:22 | [diff] [blame] | 19 | #include "base/task/task_scheduler/task_scheduler_impl.h" |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 20 | #include "base/test/test_mock_time_task_runner.h" |
Wez | d9e4cb77 | 2019-01-09 03:07:03 | [diff] [blame^] | 21 | #include "base/test/test_timeouts.h" |
Gabriel Charette | 723d8fc | 2018-03-22 16:42:43 | [diff] [blame] | 22 | #include "base/threading/sequence_local_storage_map.h" |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 23 | #include "base/threading/thread_local.h" |
Gabriel Charette | 881ffae | 2018-04-19 21:21:04 | [diff] [blame] | 24 | #include "base/threading/thread_restrictions.h" |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 25 | #include "base/threading/thread_task_runner_handle.h" |
Carlos Caballero | a6f99032 | 2018-12-06 14:45:06 | [diff] [blame] | 26 | #include "base/time/clock.h" |
| 27 | #include "base/time/tick_clock.h" |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 28 | #include "base/time/time.h" |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 29 | #include "base/time/time_override.h" |
Wez | d9e4cb77 | 2019-01-09 03:07:03 | [diff] [blame^] | 30 | #include "testing/gtest/include/gtest/gtest.h" |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 31 | |
Sami Kyostila | 91153b1 | 2018-11-28 11:57:25 | [diff] [blame] | 32 | #if defined(OS_POSIX) || defined(OS_FUCHSIA) |
Gabriel Charette | 9d8482ea | 2018-04-11 11:35:07 | [diff] [blame] | 33 | #include "base/files/file_descriptor_watcher_posix.h" |
| 34 | #endif |
| 35 | |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 36 | namespace base { |
| 37 | namespace test { |
| 38 | |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 39 | namespace { |
| 40 | |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 41 | LazyInstance<ThreadLocalPointer<ScopedTaskEnvironment::LifetimeObserver>>::Leaky |
| 42 | environment_lifetime_observer; |
| 43 | |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 44 | base::Optional<MessageLoop::Type> GetMessageLoopTypeForMainThreadType( |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 45 | ScopedTaskEnvironment::MainThreadType main_thread_type) { |
| 46 | switch (main_thread_type) { |
| 47 | case ScopedTaskEnvironment::MainThreadType::DEFAULT: |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 48 | case ScopedTaskEnvironment::MainThreadType::MOCK_TIME: |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 49 | return MessageLoop::TYPE_DEFAULT; |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 50 | case ScopedTaskEnvironment::MainThreadType::UI: |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 51 | case ScopedTaskEnvironment::MainThreadType::UI_MOCK_TIME: |
| 52 | return MessageLoop::TYPE_UI; |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 53 | case ScopedTaskEnvironment::MainThreadType::IO: |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 54 | case ScopedTaskEnvironment::MainThreadType::IO_MOCK_TIME: |
| 55 | return MessageLoop::TYPE_IO; |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 56 | } |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 57 | NOTREACHED(); |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 58 | return base::nullopt; |
| 59 | } |
| 60 | |
| 61 | std::unique_ptr<sequence_manager::SequenceManager> |
| 62 | CreateSequenceManagerForMainThreadType( |
| 63 | ScopedTaskEnvironment::MainThreadType main_thread_type) { |
| 64 | auto type = GetMessageLoopTypeForMainThreadType(main_thread_type); |
| 65 | if (!type) { |
| 66 | return nullptr; |
| 67 | } else { |
| 68 | auto settings = base::sequence_manager::SequenceManager::Settings{ |
| 69 | .message_loop_type = *type}; |
| 70 | return sequence_manager::CreateSequenceManagerOnCurrentThreadWithPump( |
| 71 | MessageLoop::CreateMessagePumpForType(*type), std::move(settings)); |
| 72 | } |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 73 | } |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 74 | |
Carlos Caballero | a6f99032 | 2018-12-06 14:45:06 | [diff] [blame] | 75 | class TickClockBasedClock : public Clock { |
| 76 | public: |
| 77 | explicit TickClockBasedClock(const TickClock* tick_clock) |
| 78 | : tick_clock_(*tick_clock), |
| 79 | start_ticks_(tick_clock_.NowTicks()), |
| 80 | start_time_(Time::UnixEpoch()) {} |
| 81 | |
| 82 | Time Now() const override { |
| 83 | return start_time_ + (tick_clock_.NowTicks() - start_ticks_); |
| 84 | } |
| 85 | |
| 86 | private: |
| 87 | const TickClock& tick_clock_; |
| 88 | const TimeTicks start_ticks_; |
| 89 | const Time start_time_; |
| 90 | }; |
| 91 | |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 92 | } // namespace |
| 93 | |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 94 | class ScopedTaskEnvironment::MockTimeDomain |
| 95 | : public sequence_manager::TimeDomain, |
| 96 | public TickClock { |
| 97 | public: |
| 98 | explicit MockTimeDomain(ScopedTaskEnvironment::NowSource now_source) { |
| 99 | DCHECK_EQ(nullptr, current_mock_time_domain_); |
| 100 | current_mock_time_domain_ = this; |
| 101 | if (now_source == ScopedTaskEnvironment::NowSource::MAIN_THREAD_MOCK_TIME) { |
| 102 | time_overrides_ = std::make_unique<subtle::ScopedTimeClockOverrides>( |
| 103 | &MockTimeDomain::GetTime, &MockTimeDomain::GetTimeTicks, nullptr); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | ~MockTimeDomain() override { |
| 108 | DCHECK_EQ(this, current_mock_time_domain_); |
| 109 | current_mock_time_domain_ = nullptr; |
| 110 | } |
| 111 | |
| 112 | static MockTimeDomain* current_mock_time_domain_; |
| 113 | |
| 114 | static Time GetTime() { |
| 115 | return Time::UnixEpoch() + (current_mock_time_domain_->Now() - TimeTicks()); |
| 116 | } |
| 117 | |
| 118 | static TimeTicks GetTimeTicks() { return current_mock_time_domain_->Now(); } |
| 119 | |
| 120 | using TimeDomain::NextScheduledRunTime; |
| 121 | |
| 122 | static std::unique_ptr<ScopedTaskEnvironment::MockTimeDomain> Create( |
| 123 | ScopedTaskEnvironment::MainThreadType main_thread_type, |
| 124 | ScopedTaskEnvironment::NowSource now_source) { |
| 125 | if (main_thread_type == MainThreadType::MOCK_TIME || |
| 126 | main_thread_type == MainThreadType::UI_MOCK_TIME || |
| 127 | main_thread_type == MainThreadType::IO_MOCK_TIME) { |
| 128 | return std::make_unique<ScopedTaskEnvironment::MockTimeDomain>( |
| 129 | now_source); |
| 130 | } |
| 131 | return nullptr; |
| 132 | } |
| 133 | |
| 134 | // sequence_manager::TimeDomain: |
| 135 | |
| 136 | sequence_manager::LazyNow CreateLazyNow() const override { |
| 137 | base::AutoLock lock(now_ticks_lock_); |
| 138 | return sequence_manager::LazyNow(now_ticks_); |
| 139 | } |
| 140 | |
| 141 | TimeTicks Now() const override { |
| 142 | // This can be called from any thread. |
| 143 | base::AutoLock lock(now_ticks_lock_); |
| 144 | return now_ticks_; |
| 145 | } |
| 146 | |
| 147 | Optional<TimeDelta> DelayTillNextTask( |
| 148 | sequence_manager::LazyNow* lazy_now) override { |
| 149 | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 150 | |
| 151 | // Make sure TimeDomain::NextScheduledRunTime has taken canceled tasks into |
| 152 | // account, ReclaimMemory sweeps canceled delayed tasks. |
| 153 | sequence_manager()->ReclaimMemory(); |
| 154 | Optional<TimeTicks> run_time = NextScheduledRunTime(); |
| 155 | // Check if we've run out of tasks. |
| 156 | if (!run_time) |
| 157 | return base::nullopt; |
| 158 | |
| 159 | // Check if we have a task that should be running now. |
| 160 | if (run_time <= now_ticks_) |
| 161 | return base::TimeDelta(); |
| 162 | |
| 163 | // The next task is a future delayed task. Since we're using mock time, we |
| 164 | // don't want an actual OS level delayed wake up scheduled, so pretend we |
| 165 | // have no more work. This will result in MaybeFastForwardToNextTask getting |
| 166 | // called which lets us advance |now_ticks_|. |
| 167 | return base::nullopt; |
| 168 | } |
| 169 | |
| 170 | // This method is called when the underlying message pump has run out of |
| 171 | // non-delayed work. |
| 172 | bool MaybeFastForwardToNextTask(bool quit_when_idle_requested) override { |
| 173 | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 174 | // If we're being externally controlled by a RunLoop in client code, check |
| 175 | // if the RunLoop is due to quit when idle, if so we don't want to advance |
| 176 | // mock time. |
| 177 | if (stop_when_message_pump_is_idle_ && quit_when_idle_requested) |
| 178 | return false; |
| 179 | |
| 180 | // We don't need to call ReclaimMemory here because |
| 181 | // DelayTillNextTask will have dealt with cancelled delayed tasks for us. |
| 182 | Optional<TimeTicks> run_time = NextScheduledRunTime(); |
| 183 | if (!run_time) { |
| 184 | // We've run out of tasks, but ScopedTaskEnvironment::FastForwardBy |
| 185 | // requires the virtual time to be consumed. |
| 186 | if (now_ticks_ < allow_advance_until_ && !allow_advance_until_.is_max()) |
| 187 | SetTime(allow_advance_until_); |
| 188 | return false; |
| 189 | } |
| 190 | |
| 191 | // Don't advance past |allow_advance_until_|. |
| 192 | DCHECK_GT(*run_time, now_ticks_); |
| 193 | TimeTicks time_to_advance_to = std::min(allow_advance_until_, *run_time); |
| 194 | if (time_to_advance_to <= now_ticks_) |
| 195 | return false; |
| 196 | |
| 197 | SetTime(time_to_advance_to); |
| 198 | |
| 199 | // Make sure a DoWork is scheduled. |
| 200 | return true; |
| 201 | } |
| 202 | |
| 203 | const char* GetName() const override { return "MockTimeDomain"; } |
| 204 | |
| 205 | // TickClock implementation: |
| 206 | TimeTicks NowTicks() const override { return Now(); } |
| 207 | |
| 208 | // Allows time to advance when reaching idle, until |
| 209 | // |now_ticks_ == advance_until|. No-op if |advance_until <= now_ticks_|. |
| 210 | // Doesn't schedule work by itself. |
| 211 | void SetAllowTimeToAutoAdvanceUntil(TimeTicks advance_until) { |
| 212 | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 213 | allow_advance_until_ = advance_until; |
| 214 | } |
| 215 | |
| 216 | void SetStopWhenMessagePumpIsIdle(bool stop_when_message_pump_is_idle) { |
| 217 | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 218 | stop_when_message_pump_is_idle_ = stop_when_message_pump_is_idle; |
| 219 | } |
| 220 | |
| 221 | private: |
| 222 | void SetTime(TimeTicks time) { |
| 223 | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| 224 | DCHECK_LE(time, allow_advance_until_); |
| 225 | |
| 226 | base::AutoLock lock(now_ticks_lock_); |
| 227 | now_ticks_ = time; |
| 228 | } |
| 229 | |
| 230 | SEQUENCE_CHECKER(sequence_checker_); |
| 231 | |
| 232 | std::unique_ptr<subtle::ScopedTimeClockOverrides> time_overrides_; |
| 233 | |
| 234 | // By default we want RunLoop.Run() to advance virtual time due to the API |
| 235 | // contract. |
| 236 | TimeTicks allow_advance_until_ = TimeTicks::Max(); |
| 237 | bool stop_when_message_pump_is_idle_ = true; |
| 238 | |
| 239 | // Protects |now_ticks_| |
| 240 | mutable Lock now_ticks_lock_; |
| 241 | |
| 242 | // Only ever written to from the main sequence. |
| 243 | // TODO(alexclarke): We can't override now by default with now_ticks_ staring |
| 244 | // from zero because many tests have checks that TimeTicks::Now() returns a |
| 245 | // non-zero result. |
| 246 | TimeTicks now_ticks_; |
| 247 | }; |
| 248 | |
| 249 | ScopedTaskEnvironment::MockTimeDomain* |
| 250 | ScopedTaskEnvironment::MockTimeDomain::current_mock_time_domain_ = nullptr; |
| 251 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 252 | class ScopedTaskEnvironment::TestTaskTracker |
| 253 | : public internal::TaskSchedulerImpl::TaskTrackerImpl { |
| 254 | public: |
| 255 | TestTaskTracker(); |
| 256 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 257 | // Allow running tasks. |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 258 | void AllowRunTasks(); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 259 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 260 | // Disallow running tasks. Returns true on success; success requires there to |
| 261 | // be no tasks currently running. Returns false if >0 tasks are currently |
| 262 | // running. Prior to returning false, it will attempt to block until at least |
| 263 | // one task has completed (in an attempt to avoid callers busy-looping |
| 264 | // DisallowRunTasks() calls with the same set of slowly ongoing tasks). This |
| 265 | // block attempt will also have a short timeout (in an attempt to prevent the |
| 266 | // fallout of blocking: if the only task remaining is blocked on the main |
| 267 | // thread, waiting for it to complete results in a deadlock...). |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 268 | bool DisallowRunTasks(); |
| 269 | |
| 270 | private: |
| 271 | friend class ScopedTaskEnvironment; |
| 272 | |
| 273 | // internal::TaskSchedulerImpl::TaskTrackerImpl: |
Robert Liao | d07daf9 | 2017-12-18 01:38:07 | [diff] [blame] | 274 | void RunOrSkipTask(internal::Task task, |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 275 | internal::Sequence* sequence, |
Jesse McKenna | c331b12a7 | 2018-11-21 22:39:03 | [diff] [blame] | 276 | const TaskTraits& traits, |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 277 | bool can_run_task) override; |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 278 | |
| 279 | // Synchronizes accesses to members below. |
| 280 | Lock lock_; |
| 281 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 282 | // True if running tasks is allowed. |
| 283 | bool can_run_tasks_ = true; |
| 284 | |
| 285 | // Signaled when |can_run_tasks_| becomes true. |
| 286 | ConditionVariable can_run_tasks_cv_; |
| 287 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 288 | // Signaled when a task is completed. |
| 289 | ConditionVariable task_completed_; |
| 290 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 291 | // Number of tasks that are currently running. |
| 292 | int num_tasks_running_ = 0; |
| 293 | |
| 294 | DISALLOW_COPY_AND_ASSIGN(TestTaskTracker); |
| 295 | }; |
| 296 | |
| 297 | ScopedTaskEnvironment::ScopedTaskEnvironment( |
| 298 | MainThreadType main_thread_type, |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 299 | ExecutionMode execution_control_mode, |
| 300 | NowSource now_source, |
Alex Clarke | 3cf9cd8e | 2019-01-08 08:40:00 | [diff] [blame] | 301 | trait_helpers::NotATraitTag) |
Alex Clarke | 1052bb0 | 2018-12-14 09:44:16 | [diff] [blame] | 302 | : main_thread_type_(main_thread_type), |
| 303 | execution_control_mode_(execution_control_mode), |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 304 | mock_time_domain_(MockTimeDomain::Create(main_thread_type, now_source)), |
| 305 | sequence_manager_( |
| 306 | CreateSequenceManagerForMainThreadType(main_thread_type)), |
| 307 | task_queue_(CreateDefaultTaskQueue()), |
| 308 | mock_clock_(mock_time_domain_ ? std::make_unique<TickClockBasedClock>( |
| 309 | mock_time_domain_.get()) |
| 310 | : nullptr), |
Sami Kyostila | 91153b1 | 2018-11-28 11:57:25 | [diff] [blame] | 311 | #if defined(OS_POSIX) || defined(OS_FUCHSIA) |
Alexander Timin | 6247f6a91a | 2018-10-27 15:40:10 | [diff] [blame] | 312 | file_descriptor_watcher_(main_thread_type == MainThreadType::IO |
| 313 | ? std::make_unique<FileDescriptorWatcher>( |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 314 | task_queue_->task_runner()) |
Alexander Timin | 6247f6a91a | 2018-10-27 15:40:10 | [diff] [blame] | 315 | : nullptr), |
Sami Kyostila | 91153b1 | 2018-11-28 11:57:25 | [diff] [blame] | 316 | #endif // defined(OS_POSIX) || defined(OS_FUCHSIA) |
Wez | d9e4cb77 | 2019-01-09 03:07:03 | [diff] [blame^] | 317 | task_tracker_(new TestTaskTracker()), |
| 318 | // TODO(https://crbug.com/918724): Enable Run() timeouts even for |
| 319 | // instances created with *MOCK_TIME, and determine whether the timeout |
| 320 | // can be reduced from action_max_timeout() to action_timeout(). |
| 321 | run_loop_timeout_( |
| 322 | mock_time_domain_ ? TimeDelta() : TestTimeouts::action_max_timeout(), |
| 323 | BindRepeating([]() { LOG(FATAL) << "Run() timed out."; })) { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 324 | CHECK(now_source == NowSource::REAL_TIME || mock_time_domain_) |
| 325 | << "NowSource must be REAL_TIME unless we're using mock time"; |
Alex Clarke | 1052bb0 | 2018-12-14 09:44:16 | [diff] [blame] | 326 | CHECK(!base::ThreadTaskRunnerHandle::IsSet()); |
Wez | eb4e995 | 2018-08-28 22:15:47 | [diff] [blame] | 327 | CHECK(!TaskScheduler::GetInstance()) |
| 328 | << "Someone has already initialized TaskScheduler. If nothing in your " |
| 329 | "test does so, then a test that ran earlier may have initialized one, " |
| 330 | "and leaked it. base::TestSuite will trap leaked globals, unless " |
| 331 | "someone has explicitly disabled it with " |
| 332 | "DisableCheckForLeakedGlobals()."; |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 333 | |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 334 | sequence_manager_->SetDefaultTaskRunner(task_queue_->task_runner()); |
| 335 | |
Marijn Kruisselbrink | cbc3df8 | 2017-07-06 19:25:59 | [diff] [blame] | 336 | // Instantiate a TaskScheduler with 2 threads in each of its 4 pools. Threads |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 337 | // stay alive even when they don't have work. |
Marijn Kruisselbrink | cbc3df8 | 2017-07-06 19:25:59 | [diff] [blame] | 338 | // Each pool uses two threads to prevent deadlocks in unit tests that have a |
| 339 | // sequence that uses WithBaseSyncPrimitives() to wait on the result of |
| 340 | // another sequence. This isn't perfect (doesn't solve wait chains) but solves |
| 341 | // the basic use case for now. |
| 342 | // TODO(fdoray/jeffreyhe): Make the TaskScheduler dynamically replace blocked |
| 343 | // threads and get rid of this limitation. http://crbug.com/738104 |
| 344 | constexpr int kMaxThreads = 2; |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 345 | const TimeDelta kSuggestedReclaimTime = TimeDelta::Max(); |
Jeffrey He | fbf000f4 | 2017-07-26 22:44:45 | [diff] [blame] | 346 | const SchedulerWorkerPoolParams worker_pool_params(kMaxThreads, |
| 347 | kSuggestedReclaimTime); |
Jeremy Roman | 9532f25 | 2017-08-16 23:27:24 | [diff] [blame] | 348 | TaskScheduler::SetInstance(std::make_unique<internal::TaskSchedulerImpl>( |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 349 | "ScopedTaskEnvironment", WrapUnique(task_tracker_))); |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 350 | task_scheduler_ = TaskScheduler::GetInstance(); |
Gabriel Charette | 43847232 | 2018-11-16 14:42:38 | [diff] [blame] | 351 | TaskScheduler::GetInstance()->Start({ |
| 352 | worker_pool_params, worker_pool_params, worker_pool_params, |
| 353 | worker_pool_params |
| 354 | #if defined(OS_WIN) |
| 355 | , |
| 356 | // Enable the MTA in unit tests to match the browser process' |
| 357 | // TaskScheduler configuration. |
| 358 | // |
| 359 | // This has the adverse side-effect of enabling the MTA in non-browser |
| 360 | // unit tests as well but the downside there is not as bad as not having |
| 361 | // it in browser unit tests. It just means some COM asserts may pass in |
| 362 | // unit tests where they wouldn't in integration tests or prod. That's |
| 363 | // okay because unit tests are already generally very loose on allowing |
| 364 | // I/O, waits, etc. Such misuse will still be caught in later phases |
| 365 | // (and COM usage should already be pretty much inexistent in sandboxed |
| 366 | // processes). |
| 367 | TaskScheduler::InitParams::SharedWorkerPoolEnvironment::COM_MTA |
| 368 | #endif |
| 369 | }); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 370 | |
| 371 | if (execution_control_mode_ == ExecutionMode::QUEUED) |
| 372 | CHECK(task_tracker_->DisallowRunTasks()); |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 373 | |
| 374 | LifetimeObserver* observer = environment_lifetime_observer.Get().Get(); |
| 375 | if (observer) { |
| 376 | observer->OnScopedTaskEnvironmentCreated(main_thread_type, |
| 377 | GetMainThreadTaskRunner()); |
| 378 | } |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 379 | } |
| 380 | |
| 381 | ScopedTaskEnvironment::~ScopedTaskEnvironment() { |
gab | 13c37ee | 2017-05-18 17:47:44 | [diff] [blame] | 382 | // Ideally this would RunLoop().RunUntilIdle() here to catch any errors or |
| 383 | // infinite post loop in the remaining work but this isn't possible right now |
| 384 | // because base::~MessageLoop() didn't use to do this and adding it here would |
| 385 | // make the migration away from MessageLoop that much harder. |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 386 | CHECK_EQ(TaskScheduler::GetInstance(), task_scheduler_); |
fdoray | 6044a51d | 2017-04-13 12:04:20 | [diff] [blame] | 387 | // Without FlushForTesting(), DeleteSoon() and ReleaseSoon() tasks could be |
| 388 | // skipped, resulting in memory leaks. |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 389 | task_tracker_->AllowRunTasks(); |
fdoray | 6044a51d | 2017-04-13 12:04:20 | [diff] [blame] | 390 | TaskScheduler::GetInstance()->FlushForTesting(); |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 391 | TaskScheduler::GetInstance()->Shutdown(); |
| 392 | TaskScheduler::GetInstance()->JoinForTesting(); |
Gabriel Charette | 881ffae | 2018-04-19 21:21:04 | [diff] [blame] | 393 | // Destroying TaskScheduler state can result in waiting on worker threads. |
| 394 | // Make sure this is allowed to avoid flaking tests that have disallowed waits |
| 395 | // on their main thread. |
| 396 | ScopedAllowBaseSyncPrimitivesForTesting allow_waits_to_destroy_task_tracker; |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 397 | TaskScheduler::SetInstance(nullptr); |
Alex Clarke | 1052bb0 | 2018-12-14 09:44:16 | [diff] [blame] | 398 | task_queue_ = nullptr; |
| 399 | NotifyDestructionObserversAndReleaseSequenceManager(); |
| 400 | } |
| 401 | |
| 402 | void ScopedTaskEnvironment:: |
| 403 | NotifyDestructionObserversAndReleaseSequenceManager() { |
| 404 | // A derived classes may call this method early. |
| 405 | if (!sequence_manager_) |
| 406 | return; |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 407 | |
| 408 | LifetimeObserver* observer = environment_lifetime_observer.Get().Get(); |
| 409 | if (observer) |
| 410 | observer->OnScopedTaskEnvironmentDestroyed(); |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 411 | |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 412 | if (mock_time_domain_) |
| 413 | sequence_manager_->UnregisterTimeDomain(mock_time_domain_.get()); |
Alex Clarke | 1052bb0 | 2018-12-14 09:44:16 | [diff] [blame] | 414 | |
| 415 | sequence_manager_.reset(); |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 416 | } |
| 417 | |
| 418 | scoped_refptr<sequence_manager::TaskQueue> |
| 419 | ScopedTaskEnvironment::CreateDefaultTaskQueue() { |
| 420 | if (mock_time_domain_) |
| 421 | sequence_manager_->RegisterTimeDomain(mock_time_domain_.get()); |
| 422 | |
| 423 | return sequence_manager_->CreateTaskQueue( |
| 424 | sequence_manager::TaskQueue::Spec("scoped_task_environment_default") |
| 425 | .SetTimeDomain(mock_time_domain_.get())); |
Alexander Timin | 011df9a | 2018-10-16 20:15:57 | [diff] [blame] | 426 | } |
| 427 | |
| 428 | void ScopedTaskEnvironment::SetLifetimeObserver( |
| 429 | ScopedTaskEnvironment::LifetimeObserver* lifetime_observer) { |
| 430 | DCHECK_NE(!!environment_lifetime_observer.Get().Get(), !!lifetime_observer); |
| 431 | environment_lifetime_observer.Get().Set(lifetime_observer); |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 432 | } |
| 433 | |
fdoray | 812f416 | 2017-05-16 15:34:51 | [diff] [blame] | 434 | scoped_refptr<base::SingleThreadTaskRunner> |
| 435 | ScopedTaskEnvironment::GetMainThreadTaskRunner() { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 436 | return task_queue_->task_runner(); |
fdoray | 812f416 | 2017-05-16 15:34:51 | [diff] [blame] | 437 | } |
| 438 | |
Gabriel Charette | 32870324 | 2018-04-25 18:07:29 | [diff] [blame] | 439 | bool ScopedTaskEnvironment::MainThreadHasPendingTask() const { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 440 | sequence_manager::internal::SequenceManagerImpl* sequence_manager_impl = |
| 441 | static_cast<sequence_manager::internal::SequenceManagerImpl*>( |
| 442 | sequence_manager_.get()); |
| 443 | // ReclaimMemory sweeps canceled delayed tasks. |
| 444 | sequence_manager_impl->ReclaimMemory(); |
| 445 | // Unfortunately this API means different things depending on whether mock |
| 446 | // time is used or not. If MockTime is used then tests want to know if there |
| 447 | // are any delayed or non-delayed tasks, otherwise only non-delayed tasks are |
| 448 | // considered. |
| 449 | if (mock_time_domain_) |
| 450 | return sequence_manager_impl->HasTasks(); |
| 451 | return !sequence_manager_impl->IsIdleForTesting(); |
Gabriel Charette | 32870324 | 2018-04-25 18:07:29 | [diff] [blame] | 452 | } |
| 453 | |
fdoray | f2b4d83 | 2017-05-10 12:24:33 | [diff] [blame] | 454 | void ScopedTaskEnvironment::RunUntilIdle() { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 455 | // Prevent virtual time from advancing while within this call. |
| 456 | if (mock_time_domain_) |
| 457 | mock_time_domain_->SetAllowTimeToAutoAdvanceUntil(TimeTicks()); |
| 458 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 459 | // TODO(gab): This can be heavily simplified to essentially: |
| 460 | // bool HasMainThreadTasks() { |
| 461 | // if (message_loop_) |
| 462 | // return !message_loop_->IsIdleForTesting(); |
| 463 | // return mock_time_task_runner_->NextPendingTaskDelay().is_zero(); |
| 464 | // } |
| 465 | // while (task_tracker_->HasIncompleteTasks() || HasMainThreadTasks()) { |
| 466 | // base::RunLoop().RunUntilIdle(); |
| 467 | // // Avoid busy-looping. |
| 468 | // if (task_tracker_->HasIncompleteTasks()) |
| 469 | // PlatformThread::Sleep(TimeDelta::FromMilliSeconds(1)); |
| 470 | // } |
Gabriel Charette | 650ec6c1 | 2018-07-30 17:28:35 | [diff] [blame] | 471 | // Update: This can likely be done now that MessageLoop::IsIdleForTesting() |
| 472 | // checks all queues. |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 473 | // |
| 474 | // Other than that it works because once |task_tracker_->HasIncompleteTasks()| |
| 475 | // is false we know for sure that the only thing that can make it true is a |
| 476 | // main thread task (ScopedTaskEnvironment owns all the threads). As such we |
| 477 | // can't racily see it as false on the main thread and be wrong as if it the |
| 478 | // main thread sees the atomic count at zero, it's the only one that can make |
| 479 | // it go up. And the only thing that can make it go up on the main thread are |
| 480 | // main thread tasks and therefore we're done if there aren't any left. |
| 481 | // |
| 482 | // This simplification further allows simplification of DisallowRunTasks(). |
| 483 | // |
| 484 | // This can also be simplified even further once TaskTracker becomes directly |
| 485 | // aware of main thread tasks. https://crbug.com/660078. |
| 486 | |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 487 | for (;;) { |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 488 | task_tracker_->AllowRunTasks(); |
fdoray | f2b4d83 | 2017-05-10 12:24:33 | [diff] [blame] | 489 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 490 | // First run as many tasks as possible on the main thread in parallel with |
| 491 | // tasks in TaskScheduler. This increases likelihood of TSAN catching |
| 492 | // threading errors and eliminates possibility of hangs should a |
| 493 | // TaskScheduler task synchronously block on a main thread task |
| 494 | // (TaskScheduler::FlushForTesting() can't be used here for that reason). |
| 495 | RunLoop().RunUntilIdle(); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 496 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 497 | // Then halt TaskScheduler. DisallowRunTasks() failing indicates that there |
| 498 | // were TaskScheduler tasks currently running. In that case, try again from |
| 499 | // top when DisallowRunTasks() yields control back to this thread as they |
| 500 | // may have posted main thread tasks. |
| 501 | if (!task_tracker_->DisallowRunTasks()) |
Matt Wolenetz | 7ab03b1 | 2017-08-09 18:32:37 | [diff] [blame] | 502 | continue; |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 503 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 504 | // Once TaskScheduler is halted. Run any remaining main thread tasks (which |
| 505 | // may have been posted by TaskScheduler tasks that completed between the |
| 506 | // above main thread RunUntilIdle() and TaskScheduler DisallowRunTasks()). |
| 507 | // Note: this assumes that no main thread task synchronously blocks on a |
| 508 | // TaskScheduler tasks (it certainly shouldn't); this call could otherwise |
| 509 | // hang. |
| 510 | RunLoop().RunUntilIdle(); |
| 511 | |
| 512 | // The above RunUntilIdle() guarantees there are no remaining main thread |
| 513 | // tasks (the TaskScheduler being halted during the last RunUntilIdle() is |
| 514 | // key as it prevents a task being posted to it racily with it determining |
| 515 | // it had no work remaining). Therefore, we're done if there is no more work |
| 516 | // on TaskScheduler either (there can be TaskScheduler work remaining if |
| 517 | // DisallowRunTasks() preempted work and/or the last RunUntilIdle() posted |
| 518 | // more TaskScheduler tasks). |
| 519 | // Note: this last |if| couldn't be turned into a |do {} while();|. A |
| 520 | // conditional loop makes it such that |continue;| results in checking the |
| 521 | // condition (not unconditionally loop again) which would be incorrect for |
| 522 | // the above logic as it'd then be possible for a TaskScheduler task to be |
| 523 | // running during the DisallowRunTasks() test, causing it to fail, but then |
| 524 | // post to the main thread and complete before the loop's condition is |
Gabriel Charette | d97b059 | 2018-01-03 18:28:59 | [diff] [blame] | 525 | // verified which could result in HasIncompleteUndelayedTasksForTesting() |
| 526 | // returning false and the loop erroneously exiting with a pending task on |
| 527 | // the main thread. |
| 528 | if (!task_tracker_->HasIncompleteUndelayedTasksForTesting()) |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 529 | break; |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 530 | } |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 531 | |
| 532 | // The above loop always ends with running tasks being disallowed. Re-enable |
| 533 | // parallel execution before returning unless in ExecutionMode::QUEUED. |
| 534 | if (execution_control_mode_ != ExecutionMode::QUEUED) |
| 535 | task_tracker_->AllowRunTasks(); |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 536 | |
| 537 | if (mock_time_domain_) |
| 538 | mock_time_domain_->SetAllowTimeToAutoAdvanceUntil(TimeTicks::Max()); |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 539 | } |
| 540 | |
| 541 | void ScopedTaskEnvironment::FastForwardBy(TimeDelta delta) { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 542 | MessageLoopCurrent::ScopedNestableTaskAllower allow; |
| 543 | DCHECK(mock_time_domain_); |
| 544 | mock_time_domain_->SetStopWhenMessagePumpIsIdle(false); |
| 545 | mock_time_domain_->SetAllowTimeToAutoAdvanceUntil(mock_time_domain_->Now() + |
| 546 | delta); |
| 547 | RunLoop().RunUntilIdle(); |
| 548 | mock_time_domain_->SetStopWhenMessagePumpIsIdle(true); |
| 549 | mock_time_domain_->SetAllowTimeToAutoAdvanceUntil(TimeTicks::Max()); |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 550 | } |
| 551 | |
| 552 | void ScopedTaskEnvironment::FastForwardUntilNoTasksRemain() { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 553 | // TimeTicks::operator+(TimeDelta) uses saturated arithmetic so it's safe to |
| 554 | // pass in TimeDelta::Max(). |
| 555 | FastForwardBy(TimeDelta::Max()); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 556 | } |
| 557 | |
Carlos Caballero | a6f99032 | 2018-12-06 14:45:06 | [diff] [blame] | 558 | const TickClock* ScopedTaskEnvironment::GetMockTickClock() const { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 559 | DCHECK(mock_time_domain_); |
| 560 | return mock_time_domain_.get(); |
tzik | 9fd6385 | 2018-03-08 06:14:18 | [diff] [blame] | 561 | } |
| 562 | |
Robbie McElrath | 57129d7 | 2018-09-05 22:58:26 | [diff] [blame] | 563 | base::TimeTicks ScopedTaskEnvironment::NowTicks() const { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 564 | DCHECK(mock_time_domain_); |
| 565 | return mock_time_domain_->Now(); |
Robbie McElrath | 57129d7 | 2018-09-05 22:58:26 | [diff] [blame] | 566 | } |
| 567 | |
Carlos Caballero | a6f99032 | 2018-12-06 14:45:06 | [diff] [blame] | 568 | const Clock* ScopedTaskEnvironment::GetMockClock() const { |
| 569 | DCHECK(mock_clock_); |
| 570 | return mock_clock_.get(); |
| 571 | } |
| 572 | |
Bence Béky | b71838b | 2018-04-19 18:23:38 | [diff] [blame] | 573 | size_t ScopedTaskEnvironment::GetPendingMainThreadTaskCount() const { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 574 | // ReclaimMemory sweeps canceled delayed tasks. |
| 575 | sequence_manager_->ReclaimMemory(); |
| 576 | return sequence_manager_->GetPendingTaskCountForTesting(); |
Bence Béky | b71838b | 2018-04-19 18:23:38 | [diff] [blame] | 577 | } |
| 578 | |
| 579 | TimeDelta ScopedTaskEnvironment::NextMainThreadPendingTaskDelay() const { |
Alex Clarke | 2ded172 | 2018-12-13 12:55:20 | [diff] [blame] | 580 | // ReclaimMemory sweeps canceled delayed tasks. |
| 581 | sequence_manager_->ReclaimMemory(); |
| 582 | DCHECK(mock_time_domain_); |
| 583 | Optional<TimeTicks> run_time = mock_time_domain_->NextScheduledRunTime(); |
| 584 | if (run_time) |
| 585 | return *run_time - mock_time_domain_->Now(); |
| 586 | return TimeDelta::Max(); |
Bence Béky | b71838b | 2018-04-19 18:23:38 | [diff] [blame] | 587 | } |
| 588 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 589 | ScopedTaskEnvironment::TestTaskTracker::TestTaskTracker() |
Gabriel Charette | 5b74dcb | 2018-01-19 11:00:53 | [diff] [blame] | 590 | : internal::TaskSchedulerImpl::TaskTrackerImpl("ScopedTaskEnvironment"), |
| 591 | can_run_tasks_cv_(&lock_), |
| 592 | task_completed_(&lock_) {} |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 593 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 594 | void ScopedTaskEnvironment::TestTaskTracker::AllowRunTasks() { |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 595 | AutoLock auto_lock(lock_); |
| 596 | can_run_tasks_ = true; |
| 597 | can_run_tasks_cv_.Broadcast(); |
| 598 | } |
| 599 | |
| 600 | bool ScopedTaskEnvironment::TestTaskTracker::DisallowRunTasks() { |
| 601 | AutoLock auto_lock(lock_); |
| 602 | |
| 603 | // Can't disallow run task if there are tasks running. |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 604 | if (num_tasks_running_ > 0) { |
| 605 | // Attempt to wait a bit so that the caller doesn't busy-loop with the same |
| 606 | // set of pending work. A short wait is required to avoid deadlock |
| 607 | // scenarios. See DisallowRunTasks()'s declaration for more details. |
| 608 | task_completed_.TimedWait(TimeDelta::FromMilliseconds(1)); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 609 | return false; |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 610 | } |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 611 | |
| 612 | can_run_tasks_ = false; |
| 613 | return true; |
| 614 | } |
| 615 | |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 616 | void ScopedTaskEnvironment::TestTaskTracker::RunOrSkipTask( |
Robert Liao | d07daf9 | 2017-12-18 01:38:07 | [diff] [blame] | 617 | internal::Task task, |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 618 | internal::Sequence* sequence, |
Jesse McKenna | c331b12a7 | 2018-11-21 22:39:03 | [diff] [blame] | 619 | const TaskTraits& traits, |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 620 | bool can_run_task) { |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 621 | { |
| 622 | AutoLock auto_lock(lock_); |
| 623 | |
| 624 | while (!can_run_tasks_) |
| 625 | can_run_tasks_cv_.Wait(); |
| 626 | |
| 627 | ++num_tasks_running_; |
| 628 | } |
| 629 | |
Francois Doray | a038ca7 | 2017-09-17 03:26:06 | [diff] [blame] | 630 | internal::TaskSchedulerImpl::TaskTrackerImpl::RunOrSkipTask( |
Jesse McKenna | c331b12a7 | 2018-11-21 22:39:03 | [diff] [blame] | 631 | std::move(task), sequence, traits, can_run_task); |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 632 | |
| 633 | { |
| 634 | AutoLock auto_lock(lock_); |
| 635 | |
| 636 | CHECK_GT(num_tasks_running_, 0); |
| 637 | CHECK(can_run_tasks_); |
| 638 | |
fdoray | b199f1be | 2017-05-29 23:00:03 | [diff] [blame] | 639 | --num_tasks_running_; |
fdoray | f2b4d83 | 2017-05-10 12:24:33 | [diff] [blame] | 640 | |
Gabriel Charette | d4723a3 | 2017-11-24 00:11:18 | [diff] [blame] | 641 | task_completed_.Broadcast(); |
Matt Giuca | 4364443 | 2017-11-22 07:27:18 | [diff] [blame] | 642 | } |
| 643 | } |
| 644 | |
fdoray | a1f53b7 | 2017-04-06 15:16:57 | [diff] [blame] | 645 | } // namespace test |
| 646 | } // namespace base |