| // Copyright 2017 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 "third_party/blink/renderer/platform/loader/fetch/resource_load_scheduler.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/time/default_clock.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom-blink.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/renderer/platform/heap/persistent.h" |
| #include "third_party/blink/renderer/platform/instrumentation/histogram.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/console_logger.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/loading_behavior_observer.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher_properties.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/aggregated_metric_reporter.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/frame_status.h" |
| #include "third_party/blink/renderer/platform/wtf/functional.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| constexpr char kRendererSideResourceScheduler[] = |
| "RendererSideResourceScheduler"; |
| |
| // Used in the tight mode (see the header file for details). |
| constexpr size_t kTightLimitForRendererSideResourceScheduler = 2u; |
| // Used in the normal mode (see the header file for details). |
| constexpr size_t kLimitForRendererSideResourceScheduler = 1024u; |
| |
| constexpr char kTightLimitForRendererSideResourceSchedulerName[] = |
| "tight_limit"; |
| constexpr char kLimitForRendererSideResourceSchedulerName[] = "limit"; |
| |
| // Represents a resource load circumstance, e.g. from main frame vs sub-frames, |
| // or on throttled state vs on not-throttled state. |
| // Used to report histograms. Do not reorder or insert new items. |
| enum class ReportCircumstance { |
| kMainframeThrottled, |
| kMainframeNotThrottled, |
| kSubframeThrottled, |
| kSubframeNotThrottled, |
| // Append new items here. |
| kNumOfCircumstances, |
| }; |
| |
| uint32_t GetFieldTrialUint32Param(const char* trial_name, |
| const char* parameter_name, |
| uint32_t default_param) { |
| base::FieldTrialParams trial_params; |
| bool result = base::GetFieldTrialParams(trial_name, &trial_params); |
| if (!result) |
| return default_param; |
| |
| const auto& found = trial_params.find(parameter_name); |
| if (found == trial_params.end()) |
| return default_param; |
| |
| uint32_t param; |
| if (!base::StringToUint(found->second, ¶m)) |
| return default_param; |
| |
| return param; |
| } |
| |
| } // namespace |
| |
| constexpr ResourceLoadScheduler::ClientId |
| ResourceLoadScheduler::kInvalidClientId; |
| |
| ResourceLoadScheduler::ResourceLoadScheduler( |
| ThrottlingPolicy initial_throttling_policy, |
| ThrottleOptionOverride throttle_option_override, |
| const DetachableResourceFetcherProperties& resource_fetcher_properties, |
| FrameOrWorkerScheduler* frame_or_worker_scheduler, |
| DetachableConsoleLogger& console_logger, |
| LoadingBehaviorObserver* loading_behavior_observer) |
| : resource_fetcher_properties_(resource_fetcher_properties), |
| policy_(initial_throttling_policy), |
| outstanding_limit_for_throttled_frame_scheduler_( |
| resource_fetcher_properties_->GetOutstandingThrottledLimit()), |
| console_logger_(console_logger), |
| clock_(base::DefaultClock::GetInstance()), |
| throttle_option_override_(throttle_option_override), |
| loading_behavior_observer_(loading_behavior_observer) { |
| if (!frame_or_worker_scheduler) |
| return; |
| |
| normal_outstanding_limit_ = |
| GetFieldTrialUint32Param(kRendererSideResourceScheduler, |
| kLimitForRendererSideResourceSchedulerName, |
| kLimitForRendererSideResourceScheduler); |
| tight_outstanding_limit_ = |
| GetFieldTrialUint32Param(kRendererSideResourceScheduler, |
| kTightLimitForRendererSideResourceSchedulerName, |
| kTightLimitForRendererSideResourceScheduler); |
| |
| scheduler_observer_handle_ = frame_or_worker_scheduler->AddLifecycleObserver( |
| FrameScheduler::ObserverType::kLoader, |
| WTF::BindRepeating(&ResourceLoadScheduler::OnLifecycleStateChanged, |
| WrapWeakPersistent(this))); |
| } |
| |
| ResourceLoadScheduler::~ResourceLoadScheduler() = default; |
| |
| void ResourceLoadScheduler::Trace(Visitor* visitor) const { |
| visitor->Trace(pending_request_map_); |
| visitor->Trace(resource_fetcher_properties_); |
| visitor->Trace(console_logger_); |
| visitor->Trace(loading_behavior_observer_); |
| } |
| |
| void ResourceLoadScheduler::LoosenThrottlingPolicy() { |
| switch (policy_) { |
| case ThrottlingPolicy::kTight: |
| break; |
| case ThrottlingPolicy::kNormal: |
| return; |
| } |
| policy_ = ThrottlingPolicy::kNormal; |
| MaybeRun(); |
| } |
| |
| void ResourceLoadScheduler::Shutdown() { |
| // Do nothing if the feature is not enabled, or Shutdown() was already called. |
| if (is_shutdown_) |
| return; |
| is_shutdown_ = true; |
| |
| scheduler_observer_handle_.reset(); |
| } |
| |
| void ResourceLoadScheduler::Request(ResourceLoadSchedulerClient* client, |
| ThrottleOption option, |
| ResourceLoadPriority priority, |
| int intra_priority, |
| ResourceLoadScheduler::ClientId* id) { |
| *id = GenerateClientId(); |
| if (is_shutdown_) |
| return; |
| |
| if (option == ThrottleOption::kStoppable && |
| throttle_option_override_ == |
| ThrottleOptionOverride::kStoppableAsThrottleable) { |
| option = ThrottleOption::kThrottleable; |
| } |
| |
| // Check if the request can be throttled. |
| ClientIdWithPriority request_info(*id, priority, intra_priority); |
| if (!IsClientDelayable(option)) { |
| Run(*id, client, /*throttleable=*/false, priority); |
| return; |
| } |
| |
| DCHECK(ThrottleOption::kStoppable == option || |
| ThrottleOption::kThrottleable == option); |
| if (pending_requests_[option].empty()) |
| pending_queue_update_times_[option] = clock_->Now(); |
| pending_requests_[option].insert(request_info); |
| pending_request_map_.insert( |
| *id, MakeGarbageCollected<ClientInfo>(client, option, priority, |
| intra_priority)); |
| |
| // Remember the ClientId since MaybeRun() below may destruct the caller |
| // instance and |id| may be inaccessible after the call. |
| MaybeRun(); |
| } |
| |
| void ResourceLoadScheduler::SetPriority(ClientId client_id, |
| ResourceLoadPriority priority, |
| int intra_priority) { |
| auto client_it = pending_request_map_.find(client_id); |
| if (client_it == pending_request_map_.end()) |
| return; |
| |
| auto& throttle_option_queue = pending_requests_[client_it->value->option]; |
| |
| auto it = throttle_option_queue.find(ClientIdWithPriority( |
| client_id, client_it->value->priority, client_it->value->intra_priority)); |
| |
| DCHECK(it != throttle_option_queue.end()); |
| throttle_option_queue.erase(it); |
| |
| client_it->value->priority = priority; |
| client_it->value->intra_priority = intra_priority; |
| |
| throttle_option_queue.emplace(client_id, priority, intra_priority); |
| MaybeRun(); |
| } |
| |
| bool ResourceLoadScheduler::Release( |
| ResourceLoadScheduler::ClientId id, |
| ResourceLoadScheduler::ReleaseOption option, |
| const ResourceLoadScheduler::TrafficReportHints& hints) { |
| // Check kInvalidClientId that can not be passed to the HashSet. |
| if (id == kInvalidClientId) |
| return false; |
| |
| auto running_request = running_requests_.find(id); |
| if (running_request != running_requests_.end()) { |
| if (running_request->value >= PriorityImportanceThreshold()) { |
| in_flight_important_requests_--; |
| DCHECK_GE(in_flight_important_requests_, 0); |
| } |
| |
| running_requests_.erase(id); |
| running_throttleable_requests_.erase(id); |
| |
| if (option == ReleaseOption::kReleaseAndSchedule) |
| MaybeRun(); |
| return true; |
| } |
| |
| // The client may not appear in the |pending_request_map_|. For example, |
| // non-delayable requests are immediately granted and skip being placed into |
| // this map. |
| auto pending_request = pending_request_map_.find(id); |
| if (pending_request != pending_request_map_.end()) { |
| pending_request_map_.erase(pending_request); |
| // Intentionally does not remove it from |pending_requests_|. |
| |
| // Didn't release any running requests, but the outstanding limit might be |
| // changed to allow another request. |
| if (option == ReleaseOption::kReleaseAndSchedule) |
| MaybeRun(); |
| return true; |
| } |
| return false; |
| } |
| |
| void ResourceLoadScheduler::SetOutstandingLimitForTesting(size_t tight_limit, |
| size_t normal_limit) { |
| tight_outstanding_limit_ = tight_limit; |
| normal_outstanding_limit_ = normal_limit; |
| MaybeRun(); |
| } |
| |
| bool ResourceLoadScheduler::IsClientDelayable(ThrottleOption option) const { |
| switch (frame_scheduler_lifecycle_state_) { |
| case scheduler::SchedulingLifecycleState::kNotThrottled: |
| case scheduler::SchedulingLifecycleState::kHidden: |
| case scheduler::SchedulingLifecycleState::kThrottled: |
| return option == ThrottleOption::kThrottleable; |
| case scheduler::SchedulingLifecycleState::kStopped: |
| return option != ThrottleOption::kCanNotBeStoppedOrThrottled; |
| } |
| } |
| |
| void ResourceLoadScheduler::OnLifecycleStateChanged( |
| scheduler::SchedulingLifecycleState state) { |
| if (frame_scheduler_lifecycle_state_ == state) |
| return; |
| |
| frame_scheduler_lifecycle_state_ = state; |
| |
| if (state == scheduler::SchedulingLifecycleState::kNotThrottled) |
| ShowConsoleMessageIfNeeded(); |
| |
| MaybeRun(); |
| } |
| |
| ResourceLoadScheduler::ClientId ResourceLoadScheduler::GenerateClientId() { |
| ClientId id = ++current_id_; |
| CHECK_NE(0u, id); |
| return id; |
| } |
| |
| bool ResourceLoadScheduler::IsPendingRequestEffectivelyEmpty( |
| ThrottleOption option) { |
| for (const auto& client : pending_requests_[option]) { |
| // The request in |pending_request_| is erased when it is scheduled. So if |
| // the request is canceled, or Release() is called before firing its Run(), |
| // the entry for the request remains in |pending_request_| until it is |
| // popped in GetNextPendingRequest(). |
| if (pending_request_map_.find(client.client_id) != |
| pending_request_map_.end()) { |
| return false; |
| } |
| } |
| // There is no entry, or no existing entries are alive in |
| // |pending_request_map_|. |
| return true; |
| } |
| |
| bool ResourceLoadScheduler::GetNextPendingRequest(ClientId* id) { |
| auto& stoppable_queue = pending_requests_[ThrottleOption::kStoppable]; |
| auto& throttleable_queue = pending_requests_[ThrottleOption::kThrottleable]; |
| |
| // Check if stoppable or throttleable requests are allowed to be run. |
| auto stoppable_it = stoppable_queue.begin(); |
| bool has_runnable_stoppable_request = |
| stoppable_it != stoppable_queue.end() && |
| (!IsClientDelayable(ThrottleOption::kStoppable) || |
| running_throttleable_requests_.size() < |
| GetOutstandingLimit(stoppable_it->priority)); |
| |
| auto throttleable_it = throttleable_queue.begin(); |
| bool has_runnable_throttleable_request = |
| throttleable_it != throttleable_queue.end() && |
| (!IsClientDelayable(ThrottleOption::kThrottleable) || |
| running_throttleable_requests_.size() < |
| GetOutstandingLimit(throttleable_it->priority)); |
| |
| if (!has_runnable_throttleable_request && !has_runnable_stoppable_request) |
| return false; |
| |
| // If both requests are allowed to be run, run the high priority requests |
| // first. |
| ClientIdWithPriority::Compare compare; |
| bool use_stoppable = has_runnable_stoppable_request && |
| (!has_runnable_throttleable_request || |
| compare(*stoppable_it, *throttleable_it)); |
| |
| // Remove the iterator from the correct set of |pending_requests_|, and update |
| // corresponding |pending_queue_update_times_|. |
| if (use_stoppable) { |
| *id = stoppable_it->client_id; |
| if (ShouldDelay(pending_request_map_.find(*id))) |
| return false; |
| stoppable_queue.erase(stoppable_it); |
| pending_queue_update_times_[ThrottleOption::kStoppable] = clock_->Now(); |
| return true; |
| } |
| |
| *id = throttleable_it->client_id; |
| if (ShouldDelay(pending_request_map_.find(*id))) |
| return false; |
| throttleable_queue.erase(throttleable_it); |
| pending_queue_update_times_[ThrottleOption::kThrottleable] = clock_->Now(); |
| return true; |
| } |
| |
| void ResourceLoadScheduler::MaybeRun() { |
| // Requests for keep-alive loaders could be remained in the pending queue, |
| // but ignore them once Shutdown() is called. |
| if (is_shutdown_) |
| return; |
| |
| ClientId id = kInvalidClientId; |
| while (GetNextPendingRequest(&id)) { |
| auto found = pending_request_map_.find(id); |
| if (found == pending_request_map_.end()) |
| continue; // Already released. |
| |
| auto priority = found->value->priority; |
| ResourceLoadSchedulerClient* client = found->value->client; |
| ThrottleOption option = found->value->option; |
| pending_request_map_.erase(found); |
| Run(id, client, option == ThrottleOption::kThrottleable, priority); |
| } |
| } |
| |
| void ResourceLoadScheduler::MarkFirstPaint() { |
| if (!base::FeatureList::IsEnabled( |
| features::kDelayCompetingLowPriorityRequests) || |
| delay_milestone_reached_) { |
| return; |
| } |
| |
| if (ComputeDelayMilestone() == |
| mojom::blink::DelayCompetingLowPriorityRequestsDelayType::kFirstPaint) { |
| DCHECK(!delay_milestone_reached_); |
| delay_milestone_reached_ = true; |
| MaybeRun(); |
| } |
| } |
| |
| void ResourceLoadScheduler::MarkFirstContentfulPaint() { |
| if (!base::FeatureList::IsEnabled( |
| features::kDelayCompetingLowPriorityRequests) || |
| delay_milestone_reached_) { |
| return; |
| } |
| |
| if (ComputeDelayMilestone() == |
| mojom::blink::DelayCompetingLowPriorityRequestsDelayType:: |
| kFirstContentfulPaint) { |
| DCHECK(!delay_milestone_reached_); |
| delay_milestone_reached_ = true; |
| MaybeRun(); |
| } |
| } |
| |
| void ResourceLoadScheduler::Run(ResourceLoadScheduler::ClientId id, |
| ResourceLoadSchedulerClient* client, |
| bool throttleable, |
| ResourceLoadPriority priority) { |
| if (priority >= PriorityImportanceThreshold()) { |
| in_flight_important_requests_++; |
| } |
| running_requests_.insert(id, priority); |
| if (throttleable) |
| running_throttleable_requests_.insert(id); |
| client->Run(); |
| } |
| |
| size_t ResourceLoadScheduler::GetOutstandingLimit( |
| ResourceLoadPriority priority) const { |
| size_t limit = kOutstandingUnlimited; |
| |
| switch (frame_scheduler_lifecycle_state_) { |
| case scheduler::SchedulingLifecycleState::kHidden: |
| case scheduler::SchedulingLifecycleState::kThrottled: |
| limit = std::min(limit, outstanding_limit_for_throttled_frame_scheduler_); |
| break; |
| case scheduler::SchedulingLifecycleState::kNotThrottled: |
| break; |
| case scheduler::SchedulingLifecycleState::kStopped: |
| limit = 0; |
| break; |
| } |
| |
| switch (policy_) { |
| case ThrottlingPolicy::kTight: |
| limit = std::min(limit, priority < ResourceLoadPriority::kHigh |
| ? tight_outstanding_limit_ |
| : normal_outstanding_limit_); |
| break; |
| case ThrottlingPolicy::kNormal: |
| limit = std::min(limit, normal_outstanding_limit_); |
| break; |
| } |
| return limit; |
| } |
| |
| void ResourceLoadScheduler::ShowConsoleMessageIfNeeded() { |
| if (is_console_info_shown_ || pending_request_map_.IsEmpty()) |
| return; |
| |
| const base::Time limit = clock_->Now() - base::TimeDelta::FromSeconds(60); |
| ThrottleOption target_option; |
| if (pending_queue_update_times_[ThrottleOption::kThrottleable] < limit && |
| !IsPendingRequestEffectivelyEmpty(ThrottleOption::kThrottleable)) { |
| target_option = ThrottleOption::kThrottleable; |
| } else if (pending_queue_update_times_[ThrottleOption::kStoppable] < limit && |
| !IsPendingRequestEffectivelyEmpty(ThrottleOption::kStoppable)) { |
| target_option = ThrottleOption::kStoppable; |
| } else { |
| // At least, one of the top requests in pending queues was handled in the |
| // last 1 minutes, or there is no pending requests in the inactive queue. |
| return; |
| } |
| console_logger_->AddConsoleMessage( |
| mojom::ConsoleMessageSource::kOther, mojom::ConsoleMessageLevel::kInfo, |
| "Some resource load requests were throttled while the tab was in " |
| "background, and no request was sent from the queue in the last 1 " |
| "minute. This means previously requested in-flight requests haven't " |
| "received any response from servers. See " |
| "https://www.chromestatus.com/feature/5527160148197376 for more details"); |
| is_console_info_shown_ = true; |
| } |
| |
| void ResourceLoadScheduler::SetClockForTesting(const base::Clock* clock) { |
| clock_ = clock; |
| } |
| |
| bool ResourceLoadScheduler::ShouldDelay( |
| PendingRequestMap::iterator found) const { |
| if (!base::FeatureList::IsEnabled( |
| features::kDelayCompetingLowPriorityRequests)) { |
| return false; |
| } |
| |
| // The milestone already passed. We no longer have to delay requests. |
| if (delay_milestone_reached_) |
| return false; |
| |
| // There are no inflight important requests. We don't have to delay the |
| // pending request even if it has low priority. |
| if (in_flight_important_requests_ == 0) |
| return false; |
| |
| // Hidden pages already have requests throttled/deprioritized, and delaying |
| // further can have undesirable effects on sites, and there's little benefit |
| // to try to optimize them using this feature. |
| if (frame_scheduler_lifecycle_state_ == |
| scheduler::SchedulingLifecycleState::kHidden) { |
| return false; |
| } |
| |
| // We didn't find the pending request for the id. |
| if (found == pending_request_map_.end()) |
| return false; |
| |
| // The pending request is not in low priority. |
| if (found->value->priority > ResourceLoadPriority::kLow) |
| return false; |
| |
| if (features::kDelayCompetingLowPriorityRequestsDelayParam.Get() == |
| features::DelayCompetingLowPriorityRequestsDelayType:: |
| kUseOptimizationGuide) { |
| // The optimization guide is supposed to be used, but the hints are not |
| // available. Give up delaying requests. |
| if (!optimization_hints_) |
| return false; |
| // The optimization guide suggests the default behavior (no delay). |
| if (optimization_hints_->delay_type == |
| mojom::blink::DelayCompetingLowPriorityRequestsDelayType::kUnknown) { |
| return false; |
| } |
| } |
| |
| // We get a chance to delay competing low priority requests. Record the fact |
| // in UKM to measure the application ratio of the optimization. |
| if (loading_behavior_observer_) { |
| loading_behavior_observer_->DidObserveLoadingBehavior( |
| kLoadingBehaviorCompetingLowPriorityRequestsDelayed); |
| } |
| |
| return true; |
| } |
| |
| ResourceLoadPriority ResourceLoadScheduler::PriorityImportanceThreshold() { |
| // The default value defined by the field trial. |
| DCHECK_EQ( |
| features::DelayCompetingLowPriorityRequestsThreshold::kHigh, |
| features::kDelayCompetingLowPriorityRequestsThresholdParam.default_value); |
| const auto default_value = ResourceLoadPriority::kHigh; |
| |
| using FeatureDelayType = features::DelayCompetingLowPriorityRequestsDelayType; |
| using FeaturePriorityThreshold = |
| features::DelayCompetingLowPriorityRequestsThreshold; |
| using MojomPriorityThreshold = |
| mojom::blink::DelayCompetingLowPriorityRequestsPriorityThreshold; |
| |
| switch (features::kDelayCompetingLowPriorityRequestsDelayParam.Get()) { |
| // Use parameters provided by the field trial. |
| case FeatureDelayType::kFirstPaint: |
| case FeatureDelayType::kFirstContentfulPaint: |
| case FeatureDelayType::kAlways: |
| switch ( |
| features::kDelayCompetingLowPriorityRequestsThresholdParam.Get()) { |
| case FeaturePriorityThreshold::kHigh: |
| return ResourceLoadPriority::kHigh; |
| case FeaturePriorityThreshold::kMedium: |
| return ResourceLoadPriority::kMedium; |
| } |
| NOTREACHED(); |
| // Use hints provided by the optimization guide. |
| case FeatureDelayType::kUseOptimizationGuide: |
| if (!optimization_hints_) { |
| // The optimization guide service didn't provide the hints. Fallback to |
| // the default value. |
| return default_value; |
| } |
| switch (optimization_hints_->priority_threshold) { |
| case MojomPriorityThreshold::kHigh: |
| return ResourceLoadPriority::kHigh; |
| case MojomPriorityThreshold::kMedium: |
| return ResourceLoadPriority::kMedium; |
| case MojomPriorityThreshold::kUnknown: |
| // The optimization guide didn't decide the priority threshold. |
| // Fallback to the default value. |
| return default_value; |
| } |
| NOTREACHED(); |
| } |
| } |
| |
| mojom::blink::DelayCompetingLowPriorityRequestsDelayType |
| ResourceLoadScheduler::ComputeDelayMilestone() { |
| DCHECK(base::FeatureList::IsEnabled( |
| features::kDelayCompetingLowPriorityRequests)); |
| |
| using FeatureDelayType = features::DelayCompetingLowPriorityRequestsDelayType; |
| using MojomDelayType = |
| mojom::blink::DelayCompetingLowPriorityRequestsDelayType; |
| |
| switch (features::kDelayCompetingLowPriorityRequestsDelayParam.Get()) { |
| // Use parameters provided by the field trial. |
| case FeatureDelayType::kFirstPaint: |
| return MojomDelayType::kFirstPaint; |
| case FeatureDelayType::kFirstContentfulPaint: |
| return MojomDelayType::kFirstContentfulPaint; |
| case FeatureDelayType::kAlways: |
| return MojomDelayType::kUnknown; |
| |
| // Use hints provided by the optimization guide. |
| case FeatureDelayType::kUseOptimizationGuide: |
| // Give up delaying requests when the optimization guide is enabled but |
| // the hints are not available. See ShouldDelay(). |
| if (!optimization_hints_) |
| return MojomDelayType::kUnknown; |
| return optimization_hints_->delay_type; |
| } |
| } |
| |
| } // namespace blink |