Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: third_party/WebKit/Source/platform/scheduler/renderer/task_queue_throttler.cc

Issue 2258133002: [scheduler] Implement time-based cpu throttling. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: One more fix Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2015 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 "platform/scheduler/renderer/task_queue_throttler.h"
6
7 #include <cstdint>
8
9 #include "base/format_macros.h"
10 #include "base/logging.h"
11 #include "base/memory/ptr_util.h"
12 #include "base/optional.h"
13 #include "base/strings/stringprintf.h"
14 #include "platform/scheduler/base/real_time_domain.h"
15 #include "platform/scheduler/child/scheduler_tqm_delegate.h"
16 #include "platform/scheduler/renderer/auto_advancing_virtual_time_domain.h"
17 #include "platform/scheduler/renderer/renderer_scheduler_impl.h"
18 #include "platform/scheduler/renderer/throttled_time_domain.h"
19 #include "platform/scheduler/renderer/web_frame_scheduler_impl.h"
20 #include "public/platform/WebFrameScheduler.h"
21
22 namespace blink {
23 namespace scheduler {
24
25 namespace {
26 const int kMaxBudgetLevelInSeconds = 1;
27
28 base::Optional<base::TimeTicks> NextTaskRunTime(LazyNow* lazy_now,
29 TaskQueue* queue) {
30 if (queue->HasPendingImmediateWork())
31 return lazy_now->Now();
32 return queue->GetNextScheduledWakeUp();
33 }
34 }
Sami 2016/09/22 12:34:07 } // namespace
altimin 2016/09/22 13:45:59 Done.
35
36 TaskQueueThrottler::TimeBudgetPool::TimeBudgetPool(
37 const char* name,
38 TaskQueueThrottler* task_queue_throttler,
39 base::TimeTicks now)
40 : name_(name),
41 task_queue_throttler_(task_queue_throttler),
42 max_budget_level_(base::TimeDelta::FromSeconds(kMaxBudgetLevelInSeconds)),
43 last_checkpoint_(now),
44 cpu_percentage_(1),
45 is_enabled_(true) {}
46
47 TaskQueueThrottler::TimeBudgetPool::~TimeBudgetPool() {}
48
49 void TaskQueueThrottler::TimeBudgetPool::SetTimeBudget(base::TimeTicks now,
50 double cpu_percentage) {
51 Advance(now);
52 cpu_percentage_ = cpu_percentage;
53 }
54
55 void TaskQueueThrottler::TimeBudgetPool::AddQueue(base::TimeTicks now,
56 TaskQueue* queue) {
57 Metadata& metadata = task_queue_throttler_->queue_details_[queue];
58 DCHECK(!metadata.time_budget_pool);
59 metadata.time_budget_pool = this;
60
61 associated_task_queues_.insert(queue);
62
63 if (metadata.IsThrottled() == 0)
Sami 2016/09/22 12:34:07 nit: !metadata.IsThrottled()
altimin 2016/09/22 13:45:59 Ooops. Thanks!
64 return;
65
66 queue->SetQueueEnabled(false);
67
68 task_queue_throttler_->MaybeSchedulePumpQueue(FROM_HERE, now, queue,
69 GetNextAllowedRunTime());
70 }
71
72 void TaskQueueThrottler::TimeBudgetPool::RemoveQueue(base::TimeTicks now,
73 TaskQueue* queue) {
74 auto find_it = task_queue_throttler_->queue_details_.find(queue);
75 DCHECK(find_it != task_queue_throttler_->queue_details_.end() &&
76 find_it->second.time_budget_pool == this);
77 find_it->second.time_budget_pool = nullptr;
78 bool is_throttled = find_it->second.IsThrottled();
79
80 task_queue_throttler_->MaybeDeleteQueueMetadata(find_it);
81 associated_task_queues_.erase(queue);
82
83 if (is_throttled)
84 return;
85
86 task_queue_throttler_->MaybeSchedulePumpQueue(FROM_HERE, now, queue,
87 base::nullopt);
88 }
89
90 void TaskQueueThrottler::TimeBudgetPool::EnableThrottling(LazyNow* lazy_now) {
91 if (is_enabled_)
92 return;
93 is_enabled_ = true;
94
95 for (TaskQueue* queue : associated_task_queues_) {
Sami 2016/09/22 12:34:07 Could we just call BlockQueues() here?
altimin 2016/09/22 13:45:58 Done.
96 if (!task_queue_throttler_->IsThrottled(queue))
97 continue;
98
99 queue->SetQueueEnabled(false);
100
101 task_queue_throttler_->MaybeSchedulePumpQueue(
102 FROM_HERE, lazy_now->Now(), queue, GetNextAllowedRunTime());
103 }
104 }
105
106 void TaskQueueThrottler::TimeBudgetPool::DisableThrottling(LazyNow* lazy_now) {
107 if (!is_enabled_)
108 return;
109 is_enabled_ = false;
110
111 for (TaskQueue* queue : associated_task_queues_) {
112 if (!task_queue_throttler_->IsThrottled(queue))
113 continue;
114
115 task_queue_throttler_->MaybeSchedulePumpQueue(FROM_HERE, lazy_now->Now(),
116 queue, base::nullopt);
117 }
118 }
119
120 bool TaskQueueThrottler::TimeBudgetPool::IsThrottlingEnabled() const {
121 return is_enabled_;
122 }
123
124 void TaskQueueThrottler::TimeBudgetPool::Close() {
125 DCHECK_EQ(0u, associated_task_queues_.size());
126
127 task_queue_throttler_->time_budget_pools_.erase(this);
128 }
129
130 bool TaskQueueThrottler::TimeBudgetPool::HasEnoughBudgetToRun(
131 base::TimeTicks now) {
132 Advance(now);
133 return !is_enabled_ || current_budget_level_.InMicroseconds() >= 0;
134 }
135
136 base::TimeTicks TaskQueueThrottler::TimeBudgetPool::GetNextAllowedRunTime() {
137 if (!is_enabled_ || current_budget_level_.InMicroseconds() >= 0) {
138 return last_checkpoint_;
139 } else {
140 // Subtract because current_budget is negative.
141 return last_checkpoint_ - current_budget_level_ / cpu_percentage_;
142 }
143 }
144
145 void TaskQueueThrottler::TimeBudgetPool::RecordTaskRunTime(
146 base::TimeDelta task_run_time) {
147 if (is_enabled_) {
Sami 2016/09/22 12:34:07 nit: no {}
altimin 2016/09/22 13:45:59 Done.
148 current_budget_level_ -= task_run_time;
149 }
150 }
151
152 const char* TaskQueueThrottler::TimeBudgetPool::Name() const {
153 return name_;
154 }
155
156 void TaskQueueThrottler::TimeBudgetPool::AsValueInto(
157 base::trace_event::TracedValue* state,
158 base::TimeTicks now) const {
159 state->BeginDictionary();
160
161 state->SetString("name", name_);
162 state->SetDouble("time_budget", cpu_percentage_);
163 state->SetDouble("time_budget_level_in_seconds",
164 current_budget_level_.InSecondsF());
165 state->SetDouble("last_checkpoint_seconds_ago",
166 (now - last_checkpoint_).InSecondsF());
caseq 2016/09/22 00:52:34 perhaps just dump last_checkpoint_?
altimin 2016/09/22 13:45:59 I believe that delta is easier to read than random
167
168 state->BeginArray("task_queues");
169 for (TaskQueue* queue : associated_task_queues_) {
170 state->AppendString(base::StringPrintf(
171 "%" PRIx64, static_cast<uint64_t>(reinterpret_cast<uintptr_t>(queue))));
172 }
173 state->EndArray();
174
175 state->EndDictionary();
176 }
177
178 void TaskQueueThrottler::TimeBudgetPool::Advance(base::TimeTicks now) {
179 if (now > last_checkpoint_) {
180 if (is_enabled_) {
181 current_budget_level_ = std::min(
182 current_budget_level_ + cpu_percentage_ * (now - last_checkpoint_),
183 max_budget_level_);
184 }
185 last_checkpoint_ = now;
186 }
187 }
188
189 void TaskQueueThrottler::TimeBudgetPool::BlockQueues(base::TimeTicks now) {
190 for (TaskQueue* queue : associated_task_queues_) {
191 if (!task_queue_throttler_->IsThrottled(queue))
192 continue;
193
194 queue->SetQueueEnabled(false);
195 task_queue_throttler_->MaybeSchedulePumpQueue(FROM_HERE, now, queue,
196 base::nullopt);
197 }
198 }
199
200 TaskQueueThrottler::TaskQueueThrottler(
201 RendererSchedulerImpl* renderer_scheduler,
202 const char* tracing_category)
caseq 2016/09/22 01:25:26 Ouch -- just noticed this! Trace macros expect sta
Sami 2016/09/22 12:34:07 Good point. We might want to fix this separately s
altimin 2016/09/22 13:45:59 I'll add a TODO and address it in a next patch.
203 : task_runner_(renderer_scheduler->ControlTaskRunner()),
204 renderer_scheduler_(renderer_scheduler),
205 tick_clock_(renderer_scheduler->tick_clock()),
206 tracing_category_(tracing_category),
207 time_domain_(new ThrottledTimeDomain(this, tracing_category)),
208 virtual_time_(false),
209 weak_factory_(this) {
210 pump_throttled_tasks_closure_.Reset(base::Bind(
211 &TaskQueueThrottler::PumpThrottledTasks, weak_factory_.GetWeakPtr()));
212 forward_immediate_work_callback_ =
213 base::Bind(&TaskQueueThrottler::OnTimeDomainHasImmediateWork,
214 weak_factory_.GetWeakPtr());
215
216 renderer_scheduler_->RegisterTimeDomain(time_domain_.get());
217 }
218
219 TaskQueueThrottler::~TaskQueueThrottler() {
220 // It's possible for queues to be still throttled, so we need to tidy up
221 // before unregistering the time domain.
222 for (const TaskQueueMap::value_type& map_entry : queue_details_) {
223 if (map_entry.second.IsThrottled()) {
224 TaskQueue* task_queue = map_entry.first;
225 task_queue->SetTimeDomain(renderer_scheduler_->real_time_domain());
226 task_queue->RemoveFence();
227 }
228 }
229
230 renderer_scheduler_->UnregisterTimeDomain(time_domain_.get());
231 }
232
233 void TaskQueueThrottler::SetQueueEnabled(TaskQueue* task_queue, bool enabled) {
234 TaskQueueMap::iterator find_it = queue_details_.find(task_queue);
235
236 if (find_it == queue_details_.end()) {
237 task_queue->SetQueueEnabled(enabled);
238 return;
239 }
240
241 find_it->second.enabled = enabled;
242
243 if (!find_it->second.IsThrottled())
244 return;
245
246 // We don't enable the queue here because it's throttled and there might be
247 // tasks in it's work queue that would execute immediatly rather than after
248 // PumpThrottledTasks runs.
249 if (!enabled) {
250 task_queue->SetQueueEnabled(false);
251 MaybeSchedulePumpQueue(FROM_HERE, tick_clock_->NowTicks(), task_queue,
252 base::nullopt);
253 }
254 }
255
256 void TaskQueueThrottler::IncreaseThrottleRefCount(TaskQueue* task_queue) {
257 DCHECK_NE(task_queue, task_runner_.get());
258
259 if (virtual_time_)
260 return;
261
262 std::pair<TaskQueueMap::iterator, bool> insert_result =
263 queue_details_.insert(std::make_pair(task_queue, Metadata()));
264
265 if (!insert_result.first->second.IsThrottled()) {
266 // The insert was successful so we need to throttle the queue.
267 insert_result.first->second.enabled = task_queue->IsQueueEnabled();
268
269 task_queue->SetTimeDomain(time_domain_.get());
270 task_queue->RemoveFence();
271 task_queue->SetQueueEnabled(false);
272
273 if (!task_queue->IsEmpty()) {
274 if (task_queue->HasPendingImmediateWork()) {
275 OnTimeDomainHasImmediateWork(task_queue);
276 } else {
277 OnTimeDomainHasDelayedWork(task_queue);
278 }
279 }
280
281 TRACE_EVENT1(tracing_category_, "TaskQueueThrottler_TaskQueueThrottled",
282 "task_queue", task_queue);
283 }
284
285 insert_result.first->second.throttling_ref_count++;
286 }
287
288 void TaskQueueThrottler::DecreaseThrottleRefCount(TaskQueue* task_queue) {
289 if (virtual_time_)
290 return;
291
292 TaskQueueMap::iterator iter = queue_details_.find(task_queue);
293
294 if (iter != queue_details_.end() &&
295 --iter->second.throttling_ref_count == 0) {
296 bool enabled = iter->second.enabled;
297
298 MaybeDeleteQueueMetadata(iter);
299
300 task_queue->SetTimeDomain(renderer_scheduler_->real_time_domain());
301 task_queue->RemoveFence();
302 task_queue->SetQueueEnabled(enabled);
303
304 TRACE_EVENT1(tracing_category_, "TaskQueueThrottler_TaskQueueUntrottled",
305 "task_queue", task_queue);
306 }
307 }
308
309 bool TaskQueueThrottler::IsThrottled(TaskQueue* task_queue) const {
310 auto find_it = queue_details_.find(task_queue);
311 if (find_it == queue_details_.end())
312 return false;
313 return find_it->second.IsThrottled();
314 }
315
316 void TaskQueueThrottler::UnregisterTaskQueue(TaskQueue* task_queue) {
317 LazyNow lazy_now(tick_clock_);
318 auto find_it = queue_details_.find(task_queue);
319
320 if (find_it == queue_details_.end())
321 return;
322
323 if (find_it->second.time_budget_pool)
324 find_it->second.time_budget_pool->RemoveQueue(lazy_now.Now(), task_queue);
325
326 queue_details_.erase(find_it);
327 }
328
329 void TaskQueueThrottler::OnTimeDomainHasImmediateWork(TaskQueue* queue) {
330 // Forward to the main thread if called from another thread
331 if (!task_runner_->RunsTasksOnCurrentThread()) {
332 task_runner_->PostTask(FROM_HERE,
333 base::Bind(forward_immediate_work_callback_, queue));
334 return;
335 }
336 TRACE_EVENT0(tracing_category_,
337 "TaskQueueThrottler::OnTimeDomainHasImmediateWork");
338
339 base::TimeTicks now = tick_clock_->NowTicks();
340 base::TimeTicks next_allowed_run_time = GetNextAllowedRunTime(now, queue);
341 MaybeSchedulePumpThrottledTasks(FROM_HERE, now, next_allowed_run_time);
342 }
343
344 void TaskQueueThrottler::OnTimeDomainHasDelayedWork(TaskQueue* queue) {
345 TRACE_EVENT0(tracing_category_,
346 "TaskQueueThrottler::OnTimeDomainHasDelayedWork");
347 base::TimeTicks now = tick_clock_->NowTicks();
348 LazyNow lazy_now(now);
349
350 base::Optional<base::TimeTicks> next_scheduled_delayed_task =
351 NextTaskRunTime(&lazy_now, queue);
352 DCHECK(next_scheduled_delayed_task);
353 MaybeSchedulePumpThrottledTasks(FROM_HERE, now,
354 next_scheduled_delayed_task.value());
355 }
356
357 namespace {
358
359 template <class T>
Sami 2016/09/22 12:34:08 Could you move these into the anonymous namespace
altimin 2016/09/22 13:45:59 Done.
360 T Min(const base::Optional<T>& optional, const T& value) {
361 if (!optional) {
362 return value;
363 }
364 return std::min(optional.value(), value);
365 }
366
367 template <class T>
368 base::Optional<T> Min(const base::Optional<T>& a, const base::Optional<T>& b) {
369 if (!b)
370 return a;
371 if (!a)
372 return b;
373 return std::min(a.value(), b.value());
374 }
375
376 template <class T>
377 T Max(const base::Optional<T>& optional, const T& value) {
378 if (!optional)
379 return value;
380 return std::max(optional.value(), value);
381 }
382
383 template <class T>
384 base::Optional<T> Max(const base::Optional<T>& a, const base::Optional<T>& b) {
385 if (!b)
386 return a;
387 if (!a)
388 return b;
389 return std::max(a.value(), b.value());
390 }
391
392 } // namespace
393
394 void TaskQueueThrottler::PumpThrottledTasks() {
395 TRACE_EVENT0(tracing_category_, "TaskQueueThrottler::PumpThrottledTasks");
396 pending_pump_throttled_tasks_runtime_.reset();
397
398 LazyNow lazy_now(tick_clock_);
399 base::Optional<base::TimeTicks> next_scheduled_delayed_task;
400
401 for (const TaskQueueMap::value_type& map_entry : queue_details_) {
402 TaskQueue* task_queue = map_entry.first;
403 if (!map_entry.second.enabled || task_queue->IsEmpty() ||
404 !map_entry.second.IsThrottled())
405 continue;
406
407 // Don't enable queues whose budget pool doesn't allow them to run now.
408 base::TimeTicks next_allowed_run_time =
409 GetNextAllowedRunTime(lazy_now.Now(), task_queue);
410 base::Optional<base::TimeTicks> next_desired_run_time =
411 NextTaskRunTime(&lazy_now, task_queue);
412
413 if (next_desired_run_time &&
414 next_allowed_run_time > next_desired_run_time.value()) {
415 TRACE_EVENT1(
416 tracing_category_,
417 "TaskQueueThrottler::PumpThrottledTasks_ExpensiveTaskThrottled",
418 "throttle_time_in_seconds",
419 (next_allowed_run_time - next_desired_run_time.value()).InSecondsF());
420
421 // Schedule a pump for queue which was disabled because of time budget.
422 next_scheduled_delayed_task =
423 Min(next_scheduled_delayed_task, next_allowed_run_time);
424
425 continue;
426 }
427
428 next_scheduled_delayed_task =
429 Min(next_scheduled_delayed_task, task_queue->GetNextScheduledWakeUp());
430
431 if (next_allowed_run_time > lazy_now.Now())
432 continue;
433
434 task_queue->SetQueueEnabled(true);
435 task_queue->InsertFence();
436 }
437
438 // Maybe schedule a call to TaskQueueThrottler::PumpThrottledTasks if there is
439 // a pending delayed task or a throttled task ready to run.
440 // NOTE: posting a non-delayed task in the future will result in
441 // TaskQueueThrottler::OnTimeDomainHasImmediateWork being called.
442 if (next_scheduled_delayed_task) {
443 MaybeSchedulePumpThrottledTasks(FROM_HERE, lazy_now.Now(),
444 *next_scheduled_delayed_task);
445 }
446 }
447
448 /* static */
449 base::TimeTicks TaskQueueThrottler::AlignedThrottledRunTime(
450 base::TimeTicks unthrottled_runtime) {
451 const base::TimeDelta one_second = base::TimeDelta::FromSeconds(1);
452 return unthrottled_runtime + one_second -
453 ((unthrottled_runtime - base::TimeTicks()) % one_second);
454 }
455
456 void TaskQueueThrottler::MaybeSchedulePumpThrottledTasks(
457 const tracked_objects::Location& from_here,
458 base::TimeTicks now,
459 base::TimeTicks unaligned_runtime) {
460 if (virtual_time_)
461 return;
462
463 base::TimeTicks runtime =
464 std::max(now, AlignedThrottledRunTime(unaligned_runtime));
465
466 // If there is a pending call to PumpThrottledTasks and it's sooner than
467 // |runtime| then return.
468 if (pending_pump_throttled_tasks_runtime_ &&
469 runtime >= pending_pump_throttled_tasks_runtime_.value()) {
470 return;
471 }
472
473 pending_pump_throttled_tasks_runtime_ = runtime;
474
475 pump_throttled_tasks_closure_.Cancel();
476
477 base::TimeDelta delay = pending_pump_throttled_tasks_runtime_.value() - now;
478 TRACE_EVENT1(tracing_category_,
479 "TaskQueueThrottler::MaybeSchedulePumpThrottledTasks",
480 "delay_till_next_pump_ms", delay.InMilliseconds());
481 task_runner_->PostDelayedTask(
482 from_here, pump_throttled_tasks_closure_.callback(), delay);
483 }
484
485 void TaskQueueThrottler::EnableVirtualTime() {
486 virtual_time_ = true;
487
488 pump_throttled_tasks_closure_.Cancel();
489
490 for (auto it = queue_details_.begin(); it != queue_details_.end();) {
491 TaskQueue* task_queue = it->first;
492 bool enabled = it->second.enabled;
493
494 if (!it->second.time_budget_pool) {
495 it = queue_details_.erase(it);
496 } else {
497 // Fall back to default values.
498 it->second.throttling_ref_count = 0;
499 it->second.enabled = false;
500 it++;
501 }
502
503 task_queue->SetTimeDomain(renderer_scheduler_->GetVirtualTimeDomain());
504 task_queue->RemoveFence();
505 task_queue->SetQueueEnabled(enabled);
506 }
507 }
508
509 TaskQueueThrottler::TimeBudgetPool* TaskQueueThrottler::CreateTimeBudgetPool(
510 const char* name) {
511 TimeBudgetPool* time_budget_pool =
512 new TimeBudgetPool(name, this, tick_clock_->NowTicks());
513 time_budget_pools_[time_budget_pool] = base::WrapUnique(time_budget_pool);
514 return time_budget_pool;
515 }
516
517 void TaskQueueThrottler::OnTaskRunTimeReported(TaskQueue* task_queue,
518 base::TimeTicks start_time,
519 base::TimeTicks end_time) {
520 if (!IsThrottled(task_queue))
521 return;
522
523 TimeBudgetPool* time_budget_pool = GetTimeBudgetPoolForQueue(task_queue);
524 if (!time_budget_pool)
525 return;
526
527 time_budget_pool->RecordTaskRunTime(end_time - start_time);
528 if (!time_budget_pool->HasEnoughBudgetToRun(end_time))
529 time_budget_pool->BlockQueues(end_time);
530 }
531
532 void TaskQueueThrottler::AsValueInto(base::trace_event::TracedValue* state,
533 base::TimeTicks now) const {
534 if (pending_pump_throttled_tasks_runtime_) {
535 state->SetDouble(
536 "next_throttled_tasks_pump_in_seconds",
537 (pending_pump_throttled_tasks_runtime_.value() - now).InSecondsF());
538 }
539
540 state->BeginDictionary("time_budget_pools");
541
542 for (const auto& map_entry : time_budget_pools_) {
543 TaskQueueThrottler::TimeBudgetPool* pool = map_entry.first;
544 pool->AsValueInto(state, now);
545 }
546
547 state->EndDictionary();
548 }
549
550 TaskQueueThrottler::TimeBudgetPool*
551 TaskQueueThrottler::GetTimeBudgetPoolForQueue(TaskQueue* queue) {
552 auto find_it = queue_details_.find(queue);
553 if (find_it == queue_details_.end()) {
554 return nullptr;
555 } else {
caseq 2016/09/22 01:25:26 style: we don't use else after return
altimin 2016/09/22 13:45:59 Done.
556 return find_it->second.time_budget_pool;
557 }
558 }
559
560 void TaskQueueThrottler::MaybeSchedulePumpQueue(
561 const tracked_objects::Location& from_here,
562 base::TimeTicks now,
563 TaskQueue* queue,
564 base::Optional<base::TimeTicks> next_possible_run_time) {
565 LazyNow lazy_now(now);
566 base::Optional<base::TimeTicks> next_run_time =
567 Max(NextTaskRunTime(&lazy_now, queue), next_possible_run_time);
568
569 if (next_run_time) {
570 MaybeSchedulePumpThrottledTasks(from_here, now, next_run_time.value());
571 }
572 }
573
574 base::TimeTicks TaskQueueThrottler::GetNextAllowedRunTime(base::TimeTicks now,
575 TaskQueue* queue) {
576 TimeBudgetPool* time_budget_pool = GetTimeBudgetPoolForQueue(queue);
577 if (!time_budget_pool) {
578 return now;
579 } else {
caseq 2016/09/22 01:25:26 ditto.
altimin 2016/09/22 13:45:59 Done.
580 return std::max(now, time_budget_pool->GetNextAllowedRunTime());
581 }
582 }
583
584 void TaskQueueThrottler::MaybeDeleteQueueMetadata(TaskQueueMap::iterator it) {
585 if (!it->second.IsThrottled() && !it->second.time_budget_pool)
586 queue_details_.erase(it);
587 }
588
589 } // namespace scheduler
590 } // namespace blink
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698