Chromium Code Reviews| Index: net/base/network_throttle_manager_impl.cc |
| diff --git a/net/base/network_throttle_manager_impl.cc b/net/base/network_throttle_manager_impl.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..3d7182f00be6f5ae6eabfdf17032925716e60cfa |
| --- /dev/null |
| +++ b/net/base/network_throttle_manager_impl.cc |
| @@ -0,0 +1,319 @@ |
| +// 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 "net/base/network_throttle_manager_impl.h" |
| + |
| +#include <algorithm> |
| + |
| +#include "base/logging.h" |
| +#include "base/message_loop/message_loop.h" |
| +#include "base/time/default_tick_clock.h" |
| + |
| +namespace net { |
| + |
| +const size_t NetworkThrottleManagerImpl::kActiveRequestThrottlingLimit = 2; |
| +const int NetworkThrottleManagerImpl::kMedianLifetimeMultiple = 5; |
| +const int NetworkThrottleManagerImpl::kInitialMedianInMs = 100; |
| + |
| +class NetworkThrottleManagerImpl::ThrottleImpl |
| + : public NetworkThrottleManager::Throttle { |
| + public: |
| + // Allowed state transitions are BLOCKED -> OUTSTANDING -> AGED. |
| + // Throttles may be created in the BLOCKED or OUTSTANDING states. |
| + enum State { |
| + // Not allowed to proceed by manager. |
| + BLOCKED, |
| + |
| + // Allowed to proceed, counts as an "outstanding" request for |
| + // manager accounting purposes. |
| + OUTSTANDING, |
| + |
| + // Old enough to not count as "outstanding" anymore for |
| + // manager accounting purposes. |
| + AGED |
| + }; |
| + |
| + using QueuePointer = NetworkThrottleManagerImpl::ThrottleList::iterator; |
| + |
| + // Caller must arrange that |*delegate| and |*manager| outlive |
| + // the ThrottleImpl class. |
| + ThrottleImpl(bool blocked, |
| + RequestPriority priority, |
| + ThrottleDelegate* delegate, |
| + NetworkThrottleManagerImpl* manager, |
| + QueuePointer queue_pointer); |
| + |
| + ~ThrottleImpl() override; |
| + |
| + // Throttle: |
| + bool IsBlocked() const override; |
| + RequestPriority Priority() const override; |
| + void SetPriority(RequestPriority priority) override; |
| + |
| + // Change the throttle's state to AGED. The previous |
| + // state must be OUTSTANDING. |
| + void SetAged(); |
| + State state() const { return state_; } |
| + RequestPriority priority() const { return priority_; } |
| + QueuePointer queue_pointer() const { return queue_pointer_; } |
| + void set_queue_pointer(const QueuePointer& pointer) { |
| + queue_pointer_ = pointer; |
| + } |
| + base::TimeTicks start_time() const { return start_time_; } |
| + void set_start_time(base::TimeTicks start_time) { start_time_ = start_time; } |
|
Charlie Harrison
2016/09/19 16:35:35
Could this logic be moved to NotifyUnblocked so we
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
So I like this idea in terms of layering (the Thro
|
| + |
| + // Note that this call calls the delegate, and hence |
| + // may result in re-entrant calls into the manager or |
| + // ThrottleImpl. The manager should not rely on |
| + // any state other than its own existence being persistent |
| + // across this call. |
| + void NotifyUnblocked(); |
| + |
| + private: |
| + State state_; |
| + RequestPriority priority_; |
| + ThrottleDelegate* const delegate_; |
| + NetworkThrottleManagerImpl* const manager_; |
| + |
| + base::TimeTicks start_time_; |
| + |
| + // For deletion from owning queue. |
| + QueuePointer queue_pointer_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(ThrottleImpl); |
| +}; |
| + |
| +NetworkThrottleManagerImpl::ThrottleImpl::ThrottleImpl( |
| + bool blocked, |
| + RequestPriority priority, |
| + NetworkThrottleManager::ThrottleDelegate* delegate, |
| + NetworkThrottleManagerImpl* manager, |
| + QueuePointer queue_pointer) |
| + : state_(blocked ? BLOCKED : OUTSTANDING), |
| + priority_(priority), |
| + delegate_(delegate), |
| + manager_(manager), |
| + queue_pointer_(queue_pointer) { |
| + DCHECK(delegate); |
| + if (!blocked) |
| + start_time_ = manager->tick_clock_->NowTicks(); |
| +} |
| + |
| +NetworkThrottleManagerImpl::ThrottleImpl::~ThrottleImpl() { |
| + manager_->OnThrottleDestroyed(this); |
| +} |
| + |
| +bool NetworkThrottleManagerImpl::ThrottleImpl::IsBlocked() const { |
| + return state_ == BLOCKED; |
| +} |
| + |
| +RequestPriority NetworkThrottleManagerImpl::ThrottleImpl::Priority() const { |
| + return priority_; |
| +} |
| + |
| +void NetworkThrottleManagerImpl::ThrottleImpl::SetPriority( |
| + RequestPriority new_priority) { |
| + RequestPriority old_priority(priority_); |
| + if (old_priority == new_priority) |
| + return; |
| + priority_ = new_priority; |
| + manager_->OnThrottlePriorityChanged(this, old_priority, new_priority); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::ThrottleImpl::SetAged() { |
| + DCHECK_EQ(OUTSTANDING, state_); |
| + state_ = AGED; |
| +} |
| + |
| +void NetworkThrottleManagerImpl::ThrottleImpl::NotifyUnblocked() { |
| + // This methods should only be called once, and only if the |
| + // current state is blocked. |
| + DCHECK_EQ(BLOCKED, state_); |
| + state_ = OUTSTANDING; |
| + delegate_->OnThrottleStateChanged(this); |
| +} |
| + |
| +NetworkThrottleManagerImpl::NetworkThrottleManagerImpl() |
| + : lifetime_median_estimate_(QuantileEstimator::kMedianQuantile, |
| + kInitialMedianInMs), |
| + outstanding_recomputation_timer_(false /* retain_user_task */, |
| + false /* is_repeating */), |
| + outstanding_throttles_( |
| + &NetworkThrottleManagerImpl::CreationTimeSetCompare), |
| + tick_clock_(new base::DefaultTickClock()), |
| + weak_ptr_factory_(this) { |
| + outstanding_recomputation_timer_.SetTaskRunner( |
| + base::MessageLoop::current()->task_runner()); |
| +} |
| + |
| +NetworkThrottleManagerImpl::~NetworkThrottleManagerImpl() {} |
| + |
| +std::unique_ptr<NetworkThrottleManager::Throttle> |
| +NetworkThrottleManagerImpl::CreateThrottle( |
| + NetworkThrottleManager::ThrottleDelegate* delegate, |
| + RequestPriority priority, |
| + bool ignore_limits) { |
| + bool blocked = |
| + (!ignore_limits && priority == THROTTLED && |
| + outstanding_throttles_.size() >= kActiveRequestThrottlingLimit); |
| + |
| + std::unique_ptr<NetworkThrottleManagerImpl::ThrottleImpl> throttle( |
| + new ThrottleImpl(blocked, priority, delegate, this, |
| + blocked_throttles_.end())); |
| + |
| + if (blocked) { |
| + throttle->set_queue_pointer( |
| + blocked_throttles_.insert(blocked_throttles_.end(), throttle.get())); |
| + } else { |
| + outstanding_throttles_.insert(throttle.get()); |
| + } |
| + |
| + RecomputeOutstanding(); |
| + |
| + return std::move(throttle); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::SetTickClockForTesting( |
| + std::unique_ptr<base::TickClock> tick_clock) { |
| + tick_clock_ = std::move(tick_clock); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::ConditionallyTriggerTimerForTesting() { |
| + // Relies on |!retain_user_task| in timer constructor. |
| + if (!outstanding_recomputation_timer_.user_task().is_null() && |
| + (tick_clock_->NowTicks() > |
| + outstanding_recomputation_timer_.desired_run_time())) { |
| + base::Closure timer_callback(outstanding_recomputation_timer_.user_task()); |
| + outstanding_recomputation_timer_.Stop(); |
| + timer_callback.Run(); |
| + } |
| +} |
| + |
|
Charlie Harrison
2016/09/19 16:35:36
nit: // static
Randy Smith (Not in Mondays)
2016/09/22 21:52:27
Done.
|
| +bool NetworkThrottleManagerImpl::CreationTimeSetCompare( |
| + ThrottleImpl* throttle1, |
| + ThrottleImpl* throttle2) { |
| + // No throttle should be in the CreationTimeOrderedSet with a null start |
| + // time. |
| + DCHECK_NE(throttle1->start_time(), base::TimeTicks()); |
| + DCHECK_NE(throttle2->start_time(), base::TimeTicks()); |
| + return (throttle1->start_time() < throttle2->start_time() || |
| + // So different throttles don't look equal to the comparison |
| + // function. |
| + (throttle1->start_time() == throttle2->start_time() && |
| + throttle1 < throttle2)); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::OnThrottlePriorityChanged( |
| + NetworkThrottleManagerImpl::ThrottleImpl* throttle, |
| + RequestPriority old_priority, |
| + RequestPriority new_priority) { |
| + // The only case requiring a state change is if the priority change |
| + // implies unblocking. |
| + if (throttle->IsBlocked() && old_priority == THROTTLED && |
|
Charlie Harrison
2016/09/19 16:35:36
Doesn't throttle->IsBlocked() imply old_priority =
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
Yes, though it probably won't be true in the next
|
| + new_priority != THROTTLED) { |
| + // May result in re-entrant calls into this class. |
| + UnblockThrottle(throttle); |
| + } |
| + RecomputeOutstanding(); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::OnThrottleDestroyed(ThrottleImpl* throttle) { |
| + switch (throttle->state()) { |
| + case ThrottleImpl::BLOCKED: |
| + DCHECK(throttle->queue_pointer() != blocked_throttles_.end()); |
| + blocked_throttles_.erase(throttle->queue_pointer()); |
| + break; |
| + case ThrottleImpl::OUTSTANDING: { |
|
Charlie Harrison
2016/09/19 16:35:36
Why do you wrap this case in {} but not the others
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
I needed the scope of items_erased to be restricte
|
| + size_t items_erased = outstanding_throttles_.erase(throttle); |
| + DCHECK_EQ(1u, items_erased); |
| + } |
| + // Fall through |
| + case ThrottleImpl::AGED: |
| + DCHECK_NE(throttle->start_time(), base::TimeTicks()); |
| + lifetime_median_estimate_.AddSample( |
| + (tick_clock_->NowTicks() - throttle->start_time()) |
| + .InMillisecondsRoundedUp()); |
|
mmenke
2016/09/19 15:26:16
This includes cancellations and failures, as long
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
I'm inclined not to worry about it. My thought is
|
| + break; |
| + } |
| + |
| + // Via PostTask so there aren't upcalls from within destructors. |
| + base::MessageLoop::current()->task_runner()->PostTask( |
| + FROM_HERE, base::Bind(&NetworkThrottleManagerImpl::MaybeUnblockThrottles, |
| + weak_ptr_factory_.GetWeakPtr())); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::RecomputeOutstanding() { |
| + if (outstanding_throttles_.empty()) |
| + // No changes needed. |
|
mmenke
2016/09/19 15:26:16
nit: Use braces or move comment before the if.
Randy Smith (Not in Mondays)
2016/09/22 21:52:27
Looking at this in light of your comment, I think
|
| + return; |
| + |
| + // Remove all throttles that have aged out of the outstanding set. |
| + base::TimeTicks age_horizon( |
| + tick_clock_->NowTicks() - |
| + base::TimeDelta::FromMilliseconds( |
| + (kMedianLifetimeMultiple * |
| + lifetime_median_estimate_.current_estimate()))); |
| + while (!outstanding_throttles_.empty()) { |
|
mmenke
2016/09/19 15:26:16
Where's this declared? Am I missing something obv
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
I think so? It's a data member in the class decla
mmenke
2016/09/22 21:57:39
Weird, I searched the header for it multiple times
|
| + ThrottleImpl* throttle = *outstanding_throttles_.begin(); |
| + if (throttle->start_time() > age_horizon) |
| + break; |
| + |
| + outstanding_throttles_.erase(throttle); |
| + throttle->SetAged(); |
| + } |
| + |
| + // If the set's now empty, we don't need a timer. |
|
mmenke
2016/09/19 15:26:16
nit: --we
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
Ooops. Done.
|
| + if (outstanding_throttles_.empty()) { |
|
mmenke
2016/09/19 15:26:17
This seems too fragile to me - if we marked all ou
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
So I think of this in terms of routine responsibil
|
| + outstanding_recomputation_timer_.Stop(); |
| + return; |
| + } |
| + |
| + // The earliest we'll need to recompute is when the earliest timer |
|
mmenke
2016/09/19 15:26:16
--we
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
Done.
|
| + // ages out of the set. This isn't precise because the median estimate |
| + // changes over time, but this is a heuristic and doesn't need to be |
| + // precise. |
| + // TODO(rdsmith): Under that same "not precise" rubric, worthwhile avoiding |
| + // resetting the timer if it's "close"? |
| + ThrottleImpl* first_throttle = *outstanding_throttles_.begin(); |
| + base::TimeTicks new_timer_start = |
| + base::TimeTicks::Now() + (first_throttle->start_time() - age_horizon); |
|
Charlie Harrison
2016/09/19 16:35:35
Why not use tick_clock_->NowTicks() here?
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
So the basic answer is "We could, but it won't hel
|
| + if (!outstanding_recomputation_timer_.user_task().is_null() && |
| + outstanding_recomputation_timer_.desired_run_time() == new_timer_start) { |
|
Charlie Harrison
2016/09/19 16:35:35
When will these be equal? Seeing as you just calle
Randy Smith (Not in Mondays)
2016/09/22 21:52:26
Consider the case where we call RecomputeOustandin
Charlie Harrison
2016/09/23 15:11:12
Calling TimeTicks::Now() gets you halfway, but des
Randy Smith (Not in Mondays)
2016/09/23 22:16:49
Ok, after reading through the code carefully, I ag
|
| + return; |
| + } |
| + |
| + outstanding_recomputation_timer_.Start( |
| + FROM_HERE, first_throttle->start_time() - age_horizon, |
| + // Unretained use of |this| is safe because the timer is |
| + // owned by this object, and will be torn down if this object |
| + // is destroyed. |
| + base::Bind(&NetworkThrottleManagerImpl::MaybeUnblockThrottles, |
| + base::Unretained(this))); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::UnblockThrottle(ThrottleImpl* throttle) { |
| + DCHECK(throttle->IsBlocked()); |
| + |
| + blocked_throttles_.erase(throttle->queue_pointer()); |
| + throttle->set_queue_pointer(blocked_throttles_.end()); |
| + throttle->set_start_time(tick_clock_->NowTicks()); |
| + outstanding_throttles_.insert(throttle); |
| + |
| + // May result in re-entrant calls into this class. |
| + throttle->NotifyUnblocked(); |
| +} |
| + |
| +void NetworkThrottleManagerImpl::MaybeUnblockThrottles() { |
| + RecomputeOutstanding(); |
| + |
| + while (outstanding_throttles_.size() < kActiveRequestThrottlingLimit && |
| + !blocked_throttles_.empty()) { |
| + // NOTE: This call may result in reentrant calls into |
| + // NetworkThrottleManagerImpl; no state should be assumed to be |
| + // persistent across this call. |
| + UnblockThrottle(blocked_throttles_.front()); |
| + } |
| +} |
| + |
| +} // namespace net |