|  | // 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 "base/logging.h" | 
|  | #include "base/stl_util.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "base/time/default_tick_clock.h" | 
|  |  | 
|  | namespace net { | 
|  |  | 
|  | const size_t NetworkThrottleManagerImpl::kActiveRequestThrottlingLimit = 2; | 
|  | const int NetworkThrottleManagerImpl::kMedianLifetimeMultiple = 5; | 
|  |  | 
|  | // Initial estimate based on the median in the | 
|  | // Net.RequestTime2.Success histogram, excluding cached results by eye. | 
|  | const int NetworkThrottleManagerImpl::kInitialMedianInMs = 400; | 
|  |  | 
|  | // Set timers slightly further into the future than they need to be set, so | 
|  | // that the algorithm isn't vulnerable to timer round off errors triggering | 
|  | // the callback before the throttle would be considered aged out of the set. | 
|  | // Set to 17 to hanlde systems with |!base::TimeTicks::IsHighResolution()|. | 
|  | // Note that even if the timer goes off before it should, all that should cost | 
|  | // is a second task; this class does not rely on timer accuracy for its | 
|  | // correctness. | 
|  | const int kTimerFudgeInMs = 17; | 
|  |  | 
|  | 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 class 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 ThrottleListQueuePointer = | 
|  | NetworkThrottleManagerImpl::ThrottleList::iterator; | 
|  |  | 
|  | // Caller must arrange that |*delegate| and |*manager| outlive | 
|  | // the ThrottleImpl class. | 
|  | ThrottleImpl(bool blocked, | 
|  | RequestPriority priority, | 
|  | ThrottleDelegate* delegate, | 
|  | NetworkThrottleManagerImpl* manager, | 
|  | ThrottleListQueuePointer queue_pointer); | 
|  |  | 
|  | ~ThrottleImpl() override; | 
|  |  | 
|  | // Throttle: | 
|  | bool IsBlocked() const override; | 
|  | RequestPriority Priority() const override; | 
|  | void SetPriority(RequestPriority priority) override; | 
|  |  | 
|  | State state() const { return state_; } | 
|  |  | 
|  | ThrottleListQueuePointer queue_pointer() const { return queue_pointer_; } | 
|  | void set_queue_pointer(const ThrottleListQueuePointer& pointer) { | 
|  | if (state_ != State::AGED) | 
|  | DCHECK_EQ(this, *pointer); | 
|  | queue_pointer_ = pointer; | 
|  | } | 
|  |  | 
|  | void set_start_time(base::TimeTicks start_time) { start_time_ = start_time; } | 
|  | base::TimeTicks start_time() const { return start_time_; } | 
|  |  | 
|  | // Change the throttle's state to AGED.  The previous | 
|  | // state must be OUTSTANDING. | 
|  | void SetAged(); | 
|  |  | 
|  | // 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_; | 
|  |  | 
|  | // To allow deletion from the blocked queue (when the throttle is in the | 
|  | // blocked queue). | 
|  | ThrottleListQueuePointer queue_pointer_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(ThrottleImpl); | 
|  | }; | 
|  |  | 
|  | NetworkThrottleManagerImpl::ThrottleImpl::ThrottleImpl( | 
|  | bool blocked, | 
|  | RequestPriority priority, | 
|  | NetworkThrottleManager::ThrottleDelegate* delegate, | 
|  | NetworkThrottleManagerImpl* manager, | 
|  | ThrottleListQueuePointer queue_pointer) | 
|  | : state_(blocked ? State::BLOCKED : State::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_ == 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(State::OUTSTANDING, state_); | 
|  | state_ = State::AGED; | 
|  | } | 
|  |  | 
|  | void NetworkThrottleManagerImpl::ThrottleImpl::NotifyUnblocked() { | 
|  | // This method should only be called once, and only if the | 
|  | // current state is blocked. | 
|  | DCHECK_EQ(State::BLOCKED, state_); | 
|  | state_ = State::OUTSTANDING; | 
|  | delegate_->OnThrottleUnblocked(this); | 
|  | } | 
|  |  | 
|  | NetworkThrottleManagerImpl::NetworkThrottleManagerImpl() | 
|  | : lifetime_median_estimate_(PercentileEstimator::kMedianPercentile, | 
|  | kInitialMedianInMs), | 
|  | outstanding_recomputation_timer_(std::make_unique<base::OneShotTimer>()), | 
|  | tick_clock_(base::DefaultTickClock::GetInstance()), | 
|  | weak_ptr_factory_(this) {} | 
|  |  | 
|  | NetworkThrottleManagerImpl::~NetworkThrottleManagerImpl() = default; | 
|  |  | 
|  | 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())); | 
|  |  | 
|  | ThrottleList& insert_list(blocked ? blocked_throttles_ | 
|  | : outstanding_throttles_); | 
|  |  | 
|  | throttle->set_queue_pointer( | 
|  | insert_list.insert(insert_list.end(), throttle.get())); | 
|  |  | 
|  | // In case oustanding_throttles_ was empty, set up timer. | 
|  | if (!blocked) | 
|  | RecomputeOutstanding(); | 
|  |  | 
|  | return std::move(throttle); | 
|  | } | 
|  |  | 
|  | void NetworkThrottleManagerImpl::SetTickClockForTesting( | 
|  | const base::TickClock* tick_clock) { | 
|  | tick_clock_ = tick_clock; | 
|  | DCHECK(!outstanding_recomputation_timer_->IsRunning()); | 
|  | outstanding_recomputation_timer_ = | 
|  | std::make_unique<base::OneShotTimer>(tick_clock_); | 
|  | } | 
|  |  | 
|  | bool NetworkThrottleManagerImpl::ConditionallyTriggerTimerForTesting() { | 
|  | if (!outstanding_recomputation_timer_->IsRunning() || | 
|  | (tick_clock_->NowTicks() < | 
|  | outstanding_recomputation_timer_->desired_run_time())) { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | base::Closure timer_callback(outstanding_recomputation_timer_->user_task()); | 
|  | outstanding_recomputation_timer_->Stop(); | 
|  | timer_callback.Run(); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | 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, which can only happen on a transition from blocked | 
|  | // (implies THROTTLED) to non-THROTTLED. | 
|  | if (throttle->IsBlocked() && new_priority != THROTTLED) { | 
|  | // May result in re-entrant calls into this class. | 
|  | UnblockThrottle(throttle); | 
|  | } | 
|  | } | 
|  |  | 
|  | void NetworkThrottleManagerImpl::OnThrottleDestroyed(ThrottleImpl* throttle) { | 
|  | switch (throttle->state()) { | 
|  | case ThrottleImpl::State::BLOCKED: | 
|  | DCHECK(throttle->queue_pointer() != blocked_throttles_.end()); | 
|  | DCHECK_EQ(throttle, *(throttle->queue_pointer())); | 
|  | blocked_throttles_.erase(throttle->queue_pointer()); | 
|  | break; | 
|  | case ThrottleImpl::State::OUTSTANDING: | 
|  | DCHECK(throttle->queue_pointer() != outstanding_throttles_.end()); | 
|  | DCHECK_EQ(throttle, *(throttle->queue_pointer())); | 
|  | outstanding_throttles_.erase(throttle->queue_pointer()); | 
|  | FALLTHROUGH; | 
|  | case ThrottleImpl::State::AGED: | 
|  | DCHECK(!throttle->start_time().is_null()); | 
|  | lifetime_median_estimate_.AddSample( | 
|  | (tick_clock_->NowTicks() - throttle->start_time()) | 
|  | .InMillisecondsRoundedUp()); | 
|  | break; | 
|  | } | 
|  |  | 
|  | DCHECK(!base::ContainsValue(blocked_throttles_, throttle)); | 
|  | DCHECK(!base::ContainsValue(outstanding_throttles_, throttle)); | 
|  |  | 
|  | // Unblock the throttles if there's some chance there's a throttle to | 
|  | // unblock. | 
|  | if (outstanding_throttles_.size() < kActiveRequestThrottlingLimit && | 
|  | !blocked_throttles_.empty()) { | 
|  | // Via PostTask so there aren't upcalls from within destructors. | 
|  | base::ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, | 
|  | base::Bind(&NetworkThrottleManagerImpl::MaybeUnblockThrottles, | 
|  | weak_ptr_factory_.GetWeakPtr())); | 
|  | } | 
|  | } | 
|  |  | 
|  | void NetworkThrottleManagerImpl::RecomputeOutstanding() { | 
|  | // Remove all throttles that have aged out of the outstanding set. | 
|  | base::TimeTicks now(tick_clock_->NowTicks()); | 
|  | base::TimeDelta age_horizon(base::TimeDelta::FromMilliseconds(( | 
|  | kMedianLifetimeMultiple * lifetime_median_estimate_.current_estimate()))); | 
|  | while (!outstanding_throttles_.empty()) { | 
|  | ThrottleImpl* throttle = *outstanding_throttles_.begin(); | 
|  | if (throttle->start_time() + age_horizon >= now) | 
|  | break; | 
|  |  | 
|  | outstanding_throttles_.erase(outstanding_throttles_.begin()); | 
|  | throttle->SetAged(); | 
|  | throttle->set_queue_pointer(outstanding_throttles_.end()); | 
|  | } | 
|  |  | 
|  | if (outstanding_throttles_.empty()) | 
|  | return; | 
|  |  | 
|  | // If the timer is already running, be conservative and leave it alone; | 
|  | // the time for which it would be set will only be later than when it's | 
|  | // currently set. | 
|  | // This addresses, e.g., situations where a RecomputeOutstanding() races | 
|  | // with a running timer which would unblock blocked throttles. | 
|  | if (outstanding_recomputation_timer_->IsRunning()) | 
|  | return; | 
|  |  | 
|  | ThrottleImpl* first_throttle(*outstanding_throttles_.begin()); | 
|  | DCHECK_GE(first_throttle->start_time() + age_horizon, now); | 
|  |  | 
|  | outstanding_recomputation_timer_->Start( | 
|  | FROM_HERE, | 
|  | ((first_throttle->start_time() + age_horizon) - now + | 
|  | base::TimeDelta::FromMilliseconds(kTimerFudgeInMs)), | 
|  | // 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_start_time(tick_clock_->NowTicks()); | 
|  | throttle->set_queue_pointer( | 
|  | outstanding_throttles_.insert(outstanding_throttles_.end(), throttle)); | 
|  |  | 
|  | // Called in case |*throttle| was added to a null set. | 
|  | RecomputeOutstanding(); | 
|  |  | 
|  | // 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 |