blob: 4eaceb2fc571f68e4d7dfb449c0bf6ba31d9a707 [file] [log] [blame]
// Copyright 2019 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 "content/browser/renderer_host/back_forward_cache_metrics.h"
#include "base/debug/dump_without_crashing.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/metrics_hashes.h"
#include "base/metrics/sparse_histogram.h"
#include "content/browser/devtools/devtools_instrumentation.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_entry_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/should_swap_browsing_instance.h"
#include "content/browser/site_instance_impl.h"
#include "content/common/debug_utils.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/reload_type.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/scheduler/web_scheduler_tracked_feature.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
// Overridden time for unit tests. Should be accessed only from the main thread.
base::TickClock* g_mock_time_clock_for_testing = nullptr;
// Reduce the resolution of the longer intervals due to privacy considerations.
base::TimeDelta ClampTime(base::TimeDelta time) {
if (time < base::Seconds(5))
return base::Milliseconds(time.InMilliseconds());
if (time < base::Minutes(3))
return base::Seconds(time.InSeconds());
if (time < base::Hours(3))
return base::Minutes(time.InMinutes());
return base::Hours(time.InHours());
}
base::TimeTicks Now() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (g_mock_time_clock_for_testing)
return g_mock_time_clock_for_testing->NowTicks();
return base::TimeTicks::Now();
}
bool IsHistoryNavigation(NavigationRequest* navigation) {
return navigation->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK;
}
} // namespace
// static
void BackForwardCacheMetrics::OverrideTimeForTesting(base::TickClock* clock) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
g_mock_time_clock_for_testing = clock;
}
// static
scoped_refptr<BackForwardCacheMetrics>
BackForwardCacheMetrics::CreateOrReuseBackForwardCacheMetrics(
NavigationEntryImpl* currently_committed_entry,
bool is_main_frame_navigation,
int64_t document_sequence_number) {
if (!currently_committed_entry) {
// In some rare cases it's possible to navigate a subframe
// without having a main frame navigation (e.g. extensions
// injecting frames into a blank page).
return base::WrapRefCounted(new BackForwardCacheMetrics(
is_main_frame_navigation ? document_sequence_number : -1));
}
BackForwardCacheMetrics* currently_committed_metrics =
currently_committed_entry->back_forward_cache_metrics();
if (!currently_committed_metrics) {
// When we restore the session it's possible to end up with an entry without
// metrics.
// We will have to create a new metrics object for the main document.
return base::WrapRefCounted(new BackForwardCacheMetrics(
is_main_frame_navigation
? document_sequence_number
: currently_committed_entry->root_node()
->frame_entry->document_sequence_number()));
}
if (!is_main_frame_navigation)
return currently_committed_metrics;
if (document_sequence_number ==
currently_committed_metrics->document_sequence_number_) {
return currently_committed_metrics;
}
return base::WrapRefCounted(
new BackForwardCacheMetrics(document_sequence_number));
}
BackForwardCacheMetrics::BackForwardCacheMetrics(
int64_t document_sequence_number)
: document_sequence_number_(document_sequence_number),
page_store_result_(
std::make_unique<BackForwardCacheCanStoreDocumentResult>()) {}
BackForwardCacheMetrics::~BackForwardCacheMetrics() = default;
void BackForwardCacheMetrics::MainFrameDidStartNavigationToDocument() {
if (!started_navigation_timestamp_)
started_navigation_timestamp_ = Now();
}
void BackForwardCacheMetrics::DidCommitNavigation(
NavigationRequest* navigation,
bool back_forward_cache_allowed) {
// "Back-forward cache in enabled only for primary frame trees, so we need to
// record metrics only for primary main frame navigations".
if (!navigation->IsInPrimaryMainFrame() || navigation->IsSameDocument())
return;
{
bool is_reload = navigation->GetReloadType() != ReloadType::NONE;
RecordHistogramForReloadsAndHistoryNavigations(is_reload,
back_forward_cache_allowed);
}
if (IsHistoryNavigation(navigation)) {
UpdateNotRestoredReasonsForNavigation(navigation);
// If a navigation serves the result from back/forward cache, then it must
// not have logged any NotRestoredReasons. Also if it is not restored from
// back/forward cache, the logged reasons must match the actual condition of
// the navigation and other logged data.
bool served_from_bfcache_not_match =
navigation->IsServedFromBackForwardCache() &&
!page_store_result_->not_stored_reasons().Empty();
bool browsing_instance_not_swapped_not_match =
page_store_result_->HasNotStoredReason(
NotRestoredReason::kBrowsingInstanceNotSwapped) &&
DidSwapBrowsingInstance();
bool disable_for_rfh_not_match =
page_store_result_->HasNotStoredReason(
NotRestoredReason::kDisableForRenderFrameHostCalled) &&
page_store_result_->disabled_reasons().size() == 0;
bool blocklisted_features_not_match =
page_store_result_->HasNotStoredReason(
NotRestoredReason::kBlocklistedFeatures) &&
page_store_result_->blocklisted_features().Empty();
if (served_from_bfcache_not_match ||
browsing_instance_not_swapped_not_match || disable_for_rfh_not_match ||
blocklisted_features_not_match) {
CaptureTraceForNavigationDebugScenario(
DebugScenario::kDebugBackForwardCacheMetricsMismatch);
}
TRACE_EVENT1("navigation", "HistoryNavigationOutcome", "outcome",
page_store_result_->ToString());
RecordMetricsForHistoryNavigationCommit(navigation,
back_forward_cache_allowed);
RecordHistoryNavigationUkm(navigation);
if (!navigation->IsServedFromBackForwardCache()) {
devtools_instrumentation::BackForwardCacheNotUsed(
navigation, page_store_result_.get());
}
}
page_store_result_ =
std::make_unique<BackForwardCacheCanStoreDocumentResult>();
previous_navigation_is_served_from_bfcache_ =
navigation->IsServedFromBackForwardCache();
previous_navigation_is_history_ = IsHistoryNavigation(navigation);
last_committed_cross_document_main_frame_navigation_id_ =
navigation->GetNavigationId();
// BackForwardCacheMetrics can be reused when reloading. Reset fields for UKM
// for the next navigation.
navigated_away_from_main_document_timestamp_ = absl::nullopt;
started_navigation_timestamp_ = absl::nullopt;
renderer_killed_timestamp_ = absl::nullopt;
browsing_instance_swap_result_ = absl::nullopt;
}
void BackForwardCacheMetrics::RecordHistoryNavigationUkm(
NavigationRequest* navigation) {
// If |IsHistoryNavigation| is true and
// |last_committed_cross_document_main_frame_navigation_id_| is not -1, it's a
// history navigation which we're interested in.
//
// |IsHistoryNavigation| is true when the navigation is history navigation,
// but just after cloning, the metrics object is missing. Then, checking this
// is not enough. |last_committed_cross_document_main_frame_navigation_id_| is
// not -1 when the metrics object is available.
if (!IsHistoryNavigation(navigation))
return;
if (last_committed_cross_document_main_frame_navigation_id_ == -1)
return;
// We've visited an entry associated with this main frame document before,
// so record metrics to determine whether it might be a back-forward cache
// hit.
ukm::SourceId source_id = ukm::ConvertToSourceId(
navigation->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID);
ukm::builders::HistoryNavigation builder(source_id);
builder.SetLastCommittedCrossDocumentNavigationSourceIdForTheSameDocument(
ukm::ConvertToSourceId(
last_committed_cross_document_main_frame_navigation_id_,
ukm::SourceIdType::NAVIGATION_ID));
builder.SetMainFrameFeatures(main_frame_features_.ToEnumBitmask());
builder.SetSameOriginSubframesFeatures(
same_origin_frames_features_.ToEnumBitmask());
builder.SetCrossOriginSubframesFeatures(
cross_origin_frames_features_.ToEnumBitmask());
// DidStart notification might be missing for some same-document
// navigations. It's good that we don't care about the time in the cache
// in that case.
if (started_navigation_timestamp_ &&
navigated_away_from_main_document_timestamp_) {
builder.SetTimeSinceNavigatedAwayFromDocument(
ClampTime(started_navigation_timestamp_.value() -
navigated_away_from_main_document_timestamp_.value())
.InMilliseconds());
}
builder.SetBackForwardCache_IsServedFromBackForwardCache(
navigation->IsServedFromBackForwardCache());
builder.SetBackForwardCache_NotRestoredReasons(
page_store_result_->not_stored_reasons().ToEnumBitmask());
builder.SetBackForwardCache_BlocklistedFeatures(
page_store_result_->blocklisted_features().ToEnumBitmask());
if (browsing_instance_swap_result_) {
builder.SetBackForwardCache_BrowsingInstanceNotSwappedReason(
static_cast<int64_t>(browsing_instance_swap_result_.value()));
}
builder.SetBackForwardCache_DisabledForRenderFrameHostReasonCount(
page_store_result_->disabled_reasons().size());
builder.Record(ukm::UkmRecorder::Get());
for (const BackForwardCache::DisabledReason& reason :
page_store_result_->disabled_reasons()) {
ukm::builders::BackForwardCacheDisabledForRenderFrameHostReason
rfh_reason_builder(source_id);
rfh_reason_builder.SetReason2(MetricValue(reason));
rfh_reason_builder.Record(ukm::UkmRecorder::Get());
}
for (const uint64_t reason :
page_store_result_->disallow_activation_reasons()) {
ukm::builders::BackForwardCacheDisallowActivationReason reason_builder(
source_id);
reason_builder.SetReason(reason);
reason_builder.Record(ukm::UkmRecorder::Get());
}
}
void BackForwardCacheMetrics::MainFrameDidNavigateAwayFromDocument(
RenderFrameHostImpl* new_main_frame,
NavigationRequest* navigation) {
// MainFrameDidNavigateAwayFromDocument is called when we commit a navigation
// to another main frame document and the current document loses its "last
// committed" status.
navigated_away_from_main_document_timestamp_ = Now();
}
void BackForwardCacheMetrics::RecordFeatureUsage(
RenderFrameHostImpl* main_frame) {
DCHECK(!main_frame->GetParent());
main_frame_features_.Clear();
same_origin_frames_features_.Clear();
cross_origin_frames_features_.Clear();
CollectFeatureUsageFromSubtree(main_frame,
main_frame->GetLastCommittedOrigin());
}
void BackForwardCacheMetrics::CollectFeatureUsageFromSubtree(
RenderFrameHostImpl* rfh,
const url::Origin& main_frame_origin) {
blink::scheduler::WebSchedulerTrackedFeatures features =
rfh->scheduler_tracked_features();
if (!rfh->GetParent()) {
main_frame_features_.PutAll(features);
} else if (rfh->GetLastCommittedOrigin().IsSameOriginWith(
main_frame_origin)) {
same_origin_frames_features_.PutAll(features);
} else {
cross_origin_frames_features_.PutAll(features);
}
for (size_t i = 0; i < rfh->child_count(); ++i) {
CollectFeatureUsageFromSubtree(rfh->child_at(i)->current_frame_host(),
main_frame_origin);
}
}
void BackForwardCacheMetrics::MarkNotRestoredWithReason(
const BackForwardCacheCanStoreDocumentResult& can_store) {
page_store_result_->AddReasonsFrom(can_store);
const BackForwardCacheCanStoreDocumentResult::NotStoredReasons&
not_stored_reasons = can_store.not_stored_reasons();
if (not_stored_reasons.Has(NotRestoredReason::kRendererProcessKilled)) {
renderer_killed_timestamp_ = Now();
}
if (!not_stored_reasons.Has(NotRestoredReason::kHTTPStatusNotOK) &&
!not_stored_reasons.Has(NotRestoredReason::kSchemeNotHTTPOrHTTPS) &&
not_stored_reasons.Has(NotRestoredReason::kNoResponseHead)) {
CaptureTraceForNavigationDebugScenario(
DebugScenario::kDebugNoResponseHeadForHttpOrHttps);
base::debug::DumpWithoutCrashing();
}
}
void BackForwardCacheMetrics::UpdateNotRestoredReasonsForNavigation(
NavigationRequest* navigation) {
// |last_committed_cross_document_main_frame_navigation_id_| is -1 when
// navigation history has never been initialized. This can happen only when
// the session history has been restored.
if (last_committed_cross_document_main_frame_navigation_id_ == -1) {
page_store_result_->No(NotRestoredReason::kSessionRestored);
}
if (!DidSwapBrowsingInstance()) {
page_store_result_->No(NotRestoredReason::kBrowsingInstanceNotSwapped);
}
// This should not happen, but record this as an 'unknown' reason just in
// case.
if (page_store_result_->not_stored_reasons().Empty() &&
!navigation->IsServedFromBackForwardCache()) {
page_store_result_->No(NotRestoredReason::kUnknown);
// TODO(altimin): Add a (D)CHECK here, but this code is reached in
// unittests.
return;
}
}
void BackForwardCacheMetrics::RecordMetricsForHistoryNavigationCommit(
NavigationRequest* navigation,
bool back_forward_cache_allowed) const {
HistoryNavigationOutcome outcome = HistoryNavigationOutcome::kNotRestored;
if (navigation->IsServedFromBackForwardCache()) {
outcome = HistoryNavigationOutcome::kRestored;
if (back_forward_cache_allowed) {
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.EvictedAfterDocumentRestoredReason",
EvictedAfterDocumentRestoredReason::kRestored);
}
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.EvictedAfterDocumentRestoredReason",
EvictedAfterDocumentRestoredReason::kRestored);
}
if (back_forward_cache_allowed) {
UMA_HISTOGRAM_ENUMERATION("BackForwardCache.HistoryNavigationOutcome",
outcome);
// Record total number of history navigations for all websites allowed by
// back-forward cache.
UMA_HISTOGRAM_ENUMERATION("BackForwardCache.ReloadsAndHistoryNavigations",
ReloadsAndHistoryNavigations::kHistoryNavigation);
}
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.HistoryNavigationOutcome", outcome);
for (NotRestoredReason reason : page_store_result_->not_stored_reasons()) {
DCHECK(!navigation->IsServedFromBackForwardCache());
if (back_forward_cache_allowed) {
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.HistoryNavigationOutcome.NotRestoredReason",
reason);
}
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.HistoryNavigationOutcome.NotRestoredReason",
reason);
if (reason == NotRestoredReason::kRendererProcessKilled) {
DCHECK(renderer_killed_timestamp_);
DCHECK(navigated_away_from_main_document_timestamp_);
base::TimeDelta time =
renderer_killed_timestamp_.value() -
navigated_away_from_main_document_timestamp_.value();
UMA_HISTOGRAM_LONG_TIMES(
"BackForwardCache.Eviction.TimeUntilProcessKilled", time);
}
}
for (blink::scheduler::WebSchedulerTrackedFeature feature :
page_store_result_->blocklisted_features()) {
if (back_forward_cache_allowed) {
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.HistoryNavigationOutcome.BlocklistedFeature",
feature);
}
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.HistoryNavigationOutcome."
"BlocklistedFeature",
feature);
}
for (const BackForwardCache::DisabledReason& reason :
page_store_result_->disabled_reasons()) {
// Use SparseHistogram instead of other simple macros for metrics. The
// reasons cannot be represented as a unified enum because they come from
// multiple sources. At first they were represented as strings but that
// makes it hard to track new additions. Now they are represented by
// a combination of source and source-specific enum.
base::UmaHistogramSparse(
"BackForwardCache.HistoryNavigationOutcome."
"DisabledForRenderFrameHostReason2",
MetricValue(reason));
}
for (const uint64_t reason :
page_store_result_->disallow_activation_reasons()) {
base::UmaHistogramSparse(
"BackForwardCache.HistoryNavigationOutcome."
"DisallowActivationReason",
reason);
}
if (!DidSwapBrowsingInstance()) {
DCHECK(!navigation->IsServedFromBackForwardCache());
if (back_forward_cache_allowed) {
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.HistoryNavigationOutcome."
"BrowsingInstanceNotSwappedReason",
browsing_instance_swap_result_.value());
}
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.HistoryNavigationOutcome."
"BrowsingInstanceNotSwappedReason",
browsing_instance_swap_result_.value());
}
}
void BackForwardCacheMetrics::RecordEvictedAfterDocumentRestored(
EvictedAfterDocumentRestoredReason reason) {
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.EvictedAfterDocumentRestoredReason", reason);
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.AllSites.EvictedAfterDocumentRestoredReason", reason);
}
void BackForwardCacheMetrics::RecordHistogramForReloadsAndHistoryNavigations(
bool is_reload,
bool back_forward_cache_allowed) const {
if (!is_reload)
return;
if (!previous_navigation_is_history_)
return;
if (!back_forward_cache_allowed)
return;
// Record the total number of reloads after a history navigation.
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.ReloadsAndHistoryNavigations",
ReloadsAndHistoryNavigations::kReloadAfterHistoryNavigation);
// Record separate buckets for cases served and not served from
// back-forward cache.
UMA_HISTOGRAM_ENUMERATION(
"BackForwardCache.ReloadsAfterHistoryNavigation",
previous_navigation_is_served_from_bfcache_
? ReloadsAfterHistoryNavigation::kServedFromBackForwardCache
: ReloadsAfterHistoryNavigation::kNotServedFromBackForwardCache);
}
// static
uint64_t BackForwardCacheMetrics::MetricValue(
BackForwardCache::DisabledReason reason) {
return static_cast<BackForwardCache::DisabledReasonType>(reason.source)
<< BackForwardCache::kDisabledReasonTypeBits |
reason.id;
}
void BackForwardCacheMetrics::SetBrowsingInstanceSwapResult(
absl::optional<ShouldSwapBrowsingInstance> reason) {
browsing_instance_swap_result_ = reason;
}
bool BackForwardCacheMetrics::DidSwapBrowsingInstance() const {
if (!browsing_instance_swap_result_)
return true;
switch (browsing_instance_swap_result_.value()) {
case ShouldSwapBrowsingInstance::kNo_ProactiveSwapDisabled:
case ShouldSwapBrowsingInstance::kNo_NotMainFrame:
case ShouldSwapBrowsingInstance::kNo_HasRelatedActiveContents:
case ShouldSwapBrowsingInstance::kNo_DoesNotHaveSite:
case ShouldSwapBrowsingInstance::kNo_SourceURLSchemeIsNotHTTPOrHTTPS:
case ShouldSwapBrowsingInstance::kNo_SameSiteNavigation:
case ShouldSwapBrowsingInstance::kNo_AlreadyHasMatchingBrowsingInstance:
case ShouldSwapBrowsingInstance::kNo_RendererDebugURL:
case ShouldSwapBrowsingInstance::kNo_NotNeededForBackForwardCache:
case ShouldSwapBrowsingInstance::kNo_SameDocumentNavigation:
case ShouldSwapBrowsingInstance::kNo_SameUrlNavigation:
case ShouldSwapBrowsingInstance::kNo_WillReplaceEntry:
case ShouldSwapBrowsingInstance::kNo_Reload:
case ShouldSwapBrowsingInstance::kNo_Guest:
case ShouldSwapBrowsingInstance::kNo_HasNotComittedAnyNavigation:
case ShouldSwapBrowsingInstance::
kNo_UnloadHandlerExistsOnSameSiteNavigation:
return false;
case ShouldSwapBrowsingInstance::kYes_ForceSwap:
case ShouldSwapBrowsingInstance::kYes_CrossSiteProactiveSwap:
case ShouldSwapBrowsingInstance::kYes_SameSiteProactiveSwap:
return true;
}
}
} // namespace content