blob: 0642aabc26beec892a5f75f9bfed46054c8c80a0 [file] [log] [blame]
// 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, &param))
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