blob: 1e3871d0e63dbdd2cccaf1d66863dad2c518b4e0 [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/page_timing_metrics_sender.h"
#include <utility>
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/page_load_metrics/common/page_load_metrics.mojom.h"
#include "components/page_load_metrics/common/page_load_metrics_util.h"
#include "components/page_load_metrics/common/page_load_timing.h"
#include "components/page_load_metrics/renderer/page_timing_sender.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/loader/javascript_framework_detection.mojom-forward.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-shared.h"
#include "third_party/blink/public/mojom/use_counter/use_counter_feature.mojom-shared.h"
#include "ui/gfx/geometry/rect.h"
namespace page_load_metrics {
namespace {
const int kInitialTimerDelayMillis = 50;
bool IsFirstFCP(const mojom::PageLoadTimingPtr& last_timing,
const mojom::PageLoadTimingPtr& new_timing) {
return (!last_timing->paint_timing ||
!last_timing->paint_timing->first_contentful_paint.has_value()) &&
new_timing->paint_timing &&
new_timing->paint_timing->first_contentful_paint.has_value();
}
bool IsFirstParseStart(const mojom::PageLoadTimingPtr& last_timing,
const mojom::PageLoadTimingPtr& new_timing) {
return (!last_timing->parse_timing ||
!last_timing->parse_timing->parse_start.has_value()) &&
new_timing->parse_timing &&
new_timing->parse_timing->parse_start.has_value();
}
bool IsFirstDCL(const mojom::PageLoadTimingPtr& last_timing,
const mojom::PageLoadTimingPtr& new_timing) {
return (!last_timing->document_timing ||
!last_timing->document_timing->dom_content_loaded_event_start
.has_value()) &&
new_timing->document_timing &&
new_timing->document_timing->dom_content_loaded_event_start
.has_value();
}
} // namespace
PageTimingMetricsSender::PageTimingMetricsSender(
std::unique_ptr<PageTimingSender> sender,
std::unique_ptr<base::OneShotTimer> timer,
mojom::PageLoadTimingPtr initial_timing,
const PageTimingMetadataRecorder::MonotonicTiming& initial_monotonic_timing,
std::unique_ptr<PageResourceDataUse> initial_request,
bool is_main_frame)
: sender_(std::move(sender)),
timer_(std::move(timer)),
last_timing_(std::move(initial_timing)),
last_cpu_timing_(mojom::CpuTiming::New()),
input_timing_delta_(mojom::InputTiming::New()),
metadata_(mojom::FrameMetadata::New()),
soft_navigation_metrics_(CreateSoftNavigationMetrics()),
buffer_timer_delay_ms_(GetBufferTimerDelayMillis(TimerType::kRenderer)),
metadata_recorder_(initial_monotonic_timing, is_main_frame) {
if (initial_request) {
int resource_id = initial_request->resource_id();
page_resource_data_use_[resource_id] = std::move(initial_request);
}
if (!IsEmpty(*last_timing_)) {
EnsureSendTimer();
}
}
PageTimingMetricsSender::~PageTimingMetricsSender() {
// Make sure we don't have any unsent data. If this assertion fails, then it
// means metrics are somehow coming in between MetricsRenderFrameObserver's
// ReadyToCommitNavigation and DidCommitProvisionalLoad.
DCHECK(!timer_->IsRunning());
}
void PageTimingMetricsSender::DidObserveLoadingBehavior(
blink::LoadingBehaviorFlag behavior) {
if (behavior & metadata_->behavior_flags) {
return;
}
metadata_->behavior_flags |= behavior;
EnsureSendTimer();
}
void PageTimingMetricsSender::DidObserveJavaScriptFrameworks(
const blink::JavaScriptFrameworkDetectionResult& result) {
metadata_->framework_detection_result = result;
EnsureSendTimer();
}
void PageTimingMetricsSender::DidObserveSubresourceLoad(
const blink::SubresourceLoadMetrics& subresource_load_metrics) {
if (subresource_load_metrics_ &&
*subresource_load_metrics_ == subresource_load_metrics) {
return;
}
subresource_load_metrics_ = subresource_load_metrics;
EnsureSendTimer();
}
void PageTimingMetricsSender::DidObserveNewFeatureUsage(
const blink::UseCounterFeature& feature) {
if (feature_tracker_.TestAndSet(feature))
return;
new_features_.push_back(feature);
EnsureSendTimer();
}
void PageTimingMetricsSender::DidObserveSoftNavigation(
blink::SoftNavigationMetricsForReporting new_metrics) {
// The start_time is a TimeDelta, and its resolution is in microseconds.
// Every time we observe a new soft navigation we expect the total count to
// increase by one, and the navigation_id to update, however, we have no
// expectations about start_time values. This is because soft-navs start_time
// might not be monotonically increasing. See: crbug.com/418449366#comment3
CHECK(new_metrics.count >= soft_navigation_metrics_->count);
CHECK(!new_metrics.start_time.is_zero());
CHECK(new_metrics.navigation_id != soft_navigation_metrics_->navigation_id);
soft_navigation_metrics_->count = new_metrics.count;
soft_navigation_metrics_->start_time = new_metrics.start_time;
soft_navigation_metrics_->navigation_id = new_metrics.navigation_id;
EnsureSendTimer();
}
void PageTimingMetricsSender::DidObserveLayoutShift(
double score,
bool after_input_or_scroll) {
DCHECK(score > 0);
render_data_.layout_shift_delta += score;
render_data_.new_layout_shifts.push_back(
mojom::LayoutShift::New(base::TimeTicks::Now(), score));
if (!after_input_or_scroll)
render_data_.layout_shift_delta_before_input_or_scroll += score;
EnsureSendTimer();
}
void PageTimingMetricsSender::DidStartResponse(
const url::SchemeHostPort& final_response_url,
int resource_id,
const network::mojom::URLResponseHead& response_head,
network::mojom::RequestDestination request_destination,
bool is_ad_resource) {
// There can be multiple `DidStartResponse` for the same resource id
// (crbug.com/1504430).
FindOrInsertPageResourceDataUse(resource_id)
->DidStartResponse(final_response_url, resource_id, response_head,
request_destination, is_ad_resource);
}
void PageTimingMetricsSender::DidReceiveTransferSizeUpdate(
int resource_id,
base::ByteCount received_data_length) {
// Transfer size updates are called in a throttled manner.
auto resource_it = page_resource_data_use_.find(resource_id);
// It is possible that resources are not in the map, if response headers were
// not received or for failed/cancelled resources.
if (resource_it == page_resource_data_use_.end()) {
return;
}
resource_it->second->DidReceiveTransferSizeUpdate(received_data_length);
modified_resources_.insert(resource_it->second.get());
EnsureSendTimer();
}
void PageTimingMetricsSender::DidCompleteResponse(
int resource_id,
const network::URLLoaderCompletionStatus& status) {
PageResourceDataUse* data_use = FindOrInsertPageResourceDataUse(resource_id);
data_use->DidCompleteResponse(status);
modified_resources_.insert(data_use);
EnsureSendTimer();
}
void PageTimingMetricsSender::DidCancelResponse(int resource_id) {
auto resource_it = page_resource_data_use_.find(resource_id);
if (resource_it == page_resource_data_use_.end()) {
return;
}
resource_it->second->DidCancelResponse();
}
void PageTimingMetricsSender::DidLoadResourceFromMemoryCache(
const GURL& response_url,
int request_id,
base::ByteCount encoded_body_length,
const std::string& mime_type) {
// In general, we should not observe the same resource being loaded twice in
// the frame. This is possible due to an existing workaround in
// ResourceFetcher::EmulateLoadStartedForInspector(). In this case, ignore
// multiple resources being loaded in the document, as memory cache resources
// are only reported once per context by design in all other cases.
if (base::Contains(page_resource_data_use_, request_id))
return;
FindOrInsertPageResourceDataUse(request_id)
->DidLoadFromMemoryCache(response_url, encoded_body_length, mime_type);
}
void PageTimingMetricsSender::OnMainFrameIntersectionChanged(
const gfx::Rect& main_frame_intersection_rect) {
metadata_->main_frame_intersection_rect = main_frame_intersection_rect;
EnsureSendTimer();
}
void PageTimingMetricsSender::OnMainFrameViewportRectangleChanged(
const gfx::Rect& main_frame_viewport_rect) {
metadata_->main_frame_viewport_rect = main_frame_viewport_rect;
EnsureSendTimer();
}
void PageTimingMetricsSender::OnMainFrameAdRectangleChanged(
int element_id,
const gfx::Rect& ad_rect) {
metadata_->main_frame_ad_rects[element_id] = ad_rect;
EnsureSendTimer();
}
void PageTimingMetricsSender::UpdateResourceMetadata(
int resource_id,
bool is_main_frame_resource) {
auto it = page_resource_data_use_.find(resource_id);
if (it == page_resource_data_use_.end())
return;
it->second->SetIsMainFrameResource(is_main_frame_resource);
}
void PageTimingMetricsSender::SetUpDroppedFramesReporting(
base::ReadOnlySharedMemoryRegion shared_memory_dropped_frames) {
sender_->SetUpDroppedFramesReporting(std::move(shared_memory_dropped_frames));
}
void PageTimingMetricsSender::Update(
mojom::PageLoadTimingPtr timing,
const PageTimingMetadataRecorder::MonotonicTiming& monotonic_timing) {
if (last_timing_->Equals(*timing)) {
return;
}
// We want to make sure that each PageTimingMetricsSender is associated
// with a distinct page navigation. Because we reset the object on commit,
// we can trash last_timing_ on a provisional load before SendNow() fires.
if (!last_timing_->navigation_start.is_null() &&
last_timing_->navigation_start != timing->navigation_start) {
return;
}
// We want to force sending the metrics quickly when some loading milestones
// are reached (currently parse start, DCL, and FCP) so that the browser can
// receive the accurate number of events. This accuracy is important to
// measure the abandoned navigation.
const bool send_urgently = IsFirstFCP(last_timing_, timing) ||
IsFirstParseStart(last_timing_, timing) ||
IsFirstDCL(last_timing_, timing);
last_timing_ = std::move(timing);
metadata_recorder_.UpdateMetadata(monotonic_timing);
EnsureSendTimer(send_urgently);
}
void PageTimingMetricsSender::UpdateSoftNavigationMetrics(
mojom::SoftNavigationMetricsPtr soft_navigation_metrics) {
if (soft_navigation_metrics_->Equals(*soft_navigation_metrics)) {
return;
}
soft_navigation_metrics_ = std::move(soft_navigation_metrics);
EnsureSendTimer(true);
}
void PageTimingMetricsSender::SendCustomUserTimingMark(
mojom::CustomUserTimingMarkPtr custom_timing) {
// `custom_timing` is sent to the browser to clarify when the abandoned
// navigation happens. When the navigation is abandoned, the renderer may be
// busy, so it's important to start IPC and report UMA immediately.
CHECK(custom_timing);
sender_->SendCustomUserTiming(std::move(custom_timing));
}
void PageTimingMetricsSender::SendLatest() {
if (!timer_->IsRunning())
return;
timer_->Stop();
SendNow();
}
void PageTimingMetricsSender::UpdateCpuTiming(base::TimeDelta task_time) {
last_cpu_timing_->task_time += task_time;
EnsureSendTimer();
}
void PageTimingMetricsSender::EnsureSendTimer(bool urgent) {
if (urgent)
timer_->Stop();
else if (timer_->IsRunning())
return;
int delay_ms;
if (urgent) {
// Send as soon as possible, but not synchronously, so that all pending
// presentation callbacks for the current frame can run first.
delay_ms = 0;
} else if (have_sent_ipc_) {
// This is the typical case.
delay_ms = buffer_timer_delay_ms_;
} else {
// Send the first IPC eagerly to make sure the receiving side knows we're
// sending metrics as soon as possible.
delay_ms = kInitialTimerDelayMillis;
}
timer_->Start(FROM_HERE, base::Milliseconds(delay_ms),
base::BindOnce(&PageTimingMetricsSender::SendNow,
base::Unretained(this)));
}
void PageTimingMetricsSender::SendNow() {
have_sent_ipc_ = true;
std::vector<mojom::ResourceDataUpdatePtr> resources;
for (PageResourceDataUse* resource : modified_resources_) {
resources.push_back(resource->GetResourceDataUpdate());
if (resource->IsFinishedLoading()) {
page_resource_data_use_.erase(resource->resource_id());
}
}
sender_->SendTiming(last_timing_, metadata_, std::move(new_features_),
std::move(resources), render_data_, last_cpu_timing_,
std::move(input_timing_delta_), subresource_load_metrics_,
soft_navigation_metrics_);
input_timing_delta_ = mojom::InputTiming::New();
new_features_.clear();
metadata_->main_frame_intersection_rect.reset();
metadata_->main_frame_viewport_rect.reset();
metadata_->main_frame_ad_rects.clear();
last_cpu_timing_->task_time = base::TimeDelta();
modified_resources_.clear();
render_data_.new_layout_shifts.clear();
render_data_.layout_shift_delta = 0;
render_data_.layout_shift_delta_before_input_or_scroll = 0;
// As PageTimingMetricsSender is owned by MetricsRenderFrameObserver, which is
// instantiated for each frame, there's no need to make soft_navigation_count_
// zero here, as its value only increments through the lifetime of the frame.
}
PageResourceDataUse* PageTimingMetricsSender::FindOrInsertPageResourceDataUse(
int resource_id) {
auto& data_use = page_resource_data_use_[resource_id];
if (!data_use) {
data_use = std::make_unique<PageResourceDataUse>(resource_id);
}
return data_use.get();
}
void PageTimingMetricsSender::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) {
metadata_recorder_.AddInteractionDurationMetadata(max_event_start,
max_event_end);
metadata_recorder_.AddInteractionDurationAfterQueueingMetadata(
max_event_start, max_event_queued_main_thread, max_event_commit_finish,
max_event_end);
base::TimeDelta max_event_duration = max_event_end - max_event_start;
input_timing_delta_->user_interaction_latencies.emplace_back(
mojom::UserInteractionLatency::New(max_event_duration, interaction_offset,
max_event_start));
EnsureSendTimer();
}
} // namespace page_load_metrics