blob: 5d0e1442a4541459a49b64df945dd8882f1936ab [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// 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/renderer/metrics_render_frame_observer.h"
#include <string>
#include <utility>
#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/page_load_metrics/renderer/features.h"
#include "components/page_load_metrics/renderer/page_timing_metrics_sender.h"
#include "components/page_load_metrics/renderer/page_timing_sender.h"
#include "content/public/renderer/render_frame.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/common/loader/resource_type_util.h"
#include "third_party/blink/public/web/web_document.h"
#include "third_party/blink/public/web/web_document_loader.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_performance_metrics_for_reporting.h"
#include "url/gurl.h"
namespace page_load_metrics {
namespace {
// This method creates a TimeDelta from two doubles that represents timestamps
// in seconds.
base::TimeDelta CreateTimeDeltaFromTimestampsInSeconds(
double event_time_in_seconds,
double start_time_in_seconds) {
if (event_time_in_seconds - start_time_in_seconds < 0) {
event_time_in_seconds = start_time_in_seconds;
}
return base::Time::FromSecondsSinceUnixEpoch(event_time_in_seconds) -
base::Time::FromSecondsSinceUnixEpoch(start_time_in_seconds);
}
base::TimeTicks ClampToStart(base::TimeTicks event, base::TimeTicks start) {
return event < start ? start : event;
}
class MojoPageTimingSender : public PageTimingSender {
public:
explicit MojoPageTimingSender(content::RenderFrame* render_frame,
bool limited_sending_mode)
: limited_sending_mode_(limited_sending_mode) {
DCHECK(render_frame);
render_frame->GetRemoteAssociatedInterfaces()->GetInterface(
&page_load_metrics_);
}
~MojoPageTimingSender() override = default;
void SendTiming(
const mojom::PageLoadTimingPtr& timing,
const mojom::FrameMetadataPtr& metadata,
const std::vector<blink::UseCounterFeature>& new_features,
std::vector<mojom::ResourceDataUpdatePtr> resources,
const mojom::FrameRenderDataUpdate& render_data,
const mojom::CpuTimingPtr& cpu_timing,
mojom::InputTimingPtr input_timing_delta,
const std::optional<blink::SubresourceLoadMetrics>&
subresource_load_metrics,
const mojom::SoftNavigationMetricsPtr& soft_navigation_metrics) override {
DCHECK(page_load_metrics_);
page_load_metrics_->UpdateTiming(
limited_sending_mode_ ? CreatePageLoadTiming() : timing->Clone(),
metadata->Clone(), new_features, std::move(resources),
render_data.Clone(), cpu_timing->Clone(), std::move(input_timing_delta),
subresource_load_metrics, soft_navigation_metrics->Clone());
}
void SetUpDroppedFramesReporting(
base::ReadOnlySharedMemoryRegion shared_memory_dropped_frames) override {
DCHECK(page_load_metrics_);
page_load_metrics_->SetUpSharedMemoryForDroppedFrames(
std::move(shared_memory_dropped_frames));
}
void SendCustomUserTiming(mojom::CustomUserTimingMarkPtr timing) override {
CHECK(timing);
CHECK(page_load_metrics_);
page_load_metrics_->AddCustomUserTiming(std::move(timing));
}
private:
// Indicates that this sender should not send timing updates or frame render
// data updates.
// TODO(crbug.com/40136524): When timing updates are handled for cases
// where we have a subframe document and no committed navigation, this can be
// removed.
bool limited_sending_mode_ = false;
// Use associated interface to make sure mojo messages are ordered with regard
// to legacy IPC messages.
mojo::AssociatedRemote<mojom::PageLoadMetrics> page_load_metrics_;
};
} // namespace
MetricsRenderFrameObserver::MetricsRenderFrameObserver(
content::RenderFrame* render_frame)
: content::RenderFrameObserver(render_frame),
blink::WebLocalFrameObserver(render_frame ? render_frame->GetWebFrame()
: nullptr) {
if (base::FeatureList::IsEnabled(
features::kDidObserveNewFeatureUsageImprovement) &&
render_frame) {
// If the optimization is enabled, `DidObserveNewFeatureUsage()` will be
// called as a callback instead of the observer interface.
render_frame->SetNewFeatureUsageCallback(base::BindRepeating(
&MetricsRenderFrameObserver::DidObserveNewFeatureUsage,
weak_factory_.GetWeakPtr()));
}
if (base::FeatureList::IsEnabled(
features::kDidObserveSubresourceLoadImprovement)) {
// If the optimization is enabled, `DidObserveSubresourceLoad()` will be
// called as a callback instead of the observer interface.
render_frame->SetSubresourceLoadCallback(base::BindRepeating(
&MetricsRenderFrameObserver::DidObserveSubresourceLoad,
weak_factory_.GetWeakPtr()));
}
}
MetricsRenderFrameObserver::~MetricsRenderFrameObserver() {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SendLatest();
}
}
void MetricsRenderFrameObserver::DidChangePerformanceTiming() {
SendMetrics();
}
void MetricsRenderFrameObserver::DidObserveUserInteraction(
base::TimeTicks max_event_start,
base::TimeTicks max_event_queued_main_thread,
base::TimeTicks max_event_commit_finish,
base::TimeTicks max_event_end,
uint64_t interaction_offset) {
if (!page_timing_metrics_sender_ || HasNoRenderFrame()) {
return;
}
page_timing_metrics_sender_->DidObserveUserInteraction(
max_event_start, max_event_queued_main_thread, max_event_commit_finish,
max_event_end, interaction_offset);
}
void MetricsRenderFrameObserver::DidChangeCpuTiming(base::TimeDelta time) {
if (!page_timing_metrics_sender_) {
return;
}
if (HasNoRenderFrame()) {
return;
}
page_timing_metrics_sender_->UpdateCpuTiming(time);
}
void MetricsRenderFrameObserver::DidObserveLoadingBehavior(
blink::LoadingBehaviorFlag behavior) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidObserveLoadingBehavior(behavior);
}
}
void MetricsRenderFrameObserver::DidObserveJavaScriptFrameworks(
const blink::JavaScriptFrameworkDetectionResult& result) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidObserveJavaScriptFrameworks(result);
}
}
void MetricsRenderFrameObserver::DidObserveSubresourceLoad(
const blink::SubresourceLoadMetrics& subresource_load_metrics) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidObserveSubresourceLoad(
subresource_load_metrics);
}
}
void MetricsRenderFrameObserver::DidObserveNewFeatureUsage(
const blink::UseCounterFeature& feature) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidObserveNewFeatureUsage(feature);
}
}
void MetricsRenderFrameObserver::DidObserveSoftNavigation(
blink::SoftNavigationMetricsForReporting soft_nav_metrics) {
if (page_timing_metrics_sender_) {
const blink::WebPerformanceMetricsForReporting& metrics =
render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
// Make soft navigation start time relative to navigation start.
soft_nav_metrics.start_time = CreateTimeDeltaFromTimestampsInSeconds(
soft_nav_metrics.start_time.InSecondsF(), metrics.NavigationStart());
// (crbug.com/40074158): will non-fatally dump in official builds if the
// start_time is 0.
DUMP_WILL_BE_CHECK(!soft_nav_metrics.start_time.is_zero());
page_timing_metrics_sender_->DidObserveSoftNavigation(soft_nav_metrics);
}
}
void MetricsRenderFrameObserver::DidObserveLayoutShift(
double score,
bool after_input_or_scroll) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidObserveLayoutShift(score,
after_input_or_scroll);
}
}
void MetricsRenderFrameObserver::DidStartResponse(
const url::SchemeHostPort& final_response_url,
int request_id,
const network::mojom::URLResponseHead& response_head,
network::mojom::RequestDestination request_destination,
bool is_ad_resource) {
if (provisional_frame_resource_data_use_ &&
blink::IsRequestDestinationFrame(request_destination)) {
// TODO(rajendrant): This frame request might start before the provisional
// load starts, and data use of the frame request might be missed in that
// case. There should be a guarantee that DidStartProvisionalLoad be called
// before DidStartResponse for the frame request.
provisional_frame_resource_data_use_->DidStartResponse(
final_response_url, request_id, response_head, request_destination,
is_ad_resource);
} else if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidStartResponse(
final_response_url, request_id, response_head, request_destination,
is_ad_resource);
UpdateResourceMetadata(request_id);
}
}
void MetricsRenderFrameObserver::DidCompleteResponse(
int request_id,
const network::URLLoaderCompletionStatus& status) {
if (provisional_frame_resource_data_use_ &&
provisional_frame_resource_data_use_->resource_id() == request_id) {
provisional_frame_resource_data_use_->DidCompleteResponse(status);
} else if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidCompleteResponse(request_id, status);
UpdateResourceMetadata(request_id);
}
}
void MetricsRenderFrameObserver::DidCancelResponse(int request_id) {
if (provisional_frame_resource_data_use_ &&
provisional_frame_resource_data_use_->resource_id() == request_id) {
provisional_frame_resource_data_use_.reset();
} else if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidCancelResponse(request_id);
UpdateResourceMetadata(request_id);
}
}
void MetricsRenderFrameObserver::DidReceiveTransferSizeUpdate(
int request_id,
base::ByteCount received_data_length) {
if (provisional_frame_resource_data_use_ &&
provisional_frame_resource_data_use_->resource_id() == request_id) {
provisional_frame_resource_data_use_->DidReceiveTransferSizeUpdate(
received_data_length);
} else if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidReceiveTransferSizeUpdate(
request_id, received_data_length);
UpdateResourceMetadata(request_id);
}
}
void MetricsRenderFrameObserver::DidLoadResourceFromMemoryCache(
const GURL& response_url,
int request_id,
base::ByteCount encoded_body_length,
const std::string& mime_type,
bool from_archive) {
// Resources from archives, such as subresources from a MHTML archive, do not
// have valid request ids and should not be reported to PLM.
if (from_archive) {
return;
}
// A provisional frame resource cannot be serviced from the memory cache, so
// we do not need to check |provisional_frame_resource_data_use_|.
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->DidLoadResourceFromMemoryCache(
response_url, request_id, encoded_body_length, mime_type);
UpdateResourceMetadata(request_id);
}
}
void MetricsRenderFrameObserver::WillDetach(blink::DetachReason detach_reason) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SendLatest();
page_timing_metrics_sender_.reset();
}
}
void MetricsRenderFrameObserver::DidStartNavigation(
const GURL& url,
std::optional<blink::WebNavigationType> navigation_type) {
// Send current metrics, as we might create a new RenderFrame later due to
// this navigation (that might end up in a different process entirely, and
// won't notify us until the current RenderFrameHost in the browser changed).
// If that happens, it will be too late to send the metrics from WillDetach
// or the destructor, because the browser ignores metrics update from
// non-current RenderFrameHosts. See crbug.com/1150242 for more details.
// TODO(crbug.com/40157795): Remove this when we have the full fix for the
// bug.
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SendLatest();
}
}
void MetricsRenderFrameObserver::DidSetPageLifecycleState(
bool restoring_from_bfcache) {
// Send current metrics, as this RenderFrame might be replaced by a new
// RenderFrame or its process might be killed, and this might be the last
// point we can send the metrics to the browser. See crbug.com/1150242 for
// more details.
// TODO(crbug.com/40157795): Remove this when we have the full fix for the
// bug.
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SendLatest();
}
}
void MetricsRenderFrameObserver::ReadyToCommitNavigation(
blink::WebDocumentLoader* document_loader) {
// Create a new data use tracker for the new document load.
provisional_frame_resource_data_use_ = std::make_unique<PageResourceDataUse>(
PageResourceDataUse::kUnknownResourceId);
// Send current metrics before the next page load commits. Don't reset here
// as it may be a same document load.
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SendLatest();
}
}
void MetricsRenderFrameObserver::DidFailProvisionalLoad() {
// Clear the data use tracker for the provisional navigation that started.
provisional_frame_resource_data_use_.reset();
}
void MetricsRenderFrameObserver::DidCreateDocumentElement() {
// If we do not have a render frame or are already tracking this frame, ignore
// the new document element.
if (HasNoRenderFrame() || page_timing_metrics_sender_) {
return;
}
// We should only track committed navigations for the main frame so ignore new
// document elements in the main frame.
if (IsMainFrame()) {
return;
}
// A new document element was created in a frame that did not commit a
// provisional load. This can be due to a doc.write in the frame that aborted
// a navigation. Create a page timing sender to track this load. This sender
// will only send resource usage updates to the browser process. There
// currently is not infrastructure in the browser process to monitor this case
// and properly handle timing updates without a committed load.
// TODO(crbug.com/40136524): Implement proper handling of timing
// updates in the browser process and create a normal page timing sender.
// It should not be possible to have a |provisional_frame_resource_data_use_|
// object at this point. If we did, it means we reached
// ReadyToCommitNavigation() and aborted prior to load commit which should not
// be possible.
DCHECK(!provisional_frame_resource_data_use_);
Timing timing = GetTiming();
page_timing_metrics_sender_ = std::make_unique<PageTimingMetricsSender>(
CreatePageTimingSender(true /* limited_sending_mode */), CreateTimer(),
std::move(timing.relative_timing), timing.monotonic_timing,
/* initial_request=*/nullptr, /* is_main_frame=*/false);
OnMetricsSenderCreated();
}
void MetricsRenderFrameObserver::DidCommitProvisionalLoad(
ui::PageTransition transition) {
// Make sure to release the sender for a previous navigation, if we have one.
page_timing_metrics_sender_.reset();
if (HasNoRenderFrame()) {
return;
}
Timing timing = GetTiming();
page_timing_metrics_sender_ = std::make_unique<PageTimingMetricsSender>(
CreatePageTimingSender(false /* limited_sending_mode*/), CreateTimer(),
std::move(timing.relative_timing), timing.monotonic_timing,
std::move(provisional_frame_resource_data_use_), IsMainFrame());
OnMetricsSenderCreated();
}
void MetricsRenderFrameObserver::OnMainFrameIntersectionChanged(
const gfx::Rect& main_frame_intersection_rect) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->OnMainFrameIntersectionChanged(
main_frame_intersection_rect);
return;
}
main_frame_intersection_rect_before_metrics_sender_created_ =
main_frame_intersection_rect;
}
void MetricsRenderFrameObserver::OnMainFrameViewportRectangleChanged(
const gfx::Rect& main_frame_viewport_rect) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->OnMainFrameViewportRectangleChanged(
main_frame_viewport_rect);
}
}
void MetricsRenderFrameObserver::OnMainFrameAdRectangleChanged(
int element_id,
const gfx::Rect& ad_rect) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->OnMainFrameAdRectangleChanged(element_id,
ad_rect);
}
}
void MetricsRenderFrameObserver::OnFrameDetached() {
WillDetach(blink::DetachReason::kNavigation);
}
bool MetricsRenderFrameObserver::SetUpDroppedFramesReporting(
base::ReadOnlySharedMemoryRegion& shared_memory_dropped_frames) {
if (page_timing_metrics_sender_) {
page_timing_metrics_sender_->SetUpDroppedFramesReporting(
std::move(shared_memory_dropped_frames));
} else {
ukm_dropped_frames_data_ = std::move(shared_memory_dropped_frames);
}
return true;
}
MetricsRenderFrameObserver::Timing::Timing(
mojom::PageLoadTimingPtr relative_timing,
const PageTimingMetadataRecorder::MonotonicTiming& monotonic_timing)
: relative_timing(std::move(relative_timing)),
monotonic_timing(monotonic_timing) {}
MetricsRenderFrameObserver::Timing::~Timing() = default;
MetricsRenderFrameObserver::Timing::Timing(Timing&&) = default;
MetricsRenderFrameObserver::Timing&
MetricsRenderFrameObserver::Timing::operator=(Timing&&) = default;
void MetricsRenderFrameObserver::UpdateResourceMetadata(int request_id) {
DCHECK(page_timing_metrics_sender_);
bool is_main_frame_resource = IsMainFrame();
if (provisional_frame_resource_data_use_ &&
provisional_frame_resource_data_use_->resource_id() == request_id) {
provisional_frame_resource_data_use_->SetIsMainFrameResource(
is_main_frame_resource);
// Don't bother with before-fcp metrics for a provisional frame.
} else {
page_timing_metrics_sender_->UpdateResourceMetadata(request_id,
is_main_frame_resource);
}
}
void MetricsRenderFrameObserver::SendMetrics() {
if (!page_timing_metrics_sender_) {
return;
}
if (HasNoRenderFrame()) {
return;
}
Timing timing = GetTiming();
page_timing_metrics_sender_->UpdateSoftNavigationMetrics(
GetSoftNavigationMetrics());
page_timing_metrics_sender_->Update(std::move(timing.relative_timing),
timing.monotonic_timing);
mojom::CustomUserTimingMarkPtr user_timing = GetCustomUserTimingMark();
if (user_timing) {
page_timing_metrics_sender_->SendCustomUserTimingMark(
std::move(user_timing));
}
}
void MetricsRenderFrameObserver::OnMetricsSenderCreated() {
if (ukm_dropped_frames_data_.IsValid()) {
page_timing_metrics_sender_->SetUpDroppedFramesReporting(
std::move(ukm_dropped_frames_data_));
}
// Send the latest the frame intersection update, as otherwise we may miss
// this information for a frame completely if there are no future updates.
if (main_frame_intersection_rect_before_metrics_sender_created_) {
page_timing_metrics_sender_->OnMainFrameIntersectionChanged(
*main_frame_intersection_rect_before_metrics_sender_created_);
main_frame_intersection_rect_before_metrics_sender_created_.reset();
}
}
mojom::SoftNavigationMetricsPtr
MetricsRenderFrameObserver::GetSoftNavigationMetrics() const {
CHECK(render_frame());
CHECK(render_frame()->GetWebFrame());
const blink::WebPerformanceMetricsForReporting& metrics =
render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
CHECK(page_timing_metrics_sender_.get());
auto soft_navigation_metrics =
page_timing_metrics_sender_->GetSoftNavigationMetrics();
CHECK(!soft_navigation_metrics.is_null());
soft_navigation_metrics->largest_contentful_paint =
CreateLargestContentfulPaintTiming();
auto soft_navigation_lcp_details_ =
metrics.SoftNavigationLargestContentfulDetailsForMetrics();
double soft_navigation_start_relative_to_navigation_start =
soft_navigation_metrics->start_time.InSecondsF();
double navigation_start = metrics.NavigationStart();
if (soft_navigation_lcp_details_.image_paint_size > 0) {
// Set largest image time.
// Note that size can be nonzero while the time is 0 since a time of 0 is
// sent when the image is painting. We assign the time even when it is 0 so
// that it's not ignored, but need to be careful when doing operations on
// the value.
if (soft_navigation_lcp_details_.image_paint_time == 0.0) {
soft_navigation_metrics->largest_contentful_paint->largest_image_paint =
base::TimeDelta();
} else {
base::TimeDelta image_paint_time_relative_to_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
soft_navigation_lcp_details_.image_paint_time, navigation_start);
base::TimeDelta image_paint_time_relative_to_soft_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
image_paint_time_relative_to_navigation_start.InSecondsF(),
soft_navigation_start_relative_to_navigation_start);
soft_navigation_metrics->largest_contentful_paint->largest_image_paint =
image_paint_time_relative_to_soft_navigation_start;
}
// Set largest image size.
soft_navigation_metrics->largest_contentful_paint
->largest_image_paint_size =
soft_navigation_lcp_details_.image_paint_size;
// Set largest image load type.
soft_navigation_metrics->largest_contentful_paint->type =
LargestContentfulPaintTypeToUKMFlags(soft_navigation_lcp_details_.type);
// Set largest image bpp value.
soft_navigation_metrics->largest_contentful_paint->image_bpp =
soft_navigation_lcp_details_.image_bpp;
// Set largest image request priority.
if (soft_navigation_lcp_details_.image_request_priority.has_value()) {
soft_navigation_metrics->largest_contentful_paint
->image_request_priority_valid = true;
soft_navigation_metrics->largest_contentful_paint
->image_request_priority_value =
blink::WebURLRequest::ConvertToNetPriority(
soft_navigation_lcp_details_.image_request_priority.value());
} else {
soft_navigation_metrics->largest_contentful_paint
->image_request_priority_valid = false;
}
// Set largest image discovery time.
if (soft_navigation_lcp_details_.resource_load_timings.discovery_time
.has_value()) {
base::TimeDelta image_discovery_time_relative_to_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
(soft_navigation_lcp_details_.resource_load_timings.discovery_time
.value())
.InSecondsF(),
navigation_start);
base::TimeDelta image_discovery_time_relative_to_soft_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
image_discovery_time_relative_to_navigation_start.InSecondsF(),
soft_navigation_start_relative_to_navigation_start);
soft_navigation_metrics->largest_contentful_paint->resource_load_timings
->discovery_time =
image_discovery_time_relative_to_soft_navigation_start;
}
// Set largest image load start.
if (soft_navigation_lcp_details_.resource_load_timings.load_start
.has_value()) {
base::TimeDelta image_load_start_relative_to_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
(soft_navigation_lcp_details_.resource_load_timings.load_start
.value())
.InSecondsF(),
navigation_start);
base::TimeDelta image_load_start_relative_to_soft_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
image_load_start_relative_to_navigation_start.InSecondsF(),
soft_navigation_start_relative_to_navigation_start);
soft_navigation_metrics->largest_contentful_paint->resource_load_timings
->load_start = image_load_start_relative_to_soft_navigation_start;
}
// Set largest image load end.
if (soft_navigation_lcp_details_.resource_load_timings.load_end
.has_value()) {
base::TimeDelta image_load_end_relative_to_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
(soft_navigation_lcp_details_.resource_load_timings.load_end
.value())
.InSecondsF(),
navigation_start);
base::TimeDelta image_load_end_relative_to_soft_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
image_load_end_relative_to_navigation_start.InSecondsF(),
soft_navigation_start_relative_to_navigation_start);
soft_navigation_metrics->largest_contentful_paint->resource_load_timings
->load_end = image_load_end_relative_to_soft_navigation_start;
}
}
if (soft_navigation_lcp_details_.text_paint_size > 0) {
// LargestTextPaint and LargestTextPaintSize should be available at the
// same time. This is a renderer side DCHECK to ensure this.
DCHECK(soft_navigation_lcp_details_.text_paint_time);
base::TimeDelta text_paint_time_relative_to_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
soft_navigation_lcp_details_.text_paint_time, navigation_start);
base::TimeDelta text_paint_time_relative_to_soft_navigation_start =
CreateTimeDeltaFromTimestampsInSeconds(
text_paint_time_relative_to_navigation_start.InSecondsF(),
soft_navigation_start_relative_to_navigation_start);
soft_navigation_metrics->largest_contentful_paint->largest_text_paint =
text_paint_time_relative_to_soft_navigation_start;
soft_navigation_metrics->largest_contentful_paint->largest_text_paint_size =
soft_navigation_lcp_details_.text_paint_size;
soft_navigation_metrics->largest_contentful_paint->type =
LargestContentfulPaintTypeToUKMFlags(soft_navigation_lcp_details_.type);
}
return soft_navigation_metrics;
}
MetricsRenderFrameObserver::Timing MetricsRenderFrameObserver::GetTiming()
const {
const blink::WebPerformanceMetricsForReporting& perf =
render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
mojom::PageLoadTimingPtr timing(CreatePageLoadTiming());
PageTimingMetadataRecorder::MonotonicTiming monotonic_timing;
double start = perf.NavigationStart();
timing->navigation_start = base::Time::FromSecondsSinceUnixEpoch(start);
monotonic_timing.navigation_start = perf.NavigationStartAsMonotonicTime();
if (perf.InputForNavigationStart() > 0.0) {
timing->input_to_navigation_start = CreateTimeDeltaFromTimestampsInSeconds(
start, perf.InputForNavigationStart());
}
if (perf.FirstInputDelay().has_value()) {
timing->interactive_timing->first_input_delay = *perf.FirstInputDelay();
monotonic_timing.first_input_delay = perf.FirstInputDelay();
}
if (perf.FirstInputTimestamp().has_value()) {
timing->interactive_timing->first_input_timestamp =
CreateTimeDeltaFromTimestampsInSeconds(
(*perf.FirstInputTimestamp()).InSecondsF(), start);
}
if (perf.FirstInputTimestampAsMonotonicTime()) {
monotonic_timing.first_input_timestamp =
perf.FirstInputTimestampAsMonotonicTime();
}
if (perf.FirstScrollDelay().has_value()) {
timing->interactive_timing->first_scroll_delay = *perf.FirstScrollDelay();
}
if (perf.FirstScrollTimestamp().has_value()) {
timing->interactive_timing->first_scroll_timestamp =
CreateTimeDeltaFromTimestampsInSeconds(
(*perf.FirstScrollTimestamp()).InSecondsF(), start);
}
if (perf.DomainLookupStart() > 0.0) {
timing->domain_lookup_timing->domain_lookup_start =
CreateTimeDeltaFromTimestampsInSeconds(perf.DomainLookupStart(), start);
}
if (perf.DomainLookupEnd() > 0.0) {
timing->domain_lookup_timing->domain_lookup_end =
CreateTimeDeltaFromTimestampsInSeconds(perf.DomainLookupEnd(), start);
}
if (perf.ConnectStart() > 0.0) {
timing->connect_start =
CreateTimeDeltaFromTimestampsInSeconds(perf.ConnectStart(), start);
}
if (perf.ConnectEnd() > 0.0) {
timing->connect_end =
CreateTimeDeltaFromTimestampsInSeconds(perf.ConnectEnd(), start);
}
if (perf.ResponseStart() > 0.0) {
timing->response_start =
CreateTimeDeltaFromTimestampsInSeconds(perf.ResponseStart(), start);
}
if (perf.DomContentLoadedEventStart() > 0.0) {
timing->document_timing->dom_content_loaded_event_start =
CreateTimeDeltaFromTimestampsInSeconds(
perf.DomContentLoadedEventStart(), start);
}
if (perf.LoadEventStart() > 0.0) {
timing->document_timing->load_event_start =
CreateTimeDeltaFromTimestampsInSeconds(perf.LoadEventStart(), start);
}
if (perf.FirstPaint() > 0.0) {
timing->paint_timing->first_paint =
CreateTimeDeltaFromTimestampsInSeconds(perf.FirstPaint(), start);
}
if (!perf.BackForwardCacheRestore().empty()) {
blink::WebPerformanceMetricsForReporting::BackForwardCacheRestoreTimings
restore_timings = perf.BackForwardCacheRestore();
for (const auto& restore_timing : restore_timings) {
double navigation_start = restore_timing.navigation_start;
double first_paint = restore_timing.first_paint;
std::optional<base::TimeDelta> first_input_delay =
restore_timing.first_input_delay;
auto back_forward_cache_timing = mojom::BackForwardCacheTiming::New();
if (first_paint) {
back_forward_cache_timing
->first_paint_after_back_forward_cache_restore =
CreateTimeDeltaFromTimestampsInSeconds(first_paint,
navigation_start);
}
for (double raf : restore_timing.request_animation_frames) {
if (!raf) {
break;
}
back_forward_cache_timing
->request_animation_frames_after_back_forward_cache_restore
.push_back(
CreateTimeDeltaFromTimestampsInSeconds(raf, navigation_start));
}
if (first_input_delay.has_value()) {
back_forward_cache_timing
->first_input_delay_after_back_forward_cache_restore =
first_input_delay;
}
timing->back_forward_cache_timings.push_back(
std::move(back_forward_cache_timing));
}
}
if (perf.FirstImagePaint() > 0.0) {
timing->paint_timing->first_image_paint =
CreateTimeDeltaFromTimestampsInSeconds(perf.FirstImagePaint(), start);
}
if (perf.FirstContentfulPaint() > 0.0) {
DCHECK(perf.FirstEligibleToPaint() > 0);
timing->paint_timing->first_contentful_paint =
CreateTimeDeltaFromTimestampsInSeconds(perf.FirstContentfulPaint(),
start);
monotonic_timing.first_contentful_paint =
ClampToStart(perf.FirstContentfulPaintAsMonotonicTime(),
perf.NavigationStartAsMonotonicTime());
}
if (perf.FirstMeaningfulPaint() > 0.0) {
timing->paint_timing->first_meaningful_paint =
CreateTimeDeltaFromTimestampsInSeconds(perf.FirstMeaningfulPaint(),
start);
}
blink::LargestContentfulPaintDetailsForReporting
largest_contentful_paint_details =
perf.LargestContentfulDetailsForMetrics();
monotonic_timing.frame_largest_contentful_paint =
largest_contentful_paint_details.merged_unclamped_paint_time;
if (largest_contentful_paint_details.image_paint_size > 0) {
timing->paint_timing->largest_contentful_paint->largest_image_paint_size =
largest_contentful_paint_details.image_paint_size;
// Note that size can be nonzero while the time is 0 since a time of 0 is
// sent when the image is painting. We assign the time even when it is 0 so
// that it's not ignored, but need to be careful when doing operations on
// the value.
timing->paint_timing->largest_contentful_paint->largest_image_paint =
largest_contentful_paint_details.image_paint_time == 0.0
? base::TimeDelta()
: CreateTimeDeltaFromTimestampsInSeconds(
largest_contentful_paint_details.image_paint_time, start);
timing->paint_timing->largest_contentful_paint->type =
LargestContentfulPaintTypeToUKMFlags(
largest_contentful_paint_details.type);
timing->paint_timing->largest_contentful_paint->image_bpp =
largest_contentful_paint_details.image_bpp;
if (largest_contentful_paint_details.image_request_priority.has_value()) {
timing->paint_timing->largest_contentful_paint
->image_request_priority_valid = true;
timing->paint_timing->largest_contentful_paint
->image_request_priority_value =
blink::WebURLRequest::ConvertToNetPriority(
largest_contentful_paint_details.image_request_priority.value());
} else {
timing->paint_timing->largest_contentful_paint
->image_request_priority_valid = false;
}
// Set largest image load timings.
if (largest_contentful_paint_details.resource_load_timings.discovery_time
.has_value()) {
timing->paint_timing->largest_contentful_paint->resource_load_timings
->discovery_time = CreateTimeDeltaFromTimestampsInSeconds(
(largest_contentful_paint_details.resource_load_timings.discovery_time
.value())
.InSecondsF(),
start);
}
if (largest_contentful_paint_details.resource_load_timings.load_start
.has_value()) {
timing->paint_timing->largest_contentful_paint->resource_load_timings
->load_start = CreateTimeDeltaFromTimestampsInSeconds(
(largest_contentful_paint_details.resource_load_timings.load_start
.value())
.InSecondsF(),
start);
}
if (largest_contentful_paint_details.resource_load_timings.load_end
.has_value()) {
timing->paint_timing->largest_contentful_paint->resource_load_timings
->load_end = CreateTimeDeltaFromTimestampsInSeconds(
(largest_contentful_paint_details.resource_load_timings.load_end
.value())
.InSecondsF(),
start);
}
}
if (largest_contentful_paint_details.text_paint_size > 0) {
// LargestTextPaint and LargestTextPaintSize should be available at the
// same time. This is a renderer side DCHECK to ensure this.
DCHECK(largest_contentful_paint_details.text_paint_time);
timing->paint_timing->largest_contentful_paint->largest_text_paint =
CreateTimeDeltaFromTimestampsInSeconds(
largest_contentful_paint_details.text_paint_time, start);
timing->paint_timing->largest_contentful_paint->largest_text_paint_size =
largest_contentful_paint_details.text_paint_size;
timing->paint_timing->largest_contentful_paint->type =
LargestContentfulPaintTypeToUKMFlags(
largest_contentful_paint_details.type);
}
// It is possible for a frame to switch from eligible for painting to
// ineligible for it prior to the first paint. If this occurs, we need to
// propagate the null value.
if (perf.FirstEligibleToPaint() > 0) {
timing->paint_timing->first_eligible_to_paint =
CreateTimeDeltaFromTimestampsInSeconds(perf.FirstEligibleToPaint(),
start);
} else {
timing->paint_timing->first_eligible_to_paint.reset();
}
if (perf.FirstInputOrScrollNotifiedTimestamp() > 0) {
timing->paint_timing->first_input_or_scroll_notified_timestamp =
CreateTimeDeltaFromTimestampsInSeconds(
perf.FirstInputOrScrollNotifiedTimestamp(), start);
}
if (perf.ParseStart() > 0.0) {
timing->parse_timing->parse_start =
CreateTimeDeltaFromTimestampsInSeconds(perf.ParseStart(), start);
}
if (perf.ParseStop() > 0.0) {
timing->parse_timing->parse_stop =
CreateTimeDeltaFromTimestampsInSeconds(perf.ParseStop(), start);
}
if (timing->parse_timing->parse_start) {
// If we started parsing, record all parser durations such as the amount of
// time blocked on script load, even if those values are zero.
timing->parse_timing->parse_blocked_on_script_load_duration =
base::Seconds(perf.ParseBlockedOnScriptLoadDuration());
timing->parse_timing
->parse_blocked_on_script_load_from_document_write_duration =
base::Seconds(perf.ParseBlockedOnScriptLoadFromDocumentWriteDuration());
timing->parse_timing->parse_blocked_on_script_execution_duration =
base::Seconds(perf.ParseBlockedOnScriptExecutionDuration());
timing->parse_timing
->parse_blocked_on_script_execution_from_document_write_duration =
base::Seconds(
perf.ParseBlockedOnScriptExecutionFromDocumentWriteDuration());
}
if (perf.PrerenderActivationStart().has_value()) {
timing->activation_start = perf.PrerenderActivationStart();
}
if (perf.UserTimingMarkFullyLoaded().has_value()) {
timing->user_timing_mark_fully_loaded = perf.UserTimingMarkFullyLoaded();
}
if (perf.UserTimingMarkFullyVisible().has_value()) {
timing->user_timing_mark_fully_visible = perf.UserTimingMarkFullyVisible();
}
if (perf.UserTimingMarkInteractive().has_value()) {
timing->user_timing_mark_interactive = perf.UserTimingMarkInteractive();
}
return Timing(std::move(timing), monotonic_timing);
}
mojom::CustomUserTimingMarkPtr
MetricsRenderFrameObserver::GetCustomUserTimingMark() const {
const blink::WebPerformanceMetricsForReporting& perf =
render_frame()->GetWebFrame()->PerformanceMetricsForReporting();
auto timing = perf.CustomUserTimingMark();
if (!timing.has_value()) {
return nullptr;
}
const auto [mark_name, start_time] = timing.value();
mojom::CustomUserTimingMarkPtr custom_user_timing_mark =
mojom::CustomUserTimingMark::New();
custom_user_timing_mark->mark_name = mark_name;
custom_user_timing_mark->start_time = start_time;
return custom_user_timing_mark;
}
std::unique_ptr<base::OneShotTimer> MetricsRenderFrameObserver::CreateTimer() {
return std::make_unique<base::OneShotTimer>();
}
std::unique_ptr<PageTimingSender>
MetricsRenderFrameObserver::CreatePageTimingSender(bool limited_sending_mode) {
return base::WrapUnique<PageTimingSender>(
new MojoPageTimingSender(render_frame(), limited_sending_mode));
}
bool MetricsRenderFrameObserver::HasNoRenderFrame() const {
bool no_frame = !render_frame() || !render_frame()->GetWebFrame();
DCHECK(!no_frame);
return no_frame;
}
bool MetricsRenderFrameObserver::IsMainFrame() const {
return render_frame()->IsMainFrame();
}
void MetricsRenderFrameObserver::OnDestruct() {
delete this;
}
} // namespace page_load_metrics