blob: 78c9d3e850a07475fbe7ea327d7f13b13aec3eb7 [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 "components/page_load_metrics/browser/observers/core/largest_contentful_paint_handler.h"
#include "components/page_load_metrics/browser/page_load_metrics_observer_delegate.h"
#include "components/page_load_metrics/common/page_load_metrics.mojom.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/site_instance.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/common/performance/largest_contentful_paint_type.h"
namespace page_load_metrics {
// TODO(crbug/616901): True in test only. Since we are unable to config
// navigation start in tests, we disable the offsetting to make the test
// deterministic.
static bool g_disable_subframe_navigation_start_offset = false;
namespace {
const ContentfulPaintTimingInfo& MergeTimingsBySizeAndTime(
const ContentfulPaintTimingInfo& timing1,
const ContentfulPaintTimingInfo& timing2) {
// When both are empty, just return either.
if (timing1.Empty() && timing2.Empty())
return timing1;
if (timing1.Empty() && !timing2.Empty())
return timing2;
if (!timing1.Empty() && timing2.Empty())
return timing1;
if (timing1.Size() > timing2.Size())
return timing1;
if (timing1.Size() < timing2.Size())
return timing2;
// When both sizes are equal
DCHECK(timing1.Time());
DCHECK(timing2.Time());
if (timing1.Time().value() < timing2.Time().value())
return timing1;
return timing2;
}
void MergeForSubframesWithAdjustedTime(
ContentfulPaintTimingInfo* inout_timing,
const absl::optional<base::TimeDelta>& candidate_new_time,
const uint64_t& candidate_new_size) {
DCHECK(inout_timing);
const ContentfulPaintTimingInfo new_candidate(
candidate_new_time, candidate_new_size, inout_timing->TextOrImage(),
inout_timing->InMainFrame(), inout_timing->Type(),
inout_timing->ImageBPP());
const ContentfulPaintTimingInfo& merged_candidate =
MergeTimingsBySizeAndTime(new_candidate, *inout_timing);
inout_timing->Reset(merged_candidate.Time(), merged_candidate.Size(),
merged_candidate.Type(), merged_candidate.ImageBPP());
}
bool IsSubframe(content::RenderFrameHost* subframe_rfh) {
return subframe_rfh != nullptr && subframe_rfh->GetParent() != nullptr;
}
void Reset(ContentfulPaintTimingInfo& timing) {
timing.Reset(absl::nullopt, 0u, /*type=*/0, /*image_bpp=*/0.0);
}
bool IsSameSite(const GURL& url1, const GURL& url2) {
// We can't use SiteInstance::IsSameSiteWithURL() because both mainframe and
// subframe are under default SiteInstance on low-end Android environment, and
// it treats them as same-site even though the passed url is actually not a
// same-site.
return url1.SchemeIs(url2.scheme()) &&
net::registry_controlled_domains::SameDomainOrHost(
url1, url2,
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
}
} // namespace
ContentfulPaintTimingInfo::ContentfulPaintTimingInfo(
LargestContentTextOrImage text_or_image,
bool in_main_frame,
uint32_t type)
: size_(0),
text_or_image_(text_or_image),
type_(type),
in_main_frame_(in_main_frame) {}
ContentfulPaintTimingInfo::ContentfulPaintTimingInfo(
const absl::optional<base::TimeDelta>& time,
const uint64_t& size,
const LargestContentTextOrImage text_or_image,
double image_bpp,
bool in_main_frame,
uint32_t type)
: time_(time),
size_(size),
text_or_image_(text_or_image),
type_(type),
image_bpp_(image_bpp),
in_main_frame_(in_main_frame) {}
ContentfulPaintTimingInfo::ContentfulPaintTimingInfo(
const ContentfulPaintTimingInfo& other) = default;
std::unique_ptr<base::trace_event::TracedValue>
ContentfulPaintTimingInfo::DataAsTraceValue() const {
std::unique_ptr<base::trace_event::TracedValue> data =
std::make_unique<base::trace_event::TracedValue>();
data->SetInteger("durationInMilliseconds", time_.value().InMilliseconds());
data->SetInteger("size", size_);
data->SetString("type", TextOrImageInString());
data->SetBoolean("inMainFrame", InMainFrame());
data->SetBoolean(
"isAnimated",
Type() & blink::LargestContentfulPaintType::kLCPTypeAnimatedImage);
return data;
}
std::string ContentfulPaintTimingInfo::TextOrImageInString() const {
switch (TextOrImage()) {
case LargestContentTextOrImage::kText:
return "text";
case LargestContentTextOrImage::kImage:
return "image";
default:
NOTREACHED();
return "NOT_REACHED";
}
}
// static
void LargestContentfulPaintHandler::SetTestMode(bool enabled) {
g_disable_subframe_navigation_start_offset = enabled;
}
void ContentfulPaintTimingInfo::Reset(
const absl::optional<base::TimeDelta>& time,
const uint64_t& size,
uint32_t type,
double image_bpp) {
size_ = size;
time_ = time;
type_ = type;
image_bpp_ = image_bpp;
}
ContentfulPaint::ContentfulPaint(bool in_main_frame, uint32_t type)
: text_(ContentfulPaintTimingInfo::LargestContentTextOrImage::kText,
in_main_frame,
type),
image_(ContentfulPaintTimingInfo::LargestContentTextOrImage::kImage,
in_main_frame,
type) {}
const ContentfulPaintTimingInfo& ContentfulPaint::MergeTextAndImageTiming()
const {
return MergeTimingsBySizeAndTime(text_, image_);
}
// static
bool LargestContentfulPaintHandler::AssignTimeAndSizeForLargestContentfulPaint(
const page_load_metrics::mojom::LargestContentfulPaintTiming&
largest_contentful_paint,
absl::optional<base::TimeDelta>* largest_content_paint_time,
uint64_t* largest_content_paint_size,
ContentfulPaintTimingInfo::LargestContentTextOrImage*
largest_content_type) {
// Size being 0 means the paint time is not recorded.
if (!largest_contentful_paint.largest_text_paint_size &&
!largest_contentful_paint.largest_image_paint_size)
return false;
if ((largest_contentful_paint.largest_text_paint_size >
largest_contentful_paint.largest_image_paint_size) ||
(largest_contentful_paint.largest_text_paint_size ==
largest_contentful_paint.largest_image_paint_size &&
largest_contentful_paint.largest_text_paint <
largest_contentful_paint.largest_image_paint)) {
*largest_content_paint_time = largest_contentful_paint.largest_text_paint;
*largest_content_paint_size =
largest_contentful_paint.largest_text_paint_size;
*largest_content_type =
ContentfulPaintTimingInfo::LargestContentTextOrImage::kText;
} else {
*largest_content_paint_time = largest_contentful_paint.largest_image_paint;
*largest_content_paint_size =
largest_contentful_paint.largest_image_paint_size;
*largest_content_type =
ContentfulPaintTimingInfo::LargestContentTextOrImage::kImage;
}
return true;
}
LargestContentfulPaintHandler::LargestContentfulPaintHandler()
: main_frame_contentful_paint_(true /*in_main_frame*/, 0 /*type*/),
subframe_contentful_paint_(false /*in_main_frame*/, 0 /*type*/),
cross_site_subframe_contentful_paint_(false /*in_main_frame*/,
0 /*type*/) {}
LargestContentfulPaintHandler::~LargestContentfulPaintHandler() = default;
void LargestContentfulPaintHandler::RecordTiming(
const page_load_metrics::mojom::LargestContentfulPaintTiming&
largest_contentful_paint,
const absl::optional<base::TimeDelta>&
first_input_or_scroll_notified_timestamp,
content::RenderFrameHost* subframe_rfh) {
if (!IsSubframe(subframe_rfh)) {
RecordMainFrameTiming(largest_contentful_paint,
first_input_or_scroll_notified_timestamp);
return;
}
// For subframes
const auto it = subframe_navigation_start_offset_.find(
subframe_rfh->GetFrameTreeNodeId());
if (it == subframe_navigation_start_offset_.end()) {
// We received timing information for an untracked load. Ignore it.
return;
}
RecordSubframeTiming(largest_contentful_paint,
first_input_or_scroll_notified_timestamp, it->second);
if (!IsSameSite(subframe_rfh->GetLastCommittedURL(),
subframe_rfh->GetMainFrame()->GetLastCommittedURL())) {
RecordCrossSiteSubframeTiming(largest_contentful_paint, it->second);
}
}
const ContentfulPaintTimingInfo&
LargestContentfulPaintHandler::MergeMainFrameAndSubframes() const {
const ContentfulPaintTimingInfo& main_frame_timing =
main_frame_contentful_paint_.MergeTextAndImageTiming();
const ContentfulPaintTimingInfo& subframe_timing =
subframe_contentful_paint_.MergeTextAndImageTiming();
return MergeTimingsBySizeAndTime(main_frame_timing, subframe_timing);
}
// We handle subframe and main frame differently. For main frame, we directly
// substitute the candidate when we receive a new one. For subframes (plural),
// we merge the candidates from different subframes by keeping the largest one.
// Note that the merging of subframes' timings will make
// |subframe_contentful_paint_| unable to be replaced with a smaller paint (it
// should have been able when a large ephemeral element is removed). This is a
// trade-off we make to keep a simple algorithm, otherwise we will have to
// track one candidate per subframe.
void LargestContentfulPaintHandler::RecordSubframeTiming(
const page_load_metrics::mojom::LargestContentfulPaintTiming&
largest_contentful_paint,
const absl::optional<base::TimeDelta>&
first_input_or_scroll_notified_timestamp,
const base::TimeDelta& navigation_start_offset) {
UpdateFirstInputOrScrollNotified(first_input_or_scroll_notified_timestamp,
navigation_start_offset);
MergeForSubframes(&subframe_contentful_paint_.Text(),
largest_contentful_paint.largest_text_paint,
largest_contentful_paint.largest_text_paint_size,
navigation_start_offset);
MergeForSubframes(&subframe_contentful_paint_.Image(),
largest_contentful_paint.largest_image_paint,
largest_contentful_paint.largest_image_paint_size,
navigation_start_offset);
}
void LargestContentfulPaintHandler::RecordCrossSiteSubframeTiming(
const page_load_metrics::mojom::LargestContentfulPaintTiming&
largest_contentful_paint,
const base::TimeDelta& navigation_start_offset) {
MergeForSubframes(&cross_site_subframe_contentful_paint_.Text(),
largest_contentful_paint.largest_text_paint,
largest_contentful_paint.largest_text_paint_size,
navigation_start_offset);
MergeForSubframes(&cross_site_subframe_contentful_paint_.Image(),
largest_contentful_paint.largest_image_paint,
largest_contentful_paint.largest_image_paint_size,
navigation_start_offset);
}
void LargestContentfulPaintHandler::RecordMainFrameTiming(
const page_load_metrics::mojom::LargestContentfulPaintTiming&
largest_contentful_paint,
const absl::optional<base::TimeDelta>&
first_input_or_scroll_notified_timestamp) {
UpdateFirstInputOrScrollNotified(
first_input_or_scroll_notified_timestamp,
/* navigation_start_offset */ base::TimeDelta());
if (IsValid(largest_contentful_paint.largest_text_paint)) {
main_frame_contentful_paint_.Text().Reset(
largest_contentful_paint.largest_text_paint,
largest_contentful_paint.largest_text_paint_size, /*type=*/0,
/*image_bpp=*/0.0);
}
if (IsValid(largest_contentful_paint.largest_image_paint)) {
main_frame_contentful_paint_.Image().Reset(
largest_contentful_paint.largest_image_paint,
largest_contentful_paint.largest_image_paint_size,
largest_contentful_paint.type, largest_contentful_paint.image_bpp);
}
}
void LargestContentfulPaintHandler::UpdateFirstInputOrScrollNotified(
const absl::optional<base::TimeDelta>& candidate_new_time,
const base::TimeDelta& navigation_start_offset) {
if (!candidate_new_time.has_value())
return;
if (first_input_or_scroll_notified_ >
navigation_start_offset + *candidate_new_time) {
first_input_or_scroll_notified_ =
navigation_start_offset + *candidate_new_time;
// Consider candidates after input to be invalid. This is needed because
// IPCs from different frames can arrive out of order. For example, this is
// consistently the case when a click on the main frame produces a new
// iframe which contains the largest content so far.
if (!IsValid(main_frame_contentful_paint_.Text().Time()))
Reset(main_frame_contentful_paint_.Text());
if (!IsValid(main_frame_contentful_paint_.Image().Time()))
Reset(main_frame_contentful_paint_.Image());
if (!IsValid(subframe_contentful_paint_.Text().Time()))
Reset(subframe_contentful_paint_.Text());
if (!IsValid(subframe_contentful_paint_.Image().Time()))
Reset(subframe_contentful_paint_.Image());
if (!IsValid(cross_site_subframe_contentful_paint_.Text().Time()))
Reset(cross_site_subframe_contentful_paint_.Text());
if (!IsValid(cross_site_subframe_contentful_paint_.Image().Time()))
Reset(cross_site_subframe_contentful_paint_.Image());
}
}
void LargestContentfulPaintHandler::OnDidFinishSubFrameNavigation(
content::NavigationHandle* navigation_handle,
base::TimeTicks navigation_start) {
if (!navigation_handle->HasCommitted())
return;
// We have a new committed navigation, so discard information about the
// previously committed navigation.
subframe_navigation_start_offset_.erase(
navigation_handle->GetFrameTreeNodeId());
if (navigation_start > navigation_handle->NavigationStart())
return;
base::TimeDelta navigation_delta;
// If navigation start offset tracking has been disabled for tests, then
// record a zero-value navigation delta. Otherwise, compute the actual delta
// between the main frame navigation start and the subframe navigation start.
// See crbug/616901 for more details on why navigation start offset tracking
// is disabled in tests.
if (!g_disable_subframe_navigation_start_offset) {
navigation_delta = navigation_handle->NavigationStart() - navigation_start;
}
subframe_navigation_start_offset_.insert(std::make_pair(
navigation_handle->GetFrameTreeNodeId(), navigation_delta));
}
void LargestContentfulPaintHandler::OnSubFrameDeleted(int frame_tree_node_id) {
subframe_navigation_start_offset_.erase(frame_tree_node_id);
}
void LargestContentfulPaintHandler::MergeForSubframes(
ContentfulPaintTimingInfo* inout_timing,
const absl::optional<base::TimeDelta>& candidate_new_time,
const uint64_t& candidate_new_size,
base::TimeDelta navigation_start_offset) {
absl::optional<base::TimeDelta> new_time = absl::nullopt;
if (candidate_new_time) {
// If |candidate_new_time| is TimeDelta(), this means that the candidate is
// an image that has not finished loading. Preserve its meaning by not
// adding the |navigation_start_offset|.
new_time = candidate_new_time->is_positive()
? navigation_start_offset + candidate_new_time.value()
: base::TimeDelta();
}
if (IsValid(new_time))
MergeForSubframesWithAdjustedTime(inout_timing, new_time,
candidate_new_size);
}
} // namespace page_load_metrics