blob: 1d0859f2c9168298a4d87c8160746a5488cec86c [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/renderer_host/back_forward_cache_impl.h"
#include <string>
#include <vector>
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/cxx20_erase.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/trace_event/typed_macros.h"
#include "build/build_config.h"
#include "content/browser/bad_message.h"
#include "content/browser/renderer_host/back_forward_cache_can_store_document_result.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_proxy_host.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/visible_time_request_trigger.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_process_host_observer.h"
#include "content/public/browser/visibility.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "third_party/blink/public/mojom/frame/sudden_termination_disabler_type.mojom-shared.h"
#if BUILDFLAG(IS_ANDROID)
#include "content/public/browser/android/child_process_importance.h"
#endif
namespace content {
class RenderProcessHostInternalObserver;
namespace {
using blink::scheduler::WebSchedulerTrackedFeature;
using blink::scheduler::WebSchedulerTrackedFeatures;
// The default number of entries the BackForwardCache can hold per tab.
static constexpr size_t kDefaultBackForwardCacheSize = 1;
// The default number value for the "foreground_cache_size" field trial
// parameter. This parameter controls the numbers of entries associated with
// foregrounded process the BackForwardCache can hold per tab, when using the
// foreground/background cache-limiting strategy. This strategy is enabled if
// the parameter values is non-zero.
static constexpr size_t kDefaultForegroundBackForwardCacheSize = 0;
// The default time to live in seconds for documents in BackForwardCache.
// See also crbug.com/1305878.
static constexpr int kDefaultTimeToLiveInBackForwardCacheInSeconds = 600;
#if BUILDFLAG(IS_ANDROID)
bool IsProcessBindingEnabled() {
// Avoid activating BackForwardCache trial for checking the parameters
// associated with it.
if (!IsBackForwardCacheEnabled())
return false;
const std::string process_binding_param =
base::GetFieldTrialParamValueByFeature(features::kBackForwardCache,
"process_binding_strength");
return process_binding_param.empty() || process_binding_param == "DISABLE";
}
// Association of ChildProcessImportance to corresponding string names.
const base::FeatureParam<ChildProcessImportance>::Option
child_process_importance_options[] = {
{ChildProcessImportance::IMPORTANT, "IMPORTANT"},
{ChildProcessImportance::MODERATE, "MODERATE"},
{ChildProcessImportance::NORMAL, "NORMAL"}};
// Defines the binding strength for a processes holding cached pages. The value
// is read from an experiment parameter value. Ideally this would be lower than
// the one for processes holding the foreground page and similar to that of
// background tabs so that the OS will hopefully kill the foreground tab last.
// The default importance is set to MODERATE.
const base::FeatureParam<ChildProcessImportance> kChildProcessImportanceParam{
&features::kBackForwardCache, "process_binding_strength",
ChildProcessImportance::MODERATE, &child_process_importance_options};
#endif
bool IsContentInjectionSupported() {
if (!IsBackForwardCacheEnabled())
return false;
static constexpr base::FeatureParam<bool> content_injection_supported(
&features::kBackForwardCache, "content_injection_supported", true);
return content_injection_supported.Get();
}
WebSchedulerTrackedFeatures SupportedFeaturesImpl() {
WebSchedulerTrackedFeatures features;
if (!IsBackForwardCacheEnabled())
return features;
static constexpr base::FeatureParam<std::string> supported_features(
&features::kBackForwardCache, "supported_features", "");
std::vector<std::string> tokens =
base::SplitString(supported_features.Get(), ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const std::string& token : tokens) {
auto feature = blink::scheduler::StringToFeature(token);
if (feature.has_value()) {
features.Put(feature.value());
} else if (!blink::scheduler::IsRemovedFeature(token)) {
// |feature| might not have its value when the user is in an experimental
// group with the feature, and the feature is already removed.
DLOG(WARNING) << "Invalid feature string: " << token;
}
}
return features;
}
WebSchedulerTrackedFeatures SupportedFeatures() {
static WebSchedulerTrackedFeatures features = SupportedFeaturesImpl();
return features;
}
bool IgnoresOutstandingNetworkRequestForTesting() {
if (!IsBackForwardCacheEnabled())
return false;
static constexpr base::FeatureParam<bool>
outstanding_network_request_supported(
&features::kBackForwardCache,
"ignore_outstanding_network_request_for_testing", false);
return outstanding_network_request_supported.Get();
}
// Ignore all features that the page is using and all DisableForRenderFrameHost
// calls and force all pages to be cached. Should be used only for local testing
// and debugging -- things will break when this param is used.
bool ShouldIgnoreBlocklists() {
if (!IsBackForwardCacheEnabled())
return false;
static constexpr base::FeatureParam<bool> should_ignore_blocklists(
&features::kBackForwardCache, "should_ignore_blocklists", false);
return should_ignore_blocklists.Get();
}
// A list of WebSchedulerTrackedFeatures that always block back/forward
// cache. Some of these features are listed as blocking back/forward cache
// when actually the blocking is flag controlled and they are not registered
// as being used if we don't want them to block.
constexpr WebSchedulerTrackedFeatures kDisallowedFeatures(
WebSchedulerTrackedFeature::kBroadcastChannel,
WebSchedulerTrackedFeature::kContainsPlugins,
WebSchedulerTrackedFeature::kDedicatedWorkerOrWorklet,
WebSchedulerTrackedFeature::kDummy,
WebSchedulerTrackedFeature::kIdleManager,
WebSchedulerTrackedFeature::kIndexedDBConnection,
WebSchedulerTrackedFeature::kKeyboardLock,
WebSchedulerTrackedFeature::kKeepaliveRequest,
WebSchedulerTrackedFeature::kOutstandingIndexedDBTransaction,
WebSchedulerTrackedFeature::kPaymentManager,
WebSchedulerTrackedFeature::kPictureInPicture,
WebSchedulerTrackedFeature::kPortal,
WebSchedulerTrackedFeature::kPrinting,
WebSchedulerTrackedFeature::kRequestedAudioCapturePermission,
WebSchedulerTrackedFeature::kRequestedBackForwardCacheBlockedSensors,
WebSchedulerTrackedFeature::kRequestedBackgroundWorkPermission,
WebSchedulerTrackedFeature::kRequestedMIDIPermission,
WebSchedulerTrackedFeature::kRequestedVideoCapturePermission,
WebSchedulerTrackedFeature::kSharedWorker,
WebSchedulerTrackedFeature::kWebDatabase,
WebSchedulerTrackedFeature::kWebOTPService,
WebSchedulerTrackedFeature::kSpeechRecognizer,
WebSchedulerTrackedFeature::kSpeechSynthesis,
WebSchedulerTrackedFeature::kWebDatabase,
WebSchedulerTrackedFeature::kWebHID,
WebSchedulerTrackedFeature::kWebLocks,
WebSchedulerTrackedFeature::kWebRTC,
WebSchedulerTrackedFeature::kWebShare,
WebSchedulerTrackedFeature::kWebSocket,
WebSchedulerTrackedFeature::kWebTransport,
WebSchedulerTrackedFeature::kWebXR);
constexpr WebSchedulerTrackedFeatures kInjectionFeatures(
WebSchedulerTrackedFeature::kInjectedJavascript,
WebSchedulerTrackedFeature::kInjectedStyleSheet);
constexpr WebSchedulerTrackedFeatures kNetworkFeatures(
WebSchedulerTrackedFeature::kOutstandingNetworkRequestOthers,
WebSchedulerTrackedFeature::kOutstandingNetworkRequestFetch,
WebSchedulerTrackedFeature::kOutstandingNetworkRequestXHR);
// A list of WebSchedulerTrackedFeatures that should never block back/forward
// cache.
constexpr WebSchedulerTrackedFeatures kAllowedFeatures(
WebSchedulerTrackedFeature::kDocumentLoaded,
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoCache,
// This is handled in |UpdateCanStoreToIncludeCacheControlNoStore()|, and no
// need to include in |GetDisallowedFeatures()|.
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore,
// TODO(crbug.com/1357482): Figure out if these two should be allowed.
WebSchedulerTrackedFeature::kOutstandingNetworkRequestDirectSocket,
WebSchedulerTrackedFeature::kRequestedStorageAccessGrant,
// We don't block on subresource cache-control:no-store or no-cache.
WebSchedulerTrackedFeature::kSubresourceHasCacheControlNoCache,
WebSchedulerTrackedFeature::kSubresourceHasCacheControlNoStore,
// We only record this if "Cache-Control: no-store" header is present on the
// main frame.
WebSchedulerTrackedFeature::kAuthorizationHeader,
// TODO(crbug.com/1357482): Figure out if this should be allowed.
WebSchedulerTrackedFeature::kWebNfc);
// The BackForwardCache feature is controlled via an experiment. This function
// returns the allowed URL list where it is enabled.
std::string GetAllowedURLList() {
// Avoid activating BackForwardCache trial for checking the parameters
// associated with it.
if (!IsBackForwardCacheEnabled()) {
if (base::FeatureList::IsEnabled(
kRecordBackForwardCacheMetricsWithoutEnabling)) {
return base::GetFieldTrialParamValueByFeature(
kRecordBackForwardCacheMetricsWithoutEnabling, "allowed_websites");
}
return "";
}
return base::GetFieldTrialParamValueByFeature(features::kBackForwardCache,
"allowed_websites");
}
// This function returns the blocked URL list.
std::string GetBlockedURLList() {
return IsBackForwardCacheEnabled()
? base::GetFieldTrialParamValueByFeature(
features::kBackForwardCache, "blocked_websites")
: "";
}
// Returns the list of blocked CGI params
std::string GetBlockedCgiParams() {
return IsBackForwardCacheEnabled()
? base::GetFieldTrialParamValueByFeature(
features::kBackForwardCache, "blocked_cgi_params")
: "";
}
// Parses the “allowed_websites” and "blocked_websites" field trial parameters
// and creates a map to represent hosts and corresponding path prefixes.
std::map<std::string, std::vector<std::string>> ParseCommaSeparatedURLs(
base::StringPiece comma_separated_urls) {
std::map<std::string, std::vector<std::string>> urls;
for (auto& it :
base::SplitString(comma_separated_urls, ",", base::TRIM_WHITESPACE,
base::SPLIT_WANT_ALL)) {
GURL url = GURL(it);
urls[url.host()].push_back(url.path());
}
return urls;
}
// Parses the "cgi_params" field trial parameter into a set by splitting on "|".
std::unordered_set<std::string> ParseBlockedCgiParams(
base::StringPiece cgi_params_string) {
std::vector<std::string> split =
base::SplitString(cgi_params_string, "|", base::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
std::unordered_set<std::string> cgi_params;
cgi_params.insert(split.begin(), split.end());
return cgi_params;
}
BackForwardCacheTestDelegate* g_bfcache_disabled_test_observer = nullptr;
void RestoreBrowserControlsState(RenderFrameHostImpl* cached_rfh) {
auto* current_rfh =
cached_rfh->frame_tree_node()->render_manager()->current_frame_host();
DCHECK_NE(current_rfh, cached_rfh);
float prev_top_controls_shown_ratio = current_rfh->GetRenderWidgetHost()
->render_frame_metadata_provider()
->LastRenderFrameMetadata()
.top_controls_shown_ratio;
if (prev_top_controls_shown_ratio < 1) {
// Make sure the state in the restored renderer matches the current one.
// If we currently aren't showing the controls let the cached renderer
// know, so that it then reacts correctly to the SHOW controls message
// that might follow during DidCommitNavigation.
cached_rfh->GetPage().UpdateBrowserControlsState(
cc::BrowserControlsState::kBoth, cc::BrowserControlsState::kHidden,
// Do not animate as we want this to happen "instantaneously"
false);
}
}
void RequestRecordTimeToVisible(RenderFrameHostImpl* rfh,
base::TimeTicks navigation_start) {
// Make sure we record only when the frame is not in hidden state to avoid
// cases like page navigating back with window.history.back(), while being
// hidden.
if (rfh->delegate()->GetVisibility() != Visibility::HIDDEN) {
// The trigger can be null in unit tests.
auto* trigger = rfh->GetRenderWidgetHost()->GetVisibleTimeRequestTrigger();
if (trigger) {
trigger->UpdateRequest(navigation_start,
false /* destination_is_loaded */,
false /* show_reason_tab_switching */,
true /* show_reason_bfcache_restore */);
}
}
}
// Returns true if any of the processes associated with the RenderViewHosts in
// this Entry are foregrounded.
bool HasForegroundedProcess(BackForwardCacheImpl::Entry& entry) {
for (const auto& rvh : entry.render_view_hosts()) {
if (!rvh->GetProcess()->IsProcessBackgrounded()) {
return true;
}
}
return false;
}
// Returns true if all of the RenderViewHosts in this Entry have received the
// acknowledgement from renderer.
bool AllRenderViewHostsReceivedAckFromRenderer(
BackForwardCacheImpl::Entry& entry) {
for (const auto& rvh : entry.render_view_hosts()) {
if (!rvh->DidReceiveBackForwardCacheAck()) {
return false;
}
}
return true;
}
// Behavior on pages with cache-control:no-store specified by flags.
enum class CacheControlNoStoreExperimentLevel {
// No experiments for cache-control:no-store are running.
kDoNotStore = 1,
// Only the metrics gathering experiment is on.
kStoreAndEvictUponRestore = 2,
// Restore the entry only when cookies have not changed while in cache.
kStoreAndRestoreUnlessCookieChange = 3,
// Restore the entry only when HTTP Only cookies have not changed while in
// cache.
kStoreAndRestoreUnlessHTTPOnlyCookieChange = 4,
};
const char kCacheControlNoStoreExperimentLevelName[] = "level";
static constexpr base::FeatureParam<CacheControlNoStoreExperimentLevel>::Option
cache_control_levels[] = {
{CacheControlNoStoreExperimentLevel::kStoreAndEvictUponRestore,
"store-and-evict"},
{CacheControlNoStoreExperimentLevel::kStoreAndRestoreUnlessCookieChange,
"restore-unless-cookie-change"},
{CacheControlNoStoreExperimentLevel::
kStoreAndRestoreUnlessHTTPOnlyCookieChange,
"restore-unless-http-only-cookie-change"},
};
const base::FeatureParam<CacheControlNoStoreExperimentLevel>
cache_control_level{&kCacheControlNoStoreEnterBackForwardCache,
kCacheControlNoStoreExperimentLevelName,
CacheControlNoStoreExperimentLevel::kDoNotStore,
&cache_control_levels};
CacheControlNoStoreExperimentLevel GetCacheControlNoStoreLevel() {
if (!IsBackForwardCacheEnabled() ||
!base::FeatureList::IsEnabled(
kCacheControlNoStoreEnterBackForwardCache)) {
return CacheControlNoStoreExperimentLevel::kDoNotStore;
}
return cache_control_level.Get();
}
bool IsSameOriginForTreeResult(RenderFrameHostImpl* rfh,
const GURL& url,
const url::Origin& main_document_origin) {
// Treat any frame inside a fenced frame as cross origin so we don't leak
// any information.
if (rfh->IsNestedWithinFencedFrame())
return false;
return url::Origin::Create(url).IsSameOriginWith(main_document_origin);
}
} // namespace
// static
BlockListedFeatures BackForwardCacheImpl::GetAllowedFeatures(
RequestedFeatures requested_features) {
WebSchedulerTrackedFeatures result = kAllowedFeatures;
if (IsContentInjectionSupported()) {
result.PutAll(kInjectionFeatures);
}
if (IgnoresOutstandingNetworkRequestForTesting()) {
result.PutAll(kNetworkFeatures);
}
result.PutAll(SupportedFeatures());
if (requested_features == RequestedFeatures::kOnlySticky) {
// Add non-sticky disallowed features.
WebSchedulerTrackedFeatures non_sticky =
Difference(kDisallowedFeatures, blink::scheduler::StickyFeatures());
if (!IsContentInjectionSupported()) {
non_sticky.PutAll(
Difference(kInjectionFeatures, blink::scheduler::StickyFeatures()));
}
if (!IgnoresOutstandingNetworkRequestForTesting()) {
non_sticky.PutAll(
Difference(kNetworkFeatures, blink::scheduler::StickyFeatures()));
}
result.PutAll(non_sticky);
}
return result;
}
// static
BlockListedFeatures BackForwardCacheImpl::GetDisallowedFeatures(
RequestedFeatures requested_features) {
WebSchedulerTrackedFeatures result = kDisallowedFeatures;
if (!IsContentInjectionSupported()) {
result.PutAll(kInjectionFeatures);
}
if (!IgnoresOutstandingNetworkRequestForTesting()) {
result.PutAll(kNetworkFeatures);
}
result.RemoveAll(SupportedFeatures());
if (requested_features == RequestedFeatures::kOnlySticky) {
// Remove all non-sticky features from |result|.
result = Intersection(result, blink::scheduler::StickyFeatures());
}
return result;
}
// static
BackForwardCacheImpl::MessageHandlingPolicyWhenCached
BackForwardCacheImpl::GetChannelAssociatedMessageHandlingPolicy() {
// Avoid activating BackForwardCache trial for checking the parameters
// associated with it.
if (!IsBackForwardCacheEnabled())
return kMessagePolicyNone;
static constexpr char kFieldTrialParam[] = "message_handling_when_cached";
auto param = base::GetFieldTrialParamValueByFeature(
features::kBackForwardCache, kFieldTrialParam);
if (param.empty() || param == "log") {
return kMessagePolicyLog;
} else if (param == "none") {
return kMessagePolicyNone;
} else if (param == "dump") {
return kMessagePolicyDump;
} else {
DLOG(WARNING) << "Failed to parse field trial param " << kFieldTrialParam
<< " with string value " << param
<< " under feature kBackForwardCache"
<< features::kBackForwardCache.name;
return kMessagePolicyLog;
}
}
BackForwardCacheImpl::Entry::Entry(std::unique_ptr<StoredPage> stored_page)
: stored_page_(std::move(stored_page)) {
if (BackForwardCacheImpl::AllowStoringPagesWithCacheControlNoStore()) {
cookie_modified_ = {/*http_only_cookie_modified*/ false,
/*cookie_modified*/ false};
}
}
BackForwardCacheImpl::Entry::~Entry() = default;
void BackForwardCacheImpl::Entry::WriteIntoTrace(
perfetto::TracedValue context) {
auto dict = std::move(context).WriteDictionary();
dict.Add("render_frame_host", render_frame_host());
}
void BackForwardCacheImpl::Entry::StartMonitoringCookieChange() {
RenderFrameHostImpl* rfh = stored_page_->render_frame_host();
StoragePartition* storage_partition = rfh->GetStoragePartition();
auto* cookie_manager = storage_partition->GetCookieManagerForBrowserProcess();
if (!cookie_listener_receiver_.is_bound()) {
// Listening only to the main document's URL, not the documents inside the
// subframes.
cookie_manager->AddCookieChangeListener(
rfh->GetLastCommittedURL(), absl::nullopt,
cookie_listener_receiver_.BindNewPipeAndPassRemote());
}
}
void BackForwardCacheImpl::Entry::OnCookieChange(
const net::CookieChangeInfo& change) {
DCHECK(cookie_modified_.has_value());
cookie_modified_->http_only_cookie_modified = change.cookie.IsHttpOnly();
cookie_modified_->cookie_modified = true;
}
void BackForwardCacheImpl::RenderProcessBackgroundedChanged(
RenderProcessHostImpl* host) {
EnforceCacheSizeLimit();
}
BackForwardCacheTestDelegate::BackForwardCacheTestDelegate() {
DCHECK(!g_bfcache_disabled_test_observer);
g_bfcache_disabled_test_observer = this;
}
BackForwardCacheTestDelegate::~BackForwardCacheTestDelegate() {
DCHECK_EQ(g_bfcache_disabled_test_observer, this);
g_bfcache_disabled_test_observer = nullptr;
}
BackForwardCacheImpl::BackForwardCacheImpl()
: allowed_urls_(ParseCommaSeparatedURLs(GetAllowedURLList())),
blocked_urls_(ParseCommaSeparatedURLs(GetBlockedURLList())),
blocked_cgi_params_(ParseBlockedCgiParams(GetBlockedCgiParams())),
weak_factory_(this) {}
BackForwardCacheImpl::~BackForwardCacheImpl() {
Shutdown();
}
absl::optional<int> GetFieldTrialParamByFeatureAsOptionalInt(
const base::Feature& feature,
const std::string& param_name) {
std::string value_as_string =
GetFieldTrialParamValueByFeature(feature, param_name);
int value_as_int = 0;
if (base::StringToInt(value_as_string, &value_as_int)) {
return absl::optional<int>(value_as_int);
}
return absl::optional<int>();
}
base::TimeDelta BackForwardCacheImpl::GetTimeToLiveInBackForwardCache() {
// We use the following order of priority if multiple values exist:
// - The programmatical value set in params. Used in specific tests.
// The TTL set in BackForwardCacheTimeToLiveControl takes precedence over
// the TTL set in the main BackForwardCache feature if both are present.
// - Infinite if kBackForwardCacheNoTimeEviction is enabled.
// - Default value otherwise, kDefaultTimeToLiveInBackForwardCacheInSeconds.
if (base::FeatureList::IsEnabled(kBackForwardCacheTimeToLiveControl)) {
absl::optional<int> time_to_live = GetFieldTrialParamByFeatureAsOptionalInt(
kBackForwardCacheTimeToLiveControl, "time_to_live_seconds");
if (time_to_live.has_value()) {
return base::Seconds(time_to_live.value());
}
}
absl::optional<int> old_time_to_live =
GetFieldTrialParamByFeatureAsOptionalInt(
features::kBackForwardCache, "TimeToLiveInBackForwardCacheInSeconds");
if (old_time_to_live.has_value()) {
return base::Seconds(old_time_to_live.value());
}
if (base::FeatureList::IsEnabled(kBackForwardCacheNoTimeEviction)) {
return base::TimeDelta::Max();
}
return base::Seconds(kDefaultTimeToLiveInBackForwardCacheInSeconds);
}
// static
size_t BackForwardCacheImpl::GetCacheSize() {
if (!IsBackForwardCacheEnabled())
return 0;
auto cache_size = GetFieldTrialParamByFeatureAsOptionalInt(
kBackForwardCacheSize, "cache_size");
if (cache_size.has_value())
return cache_size.value();
return base::GetFieldTrialParamByFeatureAsInt(
features::kBackForwardCache, "cache_size", kDefaultBackForwardCacheSize);
}
// static
size_t BackForwardCacheImpl::GetForegroundedEntriesCacheSize() {
if (!IsBackForwardCacheEnabled())
return 0;
auto foreground_cache_size = GetFieldTrialParamByFeatureAsOptionalInt(
kBackForwardCacheSize, "foreground_cache_size");
if (foreground_cache_size.has_value())
return foreground_cache_size.value();
return base::GetFieldTrialParamByFeatureAsInt(
features::kBackForwardCache, "foreground_cache_size",
kDefaultForegroundBackForwardCacheSize);
}
// static
bool BackForwardCacheImpl::UsingForegroundBackgroundCacheSizeLimit() {
return GetForegroundedEntriesCacheSize() > 0;
}
BackForwardCacheImpl::Entry* BackForwardCacheImpl::FindMatchingEntry(
PageImpl& page) {
RenderFrameHostImpl* render_frame_host = &page.GetMainDocument();
Entry* matching_entry = nullptr;
for (std::unique_ptr<Entry>& entry : entries_) {
if (render_frame_host == entry->render_frame_host()) {
matching_entry = entry.get();
break;
}
}
return matching_entry;
}
void BackForwardCacheImpl::UpdateCanStoreToIncludeCacheControlNoStore(
BackForwardCacheCanStoreDocumentResult& result,
RenderFrameHostImpl* render_frame_host) {
// If the feature is disabled, do nothing.
if (!AllowStoringPagesWithCacheControlNoStore())
return;
// If the page didn't have cache-control: no-store, do nothing.
if (!render_frame_host->GetBackForwardCacheDisablingFeatures().Has(
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore)) {
return;
}
auto* matching_entry = FindMatchingEntry(render_frame_host->GetPage());
// |matching_entry| can be nullptr for tests because this can be called from
// |GetCurrentBackForwardCacheEligibility()|, at which point |rfh| may not
// have a matching entry yet.
if (!matching_entry)
return;
// Note that kCacheControlNoStoreHTTPOnlyCookieModified,
// kCacheControlNoStoreCookieModified and kCacheControlNoStore are mutually
// exclusive.
if (matching_entry->cookie_modified_->http_only_cookie_modified) {
result.No(BackForwardCacheMetrics::NotRestoredReason::
kCacheControlNoStoreHTTPOnlyCookieModified);
} else if (matching_entry->cookie_modified_->cookie_modified) {
// JavaScript cookies are modified but not HTTP cookies. Only restore based
// on the experiment level.
if (GetCacheControlNoStoreLevel() <=
CacheControlNoStoreExperimentLevel::
kStoreAndRestoreUnlessCookieChange) {
result.No(BackForwardCacheMetrics::NotRestoredReason::
kCacheControlNoStoreCookieModified);
}
} else if (GetCacheControlNoStoreLevel() ==
CacheControlNoStoreExperimentLevel::kStoreAndEvictUponRestore) {
result.No(BackForwardCacheMetrics::NotRestoredReason::kCacheControlNoStore);
}
}
namespace {
void LogAndTraceResult(
const RenderFrameHostImpl& rfh,
const BackForwardCacheCanStoreDocumentResult& flattened_result,
const perfetto::StaticString& caller) {
VLOG(1) << caller.value << ": " << rfh.GetLastCommittedURL() << " : "
<< flattened_result.ToString();
TRACE_EVENT("navigation", caller,
ChromeTrackEvent::kBackForwardCacheCanStoreDocumentResult,
flattened_result);
}
} // namespace
BackForwardCacheCanStoreDocumentResultWithTree
BackForwardCacheImpl::GetCurrentBackForwardCacheEligibility(
RenderFrameHostImpl* rfh) {
BackForwardCacheCanStoreDocumentResult flattened_result;
std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree =
PopulateReasonsForPage(rfh, flattened_result,
/*include_non_sticky=*/true);
LogAndTraceResult(
*rfh, flattened_result,
"BackForwardCacheImpl::GetCurrentBackForwardCacheEligibility");
DCHECK(tree->FlattenTree() == flattened_result);
return BackForwardCacheCanStoreDocumentResultWithTree(flattened_result,
std::move(tree));
}
BackForwardCacheCanStoreDocumentResultWithTree
BackForwardCacheImpl::GetFutureBackForwardCacheEligibilityPotential(
RenderFrameHostImpl* rfh) {
BackForwardCacheCanStoreDocumentResult flattened;
auto tree = PopulateReasonsForPage(rfh, flattened,
/*include_non_sticky = */ false);
LogAndTraceResult(
*rfh, flattened,
"BackForwardCacheImpl::GetFutureBackForwardCacheEligibilityPotential");
DCHECK(tree->FlattenTree() == flattened);
return BackForwardCacheCanStoreDocumentResultWithTree(flattened,
std::move(tree));
}
std::unique_ptr<BackForwardCacheCanStoreTreeResult>
BackForwardCacheImpl::PopulateReasonsForPage(
RenderFrameHostImpl* rfh,
BackForwardCacheCanStoreDocumentResult& flattened_result,
bool include_non_sticky) {
// TODO(crbug.com/1275977): This function should only be called when |rfh| is
// the primary main frame. Fix |ShouldProactivelySwapBrowsingInstance()| and
// |UnloadOldFrame()| so that it will not check bfcache eligibility if not
// primary main frame.
BackForwardCacheCanStoreDocumentResult main_document_specific_result;
// This function can be called during eviction, and |rfh| can be in
// back/forward cache, which is considered as non primary main frame.
bool main_frame_in_bfcache =
rfh->IsInBackForwardCache() && rfh->IsOutermostMainFrame();
if (!rfh->IsInPrimaryMainFrame() && !main_frame_in_bfcache) {
// When |rfh| is not the primary main frame and is not the bfcache main
// frame, e.g. when |rfh| is prerendering, fenced frame root or is not the
// main frame, we can reach this block.
// We do not need to check the subframes' reasons because callers that reach
// here only care about whether can_store is true or false, not about the
// reasons.
main_document_specific_result.No(
BackForwardCacheMetrics::NotRestoredReason::kNotPrimaryMainFrame);
} else {
// Populate main document specific reasons.
PopulateReasonsForMainDocument(main_document_specific_result, rfh);
}
// Add the reasons for main document to the flattened list.
flattened_result.AddReasonsFrom(main_document_specific_result);
// Call the recursive function that adds the reasons from the subtree to the
// flattened list, and return the tree if needed.
std::unique_ptr<BackForwardCacheCanStoreTreeResult> result_tree;
if (rfh->IsInPrimaryMainFrame() || main_frame_in_bfcache) {
NotRestoredReasonBuilder builder(rfh, include_non_sticky);
result_tree = builder.GetTreeResult();
flattened_result.AddReasonsFrom(builder.GetFlattenedResult());
} else {
result_tree = BackForwardCacheCanStoreTreeResult::CreateEmptyTree(rfh);
}
// |result_tree| does not have main document specific reasons such as
// "disabled via command line", and we have to manually add them.
result_tree->AddReasonsToSubtreeRootFrom(main_document_specific_result);
return result_tree;
}
void BackForwardCacheImpl::PopulateReasonsForMainDocument(
BackForwardCacheCanStoreDocumentResult& result,
RenderFrameHostImpl* rfh) {
bool main_frame_in_bfcache =
rfh->IsInBackForwardCache() && rfh->IsOutermostMainFrame();
DCHECK(rfh->IsInPrimaryMainFrame() || main_frame_in_bfcache);
// If the the delegate doesn't support back forward cache, disable it.
if (!rfh->delegate()->IsBackForwardCacheSupported()) {
result.No(BackForwardCacheMetrics::NotRestoredReason::
kBackForwardCacheDisabledForDelegate);
}
if (!IsBackForwardCacheEnabled() || is_disabled_for_testing_) {
result.No(
BackForwardCacheMetrics::NotRestoredReason::kBackForwardCacheDisabled);
// In addition to the general "BackForwardCacheDisabled" reason above, also
// track more specific reasons on why BackForwardCache is disabled.
if (IsBackForwardCacheDisabledByCommandLine()) {
result.No(BackForwardCacheMetrics::NotRestoredReason::
kBackForwardCacheDisabledByCommandLine);
}
if (!DeviceHasEnoughMemoryForBackForwardCache()) {
result.No(BackForwardCacheMetrics::NotRestoredReason::
kBackForwardCacheDisabledByLowMemory);
}
}
// If this function is called after we navigated to a new RenderFrameHost,
// then |rfh| must already be replaced by the new RenderFrameHost. If this
// function is called before we navigated, then |rfh| must be an active
// RenderFrameHost.
bool is_active_rfh = rfh->IsActive();
// Two pages in the same BrowsingInstance can script each other. When a page
// can be scripted from outside, it can't enter the BackForwardCache.
//
// If the |rfh| is not an "active" RenderFrameHost anymore, the
// "RelatedActiveContentsCount" below is compared against 0, not 1. This is
// because |rfh| is not "active" itself.
//
// This check makes sure the old and new document aren't sharing the same
// BrowsingInstance. Note that the existence of related active contents might
// change in the future, but we are checking this in
// GetFutureBackForwardCacheEligibilityPotential instead of
// GetCurrentBackForwardCacheEligibility because it's needed to determine
// whether to do a proactive BrowsingInstance swap or not, which should not be
// done if the page has related active contents.
unsigned expected_related_active_contents_count = is_active_rfh ? 1 : 0;
// We should never have fewer than expected.
DCHECK_GE(rfh->GetSiteInstance()->GetRelatedActiveContentsCount(),
expected_related_active_contents_count);
if (rfh->GetSiteInstance()->GetRelatedActiveContentsCount() >
expected_related_active_contents_count) {
absl::optional<ShouldSwapBrowsingInstance> browsing_instance_swap_result;
if (auto* metrics = rfh->GetBackForwardCacheMetrics())
browsing_instance_swap_result = metrics->browsing_instance_swap_result();
result.NoDueToRelatedActiveContents(browsing_instance_swap_result);
}
// Only store documents that have successful http status code.
// Note that for error pages, |last_http_status_code| is equal to 0.
if (rfh->last_http_status_code() != net::HTTP_OK)
result.No(BackForwardCacheMetrics::NotRestoredReason::kHTTPStatusNotOK);
// Interstitials and other internal error pages should set an error status
// code but there's no guarantee, e.g. https://crbug/1274308,
// https://crbug/1287996. This catches those cases. It might also make the
// kHTTPStatusNotOK check redundant.
if (rfh->IsErrorDocument())
result.No(BackForwardCacheMetrics::NotRestoredReason::kErrorDocument);
// Only store documents that were fetched via HTTP GET method.
if (rfh->last_http_method() != net::HttpRequestHeaders::kGetMethod)
result.No(BackForwardCacheMetrics::NotRestoredReason::kHTTPMethodNotGET);
// Only store documents that have a valid network::mojom::URLResponseHead.
// We actually don't know the actual case this reason is solely set without
// kHTTPStatusNotOK and kSchemeNotHTTPOrHTTPS, but crash reports imply it
// happens.
// TODO(https://crbug.com/1216997): Understand the case and remove
// DebugScenario::kDebugNoResponseHeadForHTTPOrHTTPS.
if (!rfh->last_response_head())
result.No(BackForwardCacheMetrics::NotRestoredReason::kNoResponseHead);
// Do not store main document with non HTTP/HTTPS URL scheme. Among other
// things, this excludes the new tab page and all WebUI pages.
if (!rfh->GetLastCommittedURL().SchemeIsHTTPOrHTTPS()) {
result.No(
BackForwardCacheMetrics::NotRestoredReason::kSchemeNotHTTPOrHTTPS);
}
// We should not cache pages with Cache-control: no-store. Note that
// even though this is categorized as a "feature", we will check this within
// GetFutureBackForwardCacheEligibilityPotential as it's not possible to
// change the HTTP headers, so if it's not possible to cache this page now due
// to this, it's impossible to cache this page later.
// TODO(rakina): Once we move cache-control tracking to RenderFrameHostImpl,
// change this part to use the information stored in RenderFrameHostImpl
// instead.
if (rfh->GetBackForwardCacheDisablingFeatures().Has(
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore)) {
if (!AllowStoringPagesWithCacheControlNoStore()) {
// Block pages with cache-control: no-store only when
// |should_cache_control_no_store_enter| flag is false. If true, put the
// page in and evict later.
result.NoDueToFeatures(
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore);
}
}
// Only store documents that have URLs allowed through experiment.
if (!IsAllowed(rfh->GetLastCommittedURL()))
result.No(BackForwardCacheMetrics::NotRestoredReason::kDomainNotAllowed);
}
void BackForwardCacheImpl::NotRestoredReasonBuilder::
PopulateStickyReasonsForDocument(
BackForwardCacheCanStoreDocumentResult& result,
RenderFrameHostImpl* rfh) {
// If the rfh has ever granted media access, prevent it from entering cache.
// TODO(crbug.com/989379): Consider only blocking when there's an active
// media stream.
if (rfh->was_granted_media_access()) {
result.No(
BackForwardCacheMetrics::NotRestoredReason::kWasGrantedMediaAccess);
}
if (rfh->IsBackForwardCacheDisabled() && !ShouldIgnoreBlocklists()) {
result.NoDueToDisableForRenderFrameHostCalled(
rfh->back_forward_cache_disabled_reasons());
}
// Do not store documents if they have inner WebContents. Inner frame trees
// that are based on MPArch are allowed to be stored. To determine if this
// is an inner WebContents we check the inner frame tree's type to see if
// it is `kPrimary`.
if (rfh->frame_tree()->delegate()->GetOuterDelegateFrameTreeNodeId() !=
FrameTreeNode::kFrameTreeNodeInvalidId &&
rfh->frame_tree()->type() == FrameTree::Type::kPrimary) {
result.No(BackForwardCacheMetrics::NotRestoredReason::kHaveInnerContents);
}
#if !BUILDFLAG(IS_ANDROID)
const bool has_unload_handler = rfh->has_unload_handler();
if (has_unload_handler) {
// Note that pages with unload handlers are cached on android.
result.No(rfh->GetParent() ? BackForwardCacheMetrics::NotRestoredReason::
kUnloadHandlerExistsInSubFrame
: BackForwardCacheMetrics::NotRestoredReason::
kUnloadHandlerExistsInMainFrame);
}
#endif
// When it's not the final decision for putting a page in the back-forward
// cache, we should only consider "sticky" features here - features that
// will always result in a page becoming ineligible for back-forward cache
// since the first time it's used.
WebSchedulerTrackedFeatures banned_features =
Intersection(GetDisallowedFeatures(RequestedFeatures::kOnlySticky),
rfh->GetBackForwardCacheDisablingFeatures());
if (!banned_features.Empty()) {
if (!ShouldIgnoreBlocklists()) {
result.NoDueToFeatures(banned_features);
}
}
// If the main document had CCNS and this document is same-origin with the
// main document and used the "Authorization" header then add that reason.
// This does not use `IsSameOriginForTreeResult` because we want to be more
// conservative and react to *any* same-origin frame using it.
if (root_rfh_->GetBackForwardCacheDisablingFeatures().Has(
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore) &&
rfh->GetLastCommittedOrigin().IsSameOriginWith(
root_rfh_->GetLastCommittedOrigin()) &&
rfh->GetBackForwardCacheDisablingFeatures().Has(
WebSchedulerTrackedFeature::kAuthorizationHeader)) {
result.NoDueToFeatures(WebSchedulerTrackedFeature::kAuthorizationHeader);
}
}
void BackForwardCacheImpl::NotRestoredReasonBuilder::
PopulateNonStickyReasonsForDocument(
BackForwardCacheCanStoreDocumentResult& result,
RenderFrameHostImpl* rfh) {
if (!rfh->IsDOMContentLoaded())
result.No(BackForwardCacheMetrics::NotRestoredReason::kLoading);
// Check for banned features currently being used. Note that unlike the check
// in CanStoreRenderFrameHostLater, we are checking all banned features here
// (not only the "sticky" features), because this time we're making a decision
// on whether we should store a page in the back-forward cache or not.
WebSchedulerTrackedFeatures banned_features =
Intersection(GetDisallowedFeatures(RequestedFeatures::kAll),
rfh->GetBackForwardCacheDisablingFeatures());
if (!banned_features.Empty() && !ShouldIgnoreBlocklists() &&
rfh->render_view_host()->DidReceiveBackForwardCacheAck()) {
result.NoDueToFeatures(banned_features);
}
// Do not cache if we have navigations in any of the subframes.
if (rfh->GetParentOrOuterDocument() &&
rfh->frame_tree_node()->HasNavigation()) {
result.No(
BackForwardCacheMetrics::NotRestoredReason::kSubframeIsNavigating);
}
}
void BackForwardCacheImpl::NotRestoredReasonBuilder::PopulateReasonsForDocument(
BackForwardCacheCanStoreDocumentResult& result,
RenderFrameHostImpl* rfh,
bool include_non_sticky) {
PopulateStickyReasonsForDocument(result, rfh);
if (include_non_sticky) {
PopulateNonStickyReasonsForDocument(result, rfh);
}
}
BackForwardCacheCanStoreDocumentResultWithTree
BackForwardCacheImpl::CreateEvictionBackForwardCacheCanStoreTreeResult(
RenderFrameHostImpl& rfh,
BackForwardCacheCanStoreDocumentResult& eviction_reason) {
// At this point the page already has some NotRestoredReasons for eviction, so
// we should always record cache_control:no-store related reasons.
BackForwardCacheImpl::NotRestoredReasonBuilder builder(
rfh.GetOutermostMainFrame(),
/* include_non_sticky = */ false,
BackForwardCacheImpl::NotRestoredReasonBuilder::EvictionInfo(
rfh, &eviction_reason));
BackForwardCacheCanStoreDocumentResult flattened_result =
builder.GetFlattenedResult();
LogAndTraceResult(
rfh, flattened_result,
"BackForwardCacheImpl::CreateEvictionBackForwardCacheCanStoreTreeResult");
return BackForwardCacheCanStoreDocumentResultWithTree(
flattened_result, builder.GetTreeResult());
}
BackForwardCacheImpl::NotRestoredReasonBuilder::NotRestoredReasonBuilder(
RenderFrameHostImpl* root_rfh,
bool include_non_sticky)
: NotRestoredReasonBuilder(root_rfh,
include_non_sticky,
/* eviction_info = */ absl::nullopt) {}
BackForwardCacheImpl::NotRestoredReasonBuilder::NotRestoredReasonBuilder(
RenderFrameHostImpl* root_rfh,
bool include_non_sticky,
absl::optional<EvictionInfo> eviction_info)
: root_rfh_(root_rfh),
bfcache_(root_rfh_->frame_tree_node()
->navigator()
.controller()
.GetBackForwardCache()),
include_non_sticky_(include_non_sticky),
eviction_info_(eviction_info) {
// |root_rfh_| should be either primary main frame or back/forward cached
// page's outermost main frame.
DCHECK(
root_rfh_->IsInPrimaryMainFrame() ||
(root_rfh_->IsInBackForwardCache() && root_rfh_->IsOutermostMainFrame()));
// Populate the reasons and build the tree.
std::map<RenderFrameHostImpl*, BackForwardCacheCanStoreTreeResult*>
parent_map;
root_rfh_->ForEachRenderFrameHost([&](RenderFrameHostImpl* rfh) {
auto rfh_result = PopulateReasons(rfh);
parent_map[rfh] = rfh_result.get();
if (rfh == root_rfh_) {
tree_result_ = std::move(rfh_result);
} else {
RenderFrameHostImpl* parent = rfh->GetParentOrOuterDocumentOrEmbedder();
// TODO(https://crbug.com/1257276): parent can return null for unattached
// guests.
if (!parent)
parent = root_rfh_;
parent_map[parent]->AppendChild(std::move(rfh_result));
}
});
}
BackForwardCacheImpl::NotRestoredReasonBuilder::~NotRestoredReasonBuilder() =
default;
std::unique_ptr<BackForwardCacheCanStoreTreeResult>
BackForwardCacheImpl::NotRestoredReasonBuilder::PopulateReasons(
RenderFrameHostImpl* rfh) {
BackForwardCacheCanStoreDocumentResult result_for_rfh;
if (eviction_info_.has_value()) {
// When |eviction_info_| is set, that means that we are populating the
// reasons for eviction. In that case, we do not need to check each frame's
// eligibility, but only mark |rfh_to_be_evicted| with |reasons|, as it is
// the cause of eviction.
if (rfh == eviction_info_->rfh_to_be_evicted) {
result_for_rfh.AddReasonsFrom(*(eviction_info_->reasons));
}
} else {
// Populate |result_for_rfh| by checking the bfcache eligibility of |rfh|.
PopulateReasonsForDocument(result_for_rfh, rfh, include_non_sticky_);
}
bfcache_->UpdateCanStoreToIncludeCacheControlNoStore(result_for_rfh, rfh);
flattened_result_.AddReasonsFrom(result_for_rfh);
std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree(
new BackForwardCacheCanStoreTreeResult(
rfh, root_rfh_->GetLastCommittedOrigin(), rfh->GetLastCommittedURL(),
result_for_rfh));
return tree;
}
void BackForwardCacheImpl::StoreEntry(
std::unique_ptr<BackForwardCacheImpl::Entry> entry) {
TRACE_EVENT("navigation", "BackForwardCache::StoreEntry", "entry", entry);
DCHECK(GetCurrentBackForwardCacheEligibility(entry->render_frame_host())
.CanStore());
#if BUILDFLAG(IS_ANDROID)
if (!IsProcessBindingEnabled()) {
// Set the priority of the main frame on entering the back-forward cache to
// make sure the page gets evicted instead of foreground tab. This might not
// become the effective priority of the process if it owns other higher
// priority RenderWidgetHost. We don't need to reset the priority in
// RestoreEntry as it is taken care by WebContentsImpl::NotifyFrameSwapped
// on restoration.
RenderWidgetHostImpl* rwh =
entry->render_frame_host()->GetRenderWidgetHost();
ChildProcessImportance current_importance = rwh->importance();
rwh->SetImportance(
std::min(current_importance, kChildProcessImportanceParam.Get()));
}
#endif
entry->render_frame_host()->DidEnterBackForwardCache();
if (AllowStoringPagesWithCacheControlNoStore()) {
if (entry->render_frame_host()->GetBackForwardCacheDisablingFeatures().Has(
WebSchedulerTrackedFeature::kMainResourceHasCacheControlNoStore)) {
// Start monitoring the cookie change only when cache-control:no-store
// header is present.
entry->StartMonitoringCookieChange();
}
}
entry->SetStoredPageDelegate(this);
entries_.push_front(std::move(entry));
AddProcessesForEntry(*entries_.front());
EnforceCacheSizeLimit();
}
void BackForwardCacheImpl::EnforceCacheSizeLimit() {
if (!IsBackForwardCacheEnabled())
return;
if (UsingForegroundBackgroundCacheSizeLimit()) {
// First enforce the foregrounded limit. The idea is that we need to
// strictly enforce the limit on pages using foregrounded processes because
// Android will not kill a foregrounded process, however it will kill a
// backgrounded process if there is memory pressue, so we can allow more of
// those to be kept in the cache.
EnforceCacheSizeLimitInternal(GetForegroundedEntriesCacheSize(),
/*foregrounded_only=*/true);
}
EnforceCacheSizeLimitInternal(GetCacheSize(),
/*foregrounded_only=*/false);
}
void BackForwardCacheImpl::Prune(size_t limit) {
EnforceCacheSizeLimitInternal(limit,
/*foregrounded_only=*/false);
}
size_t BackForwardCacheImpl::EnforceCacheSizeLimitInternal(
size_t limit,
bool foregrounded_only) {
size_t count = 0;
size_t not_received_ack_count = 0;
for (auto& stored_entry : entries_) {
if (stored_entry->render_frame_host()->is_evicted_from_back_forward_cache())
continue;
if (foregrounded_only && !HasForegroundedProcess(*stored_entry))
continue;
if (!AllRenderViewHostsReceivedAckFromRenderer(*stored_entry)) {
not_received_ack_count++;
continue;
}
if (++count > limit) {
stored_entry->render_frame_host()->EvictFromBackForwardCacheWithReason(
foregrounded_only
? BackForwardCacheMetrics::NotRestoredReason::
kForegroundCacheLimit
: BackForwardCacheMetrics::NotRestoredReason::kCacheLimit);
}
}
UMA_HISTOGRAM_COUNTS_100(
"BackForwardCache.AllSites.HistoryNavigationOutcome."
"CountEntriesWithoutRendererAck",
not_received_ack_count);
return count;
}
std::unique_ptr<BackForwardCacheImpl::Entry> BackForwardCacheImpl::RestoreEntry(
int navigation_entry_id,
blink::mojom::PageRestoreParamsPtr page_restore_params) {
TRACE_EVENT0("navigation", "BackForwardCache::RestoreEntry");
// Select the RenderFrameHostImpl matching the navigation entry.
auto matching_entry = base::ranges::find(
entries_, navigation_entry_id, [](std::unique_ptr<Entry>& entry) {
return entry->render_frame_host()->nav_entry_id();
});
// Not found.
if (matching_entry == entries_.end())
return nullptr;
// Don't restore an evicted frame.
if ((*matching_entry)
->render_frame_host()
->is_evicted_from_back_forward_cache())
return nullptr;
std::unique_ptr<Entry> entry = std::move(*matching_entry);
TRACE_EVENT_INSTANT("navigation",
"BackForwardCache::RestoreEntry_matched_entry", "entry",
entry);
entry->SetStoredPageDelegate(nullptr);
entries_.erase(matching_entry);
RemoveProcessesForEntry(*entry);
base::TimeTicks start_time = page_restore_params->navigation_start;
entry->SetPageRestoreParams(std::move(page_restore_params));
RequestRecordTimeToVisible(entry->render_frame_host(), start_time);
entry->render_frame_host()->WillLeaveBackForwardCache();
RestoreBrowserControlsState(entry->render_frame_host());
return entry;
}
void BackForwardCacheImpl::Flush() {
TRACE_EVENT0("navigation", "BackForwardCache::Flush");
for (std::unique_ptr<Entry>& entry : entries_) {
entry->render_frame_host()->EvictFromBackForwardCacheWithReason(
BackForwardCacheMetrics::NotRestoredReason::kCacheFlushed);
}
}
void BackForwardCacheImpl::Shutdown() {
if (UsingForegroundBackgroundCacheSizeLimit()) {
for (auto& entry : entries_)
RemoveProcessesForEntry(*entry.get());
}
entries_.clear();
}
void BackForwardCacheImpl::EvictFramesInRelatedSiteInstances(
SiteInstance* site_instance) {
for (std::unique_ptr<Entry>& entry : entries_) {
if (entry->render_frame_host()->GetSiteInstance()->IsRelatedSiteInstance(
site_instance)) {
entry->render_frame_host()->EvictFromBackForwardCacheWithReason(
BackForwardCacheMetrics::NotRestoredReason::
kConflictingBrowsingInstance);
}
}
}
void BackForwardCacheImpl::PostTaskToDestroyEvictedFrames() {
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&BackForwardCacheImpl::DestroyEvictedFrames,
weak_factory_.GetWeakPtr()));
}
// static
bool BackForwardCache::IsBackForwardCacheFeatureEnabled() {
return IsBackForwardCacheEnabled();
}
// static
void BackForwardCache::DisableForRenderFrameHost(
RenderFrameHost* render_frame_host,
BackForwardCache::DisabledReason reason) {
DisableForRenderFrameHost(render_frame_host->GetGlobalId(), reason);
}
// static
void BackForwardCache::DisableForRenderFrameHost(
GlobalRenderFrameHostId id,
BackForwardCache::DisabledReason reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (g_bfcache_disabled_test_observer)
g_bfcache_disabled_test_observer->OnDisabledForFrameWithReason(id, reason);
if (auto* rfh = RenderFrameHostImpl::FromID(id))
rfh->DisableBackForwardCache(reason);
}
void BackForwardCacheImpl::DisableForTesting(DisableForTestingReason reason) {
is_disabled_for_testing_ = true;
// Flush all the entries to make sure there are no entries in the cache after
// DisableForTesting() is called.
Flush();
}
const std::list<std::unique_ptr<BackForwardCacheImpl::Entry>>&
BackForwardCacheImpl::GetEntries() {
return entries_;
}
BackForwardCacheImpl::Entry* BackForwardCacheImpl::GetEntry(
int navigation_entry_id) {
auto matching_entry = base::ranges::find(
entries_, navigation_entry_id, [](std::unique_ptr<Entry>& entry) {
return entry->render_frame_host()->nav_entry_id();
});
if (matching_entry == entries_.end())
return nullptr;
auto* render_frame_host = (*matching_entry)->render_frame_host();
// If we are in the experiments to allow pages with cache-control:no-store
// in back/forward cache and the page has cache-control:no-store, we should
// record them as reasons. It might not be possible to restore the entry even
// if it hasn't been evicted up until this point, e.g. due to cache-control:
// no-store preventing restoration but not storage
BackForwardCacheCanStoreDocumentResultWithTree bfcache_eligibility =
GetCurrentBackForwardCacheEligibility(render_frame_host);
if (!bfcache_eligibility.CanRestore()) {
render_frame_host->EvictFromBackForwardCacheWithFlattenedAndTreeReasons(
bfcache_eligibility);
}
// Don't return the frame if it is evicted.
if (render_frame_host->is_evicted_from_back_forward_cache())
return nullptr;
return (*matching_entry).get();
}
void BackForwardCacheImpl::RenderViewHostNoLongerStored(
RenderViewHostImpl* rvh) {
// `AddProcessesForEntry` are gated on
// `UsingForegroundBackgroundCacheSizeLimit` in adding entries to the
// `observed_processes_` list so we have the same conditional here.
if (!UsingForegroundBackgroundCacheSizeLimit())
return;
RenderViewHostNoLongerStoredInternal(rvh);
}
void BackForwardCacheImpl::RenderViewHostNoLongerStoredInternal(
RenderViewHostImpl* rvh) {
RenderProcessHostImpl* process =
static_cast<RenderProcessHostImpl*>(rvh->GetProcess());
// Remove 1 instance of this process from the multiset.
observed_processes_.erase(observed_processes_.find(process));
if (observed_processes_.find(process) == observed_processes_.end())
process->RemoveInternalObserver(this);
}
void BackForwardCacheImpl::AddProcessesForEntry(Entry& entry) {
if (!UsingForegroundBackgroundCacheSizeLimit())
return;
for (const auto& rvh : entry.render_view_hosts()) {
RenderProcessHostImpl* process =
static_cast<RenderProcessHostImpl*>(rvh->GetProcess());
if (observed_processes_.find(process) == observed_processes_.end())
process->AddInternalObserver(this);
observed_processes_.insert(process);
}
}
void BackForwardCacheImpl::RemoveProcessesForEntry(Entry& entry) {
if (!UsingForegroundBackgroundCacheSizeLimit())
return;
for (const auto& rvh : entry.render_view_hosts()) {
RenderViewHostNoLongerStoredInternal(&*rvh);
}
}
void BackForwardCacheImpl::DestroyEvictedFrames() {
TRACE_EVENT0("navigation", "BackForwardCache::DestroyEvictedFrames");
if (entries_.empty())
return;
base::EraseIf(entries_, [this](std::unique_ptr<Entry>& entry) {
if (entry->render_frame_host()->is_evicted_from_back_forward_cache()) {
RemoveProcessesForEntry(*entry);
return true;
}
return false;
});
}
bool BackForwardCacheImpl::IsAllowed(const GURL& current_url) {
return IsHostPathAllowed(current_url) && IsQueryAllowed(current_url);
}
bool BackForwardCacheImpl::IsHostPathAllowed(const GURL& current_url) {
// If the current_url matches the blocked host and path, current_url is
// not allowed to be cached.
const auto& it = blocked_urls_.find(current_url.host());
if (it != blocked_urls_.end()) {
for (const std::string& blocked_path : it->second) {
if (base::StartsWith(current_url.path_piece(), blocked_path))
return false;
}
}
// By convention, when |allowed_urls_| is empty, it means there are no
// restrictions about what RenderFrameHost can enter the BackForwardCache.
if (allowed_urls_.empty())
return true;
// Checking for each url in the |allowed_urls_|, if the current_url matches
// the corresponding host and path is the prefix of the allowed url path. We
// only check for host and path and not any other components including url
// scheme here.
const auto& entry = allowed_urls_.find(current_url.host());
if (entry != allowed_urls_.end()) {
for (const std::string& allowed_path : entry->second) {
if (base::StartsWith(current_url.path_piece(), allowed_path))
return true;
}
}
return false;
}
bool BackForwardCacheImpl::IsQueryAllowed(const GURL& current_url) {
std::vector<std::string> cgi_params =
base::SplitString(current_url.query_piece(), "&", base::TRIM_WHITESPACE,
base::SplitResult::SPLIT_WANT_NONEMPTY);
for (const std::string& cgi_param : cgi_params) {
if (base::Contains(blocked_cgi_params_, cgi_param))
return false;
}
return true;
}
void BackForwardCacheImpl::WillCommitNavigationToCachedEntry(
Entry& bfcache_entry,
base::OnceClosure done_callback) {
// Disable JS eviction in renderers and defer the navigation commit until
// we've received confirmation that eviction is disabled from renderers.
auto cb = base::BarrierClosure(
bfcache_entry.render_view_hosts().size(),
base::BindOnce(
[](base::TimeTicks ipc_start_time, base::OnceClosure cb) {
std::move(cb).Run();
base::UmaHistogramTimes(
"BackForwardCache.Restore.DisableEvictionDelay",
base::TimeTicks::Now() - ipc_start_time);
},
base::TimeTicks::Now(), std::move(done_callback)));
for (const auto& rvh : bfcache_entry.render_view_hosts()) {
rvh->PrepareToLeaveBackForwardCache(cb);
}
}
bool BackForwardCacheImpl::AllowStoringPagesWithCacheControlNoStore() {
return GetCacheControlNoStoreLevel() >
CacheControlNoStoreExperimentLevel::kDoNotStore;
}
bool BackForwardCacheImpl::IsBrowsingInstanceInBackForwardCacheForDebugging(
BrowsingInstanceId browsing_instance_id) {
for (std::unique_ptr<Entry>& entry : entries_) {
if (entry->render_frame_host()
->GetSiteInstance()
->GetBrowsingInstanceId() == browsing_instance_id) {
return true;
}
}
return false;
}
bool BackForwardCacheImpl::IsProxyInBackForwardCacheForDebugging(
RenderFrameProxyHost* proxy) {
for (std::unique_ptr<Entry>& entry : entries_) {
for (auto& proxy_map_entry : entry->proxy_hosts()) {
if (proxy_map_entry.second.get() == proxy) {
return true;
}
}
}
return false;
}
bool BackForwardCacheImpl::IsMediaSessionServiceAllowed() {
return base::FeatureList::IsEnabled(
features::kBackForwardCacheMediaSessionService);
}
bool BackForwardCacheImpl::IsScreenReaderAllowed() {
return base::FeatureList::IsEnabled(
features::kEnableBackForwardCacheForScreenReader);
}
// static
void BackForwardCacheImpl::VlogUnexpectedRendererToBrowserMessage(
const char* interface_name,
uint32_t message_name,
RenderFrameHostImpl* rfh) {
VLOG(1) << "BackForwardCacheMessageFilter::WillDispatch bad_message "
<< "interface_name " << interface_name << " message_name "
<< message_name;
// TODO(https://crbug.com/1379490): Remove these when bug is fixed.
PageLifecycleStateManager* page_lifecycle_state_manager =
rfh->render_view_host()->GetPageLifecycleStateManager();
VLOG(1) << "URL: " << rfh->GetLastCommittedURL() << " current "
<< page_lifecycle_state_manager->IsInBackForwardCache() << " acked "
<< page_lifecycle_state_manager->last_acknowledged_state()
.is_in_back_forward_cache;
}
BackForwardCache::DisabledReason::DisabledReason(
content::BackForwardCache::DisabledSource source,
content::BackForwardCache::DisabledReasonType id,
std::string description,
std::string context,
std::string report_string)
: source(source),
id(id),
description(description),
context(context),
report_string(report_string) {}
BackForwardCache::DisabledReason::DisabledReason(
const BackForwardCache::DisabledReason& reason) = default;
bool BackForwardCache::DisabledReason::operator<(
const DisabledReason& other) const {
return std::tie(source, id) < std::tie(other.source, other.id);
}
bool BackForwardCache::DisabledReason::operator==(
const DisabledReason& other) const {
return std::tie(source, id) == std::tie(other.source, other.id);
}
bool BackForwardCache::DisabledReason::operator!=(
const DisabledReason& other) const {
return !(*this == other);
}
BackForwardCacheCanStoreTreeResult::BackForwardCacheCanStoreTreeResult(
RenderFrameHostImpl* rfh,
const url::Origin& main_document_origin,
const GURL& url,
BackForwardCacheCanStoreDocumentResult& result_for_this_document)
: document_result_(std::move(result_for_this_document)),
is_same_origin_(
IsSameOriginForTreeResult(rfh, url, main_document_origin)),
id_(rfh->frame_tree_node()->html_id()),
name_(rfh->frame_tree_node()->html_name()),
src_(rfh->frame_tree_node()->html_src()),
url_(url) {}
BackForwardCacheCanStoreTreeResult::BackForwardCacheCanStoreTreeResult(
bool is_same_origin,
const GURL& url)
: document_result_(BackForwardCacheCanStoreDocumentResult()),
is_same_origin_(is_same_origin),
id_(""),
name_(""),
src_(""),
url_(url) {}
BackForwardCacheCanStoreTreeResult::~BackForwardCacheCanStoreTreeResult() =
default;
void BackForwardCacheCanStoreTreeResult::AddReasonsToSubtreeRootFrom(
const BackForwardCacheCanStoreDocumentResult& result) {
document_result_.AddReasonsFrom(result);
}
void BackForwardCacheCanStoreTreeResult::AppendChild(
std::unique_ptr<BackForwardCacheCanStoreTreeResult> child) {
children_.push_back(std::move(child));
}
const BackForwardCacheCanStoreDocumentResult
BackForwardCacheCanStoreTreeResult::FlattenTree() {
BackForwardCacheCanStoreDocumentResult document_result;
FlattenTreeHelper(&document_result);
return document_result;
}
void BackForwardCacheCanStoreTreeResult::FlattenTreeHelper(
BackForwardCacheCanStoreDocumentResult* document_result) {
document_result->AddReasonsFrom(document_result_);
for (const auto& subtree : GetChildren()) {
subtree->FlattenTreeHelper(document_result);
}
}
std::unique_ptr<BackForwardCacheCanStoreTreeResult>
BackForwardCacheCanStoreTreeResult::CreateEmptyTreeForNavigation(
NavigationRequest* navigation) {
DCHECK(BackForwardCacheMetrics::IsCrossDocumentMainFrameHistoryNavigation(
navigation));
std::unique_ptr<BackForwardCacheCanStoreTreeResult> empty_tree(
new BackForwardCacheCanStoreTreeResult(
/*is_same_origin=*/true, navigation->GetURL()));
return empty_tree;
}
std::unique_ptr<BackForwardCacheCanStoreTreeResult>
BackForwardCacheCanStoreTreeResult::CreateEmptyTree(RenderFrameHostImpl* rfh) {
BackForwardCacheCanStoreDocumentResult empty_result;
std::unique_ptr<BackForwardCacheCanStoreTreeResult> empty_tree(
new BackForwardCacheCanStoreTreeResult(rfh, rfh->GetLastCommittedOrigin(),
rfh->GetLastCommittedURL(),
empty_result));
return empty_tree;
}
blink::mojom::BackForwardCacheNotRestoredReasonsPtr
BackForwardCacheCanStoreTreeResult::GetWebExposedNotRestoredReasons() {
uint32_t count = GetCrossOriginReachableFrameCount();
int index = count == 0 ? 0 : base::RandInt(0, count - 1);
return GetWebExposedNotRestoredReasonsInternal(index);
}
blink::mojom::BackForwardCacheNotRestoredReasonsPtr
BackForwardCacheCanStoreTreeResult::GetWebExposedNotRestoredReasonsInternal(
int& index) {
blink::mojom::BackForwardCacheNotRestoredReasonsPtr not_restored_reasons =
blink::mojom::BackForwardCacheNotRestoredReasons::New();
if (IsSameOrigin()) {
// Only include same_origin_details for documents that are same-origin with
// the main document. Stop recursion as soon as we hit a cross-origin
// document.
not_restored_reasons->same_origin_details =
blink::mojom::SameOriginBfcacheNotRestoredDetails::New();
not_restored_reasons->same_origin_details->url = url_.spec();
not_restored_reasons->same_origin_details->src = src_;
not_restored_reasons->same_origin_details->id = id_;
not_restored_reasons->same_origin_details->name = name_;
not_restored_reasons->same_origin_details->reasons =
GetDocumentResult().GetStringReasons();
not_restored_reasons->blocked = GetDocumentResult().CanRestore()
? blink::mojom::BFCacheBlocked::kNo
: blink::mojom::BFCacheBlocked::kYes;
for (const auto& subtree : GetChildren()) {
not_restored_reasons->same_origin_details->children.push_back(
subtree->GetWebExposedNotRestoredReasonsInternal(index));
}
} else {
// If the subtree's root document is cross-origin from the main frame
// document, and if this is the randomly selected cross-origin iframe,
// report whether or not this entire subtree is blocking back/forward cache.
if (index == 0) {
not_restored_reasons->blocked =
(!GetDocumentResult().CanRestore() || !FlattenTree().CanRestore())
? blink::mojom::BFCacheBlocked::kYes
: blink::mojom::BFCacheBlocked::kNo;
} else {
not_restored_reasons->blocked = blink::mojom::BFCacheBlocked::kMasked;
}
// Decrease the index now that we saw a cross-origin iframe.
index--;
}
return not_restored_reasons;
}
uint32_t
BackForwardCacheCanStoreTreeResult::GetCrossOriginReachableFrameCount() {
// If the document is cross-origin, we cannot reach any further. Only count
// the one we have reached and return.
if (!IsSameOrigin())
return 1;
uint32_t count = 0;
for (const auto& subtree : GetChildren()) {
count += subtree->GetCrossOriginReachableFrameCount();
}
return count;
}
BackForwardCacheCanStoreDocumentResultWithTree::
BackForwardCacheCanStoreDocumentResultWithTree(
BackForwardCacheCanStoreDocumentResult& flattened_reasons,
std::unique_ptr<BackForwardCacheCanStoreTreeResult> tree_reasons)
: flattened_reasons(std::move(flattened_reasons)),
tree_reasons(std::move(tree_reasons)) {}
BackForwardCacheCanStoreDocumentResultWithTree::
~BackForwardCacheCanStoreDocumentResultWithTree() = default;
} // namespace content