blob: fa14dba310641d83d81448cd462db56794b6a10a [file] [log] [blame]
// Copyright 2018 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 "chrome/browser/page_load_metrics/observers/ad_metrics/frame_data.h"
#include <algorithm>
#include <limits>
#include <string>
#include "base/feature_list.h"
#include "chrome/browser/page_load_metrics/observers/ad_metrics/ads_page_load_metrics_observer.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/mime_util.h"
#include "services/metrics/public/cpp/metrics_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "third_party/blink/public/common/mime_util/mime_util.h"
#include "url/gurl.h"
namespace {
// A frame with area less than kMinimumVisibleFrameArea is not considered
// visible.
const int kMinimumVisibleFrameArea = 25;
} // namespace
// static
constexpr base::TimeDelta FrameData::kCpuWindowSize;
// static
FrameData::ResourceMimeType FrameData::GetResourceMimeType(
const page_load_metrics::mojom::ResourceDataUpdatePtr& resource) {
if (blink::IsSupportedImageMimeType(resource->mime_type))
return ResourceMimeType::kImage;
if (blink::IsSupportedJavascriptMimeType(resource->mime_type))
return ResourceMimeType::kJavascript;
std::string top_level_type;
std::string subtype;
// Categorize invalid mime types as "Other".
if (!net::ParseMimeTypeWithoutParameter(resource->mime_type, &top_level_type,
&subtype)) {
return ResourceMimeType::kOther;
}
if (top_level_type.compare("video") == 0)
return ResourceMimeType::kVideo;
if (top_level_type.compare("text") == 0 && subtype.compare("css") == 0)
return ResourceMimeType::kCss;
if (top_level_type.compare("text") == 0 && subtype.compare("html") == 0)
return ResourceMimeType::kHtml;
return ResourceMimeType::kOther;
}
FrameData::FrameData(FrameTreeNodeId frame_tree_node_id,
int heavy_ad_network_threshold_noise)
: bytes_(0u),
network_bytes_(0u),
same_origin_bytes_(0u),
frame_tree_node_id_(frame_tree_node_id),
origin_status_(OriginStatus::kUnknown),
frame_navigated_(false),
user_activation_status_(UserActivationStatus::kNoActivation),
is_display_none_(false),
visibility_(FrameVisibility::kVisible),
frame_size_(gfx::Size()),
heavy_ad_status_(HeavyAdStatus::kNone),
heavy_ad_status_with_noise_(HeavyAdStatus::kNone),
heavy_ad_network_threshold_noise_(heavy_ad_network_threshold_noise) {}
FrameData::~FrameData() = default;
void FrameData::UpdateForNavigation(content::RenderFrameHost* render_frame_host,
bool frame_navigated) {
frame_navigated_ = frame_navigated;
if (!render_frame_host)
return;
SetDisplayState(render_frame_host->IsFrameDisplayNone());
if (render_frame_host->GetFrameSize())
SetFrameSize(*(render_frame_host->GetFrameSize()));
// For frames triggered on render, their origin is their parent's origin.
origin_status_ =
AdsPageLoadMetricsObserver::IsSubframeSameOriginToMainFrame(
render_frame_host, !frame_navigated /* use_parent_origin */)
? OriginStatus::kSame
: OriginStatus::kCross;
origin_ = frame_navigated
? render_frame_host->GetLastCommittedOrigin()
: render_frame_host->GetParent()->GetLastCommittedOrigin();
root_frame_depth_ = render_frame_host->GetFrameDepth();
}
void FrameData::ProcessResourceLoadInFrame(
const page_load_metrics::mojom::ResourceDataUpdatePtr& resource,
int process_id,
const page_load_metrics::ResourceTracker& resource_tracker) {
// TODO(968141): Update these metrics to include resources loaded by the
// memory cache.
if (resource->cache_type == page_load_metrics::mojom::CacheType::kMemory)
return;
bool is_same_origin = origin_.IsSameOriginWith(resource->origin);
bytes_ += resource->delta_bytes;
network_bytes_ += resource->delta_bytes;
if (is_same_origin)
same_origin_bytes_ += resource->delta_bytes;
content::GlobalRequestID global_id(process_id, resource->request_id);
if (!resource_tracker.HasPreviousUpdateForResource(global_id))
num_resources_++;
// Report cached resource body bytes to overall frame bytes.
if (resource->is_complete &&
resource->cache_type != page_load_metrics::mojom::CacheType::kNotCached) {
bytes_ += resource->encoded_body_length;
if (is_same_origin)
same_origin_bytes_ += resource->encoded_body_length;
}
if (resource->reported_as_ad_resource) {
ad_network_bytes_ += resource->delta_bytes;
ad_bytes_ += resource->delta_bytes;
// Report cached resource body bytes to overall frame bytes.
if (resource->is_complete &&
resource->cache_type != page_load_metrics::mojom::CacheType::kNotCached)
ad_bytes_ += resource->encoded_body_length;
ResourceMimeType mime_type = GetResourceMimeType(resource);
ad_bytes_by_mime_[static_cast<size_t>(mime_type)] += resource->delta_bytes;
}
}
void FrameData::AdjustAdBytes(int64_t unaccounted_ad_bytes,
ResourceMimeType mime_type) {
ad_network_bytes_ += unaccounted_ad_bytes;
ad_bytes_ += unaccounted_ad_bytes;
ad_bytes_by_mime_[static_cast<size_t>(mime_type)] += unaccounted_ad_bytes;
}
void FrameData::SetFrameSize(gfx::Size frame_size) {
frame_size_ = frame_size;
UpdateFrameVisibility();
}
void FrameData::SetDisplayState(bool is_display_none) {
is_display_none_ = is_display_none;
UpdateFrameVisibility();
}
void FrameData::UpdateCpuUsage(base::TimeTicks update_time,
base::TimeDelta update,
InteractiveStatus interactive) {
// Update the overall usage for all of the relevant buckets.
cpu_by_interactive_period_[static_cast<size_t>(interactive)] += update;
cpu_by_activation_period_[static_cast<size_t>(user_activation_status_)] +=
update;
// If the frame has been activated, then we don't update the peak usage.
if (user_activation_status_ == UserActivationStatus::kReceivedActivation)
return;
// Update the peak usage.
cpu_total_for_current_window_ += update;
cpu_updates_for_current_window_.push(CpuUpdateData(update_time, update));
base::TimeTicks cutoff_time = update_time - kCpuWindowSize;
while (!cpu_updates_for_current_window_.empty() &&
cpu_updates_for_current_window_.front().update_time < cutoff_time) {
cpu_total_for_current_window_ -=
cpu_updates_for_current_window_.front().usage_info;
cpu_updates_for_current_window_.pop();
}
int current_windowed_cpu_percent =
100 * cpu_total_for_current_window_.InMilliseconds() /
kCpuWindowSize.InMilliseconds();
if (current_windowed_cpu_percent > peak_windowed_cpu_percent_) {
peak_windowed_cpu_percent_ = current_windowed_cpu_percent;
peak_window_start_time_ =
cpu_updates_for_current_window_.front().update_time;
}
}
bool FrameData::MaybeTriggerHeavyAdIntervention() {
if (user_activation_status_ == UserActivationStatus::kReceivedActivation ||
heavy_ad_status_with_noise_ != HeavyAdStatus::kNone)
return false;
if (heavy_ad_status_ == HeavyAdStatus::kNone) {
heavy_ad_status_ =
ComputeHeavyAdStatus(false /* use_network_threshold_noise */);
}
heavy_ad_status_with_noise_ =
ComputeHeavyAdStatus(true /* use_network_threshold_noise */);
if (heavy_ad_status_with_noise_ == HeavyAdStatus::kNone)
return false;
// Only check if the feature is enabled once we have a heavy ad. This is done
// to ensure that any experiment for this feature will only be comparing
// groups who have seen a heavy ad.
if (!base::FeatureList::IsEnabled(features::kHeavyAdIntervention))
return false;
return true;
}
base::TimeDelta FrameData::GetInteractiveCpuUsage(
InteractiveStatus status) const {
return cpu_by_interactive_period_[static_cast<int>(status)];
}
base::TimeDelta FrameData::GetActivationCpuUsage(
UserActivationStatus status) const {
return cpu_by_activation_period_[static_cast<int>(status)];
}
base::TimeDelta FrameData::GetTotalCpuUsage() const {
base::TimeDelta total_cpu_time;
for (base::TimeDelta cpu_time : cpu_by_interactive_period_)
total_cpu_time += cpu_time;
return total_cpu_time;
}
void FrameData::SetReceivedUserActivation(base::TimeDelta foreground_duration) {
user_activation_status_ = UserActivationStatus::kReceivedActivation;
pre_activation_foreground_duration_ = foreground_duration;
}
size_t FrameData::GetAdNetworkBytesForMime(ResourceMimeType mime_type) const {
return ad_bytes_by_mime_[static_cast<size_t>(mime_type)];
}
void FrameData::MaybeUpdateFrameDepth(
content::RenderFrameHost* render_frame_host) {
if (!render_frame_host)
return;
DCHECK_GE(render_frame_host->GetFrameDepth(), root_frame_depth_);
if (render_frame_host->GetFrameDepth() - root_frame_depth_ > frame_depth_)
frame_depth_ = render_frame_host->GetFrameDepth() - root_frame_depth_;
}
bool FrameData::ShouldRecordFrameForMetrics() const {
return bytes() != 0 || !GetTotalCpuUsage().is_zero();
}
void FrameData::RecordAdFrameLoadUkmEvent(ukm::SourceId source_id) const {
if (!ShouldRecordFrameForMetrics())
return;
auto* ukm_recorder = ukm::UkmRecorder::Get();
ukm::builders::AdFrameLoad builder(source_id);
builder
.SetLoading_NetworkBytes(
ukm::GetExponentialBucketMinForBytes(network_bytes()))
.SetLoading_CacheBytes(
ukm::GetExponentialBucketMinForBytes((bytes() - network_bytes())))
.SetLoading_VideoBytes(ukm::GetExponentialBucketMinForBytes(
GetAdNetworkBytesForMime(ResourceMimeType::kVideo)))
.SetLoading_JavascriptBytes(ukm::GetExponentialBucketMinForBytes(
GetAdNetworkBytesForMime(ResourceMimeType::kJavascript)))
.SetLoading_ImageBytes(ukm::GetExponentialBucketMinForBytes(
GetAdNetworkBytesForMime(ResourceMimeType::kImage)))
.SetLoading_NumResources(num_resources_);
builder.SetCpuTime_Total(GetTotalCpuUsage().InMilliseconds());
if (user_activation_status() == UserActivationStatus::kReceivedActivation) {
builder.SetCpuTime_PreActivation(
GetActivationCpuUsage(UserActivationStatus::kNoActivation)
.InMilliseconds());
builder.SetTiming_PreActivationForegroundDuration(
pre_activation_foreground_duration().InMilliseconds());
}
builder.SetCpuTime_PeakWindowedPercent(peak_windowed_cpu_percent_);
builder
.SetVisibility_FrameWidth(
ukm::GetExponentialBucketMinForCounts1000(frame_size().width()))
.SetVisibility_FrameHeight(
ukm::GetExponentialBucketMinForCounts1000(frame_size().height()))
.SetVisibility_Hidden(is_display_none_);
builder.SetStatus_CrossOrigin(static_cast<int>(origin_status()))
.SetStatus_Media(static_cast<int>(media_status()))
.SetStatus_UserActivation(static_cast<int>(user_activation_status()));
builder.SetFrameDepth(frame_depth_);
if (timing_) {
if (!timing_->paint_timing.is_null() &&
timing_->paint_timing->first_contentful_paint) {
builder.SetTiming_FirstContentfulPaint(
timing_->paint_timing->first_contentful_paint->InMilliseconds());
}
if (!timing_->interactive_timing.is_null() &&
timing_->interactive_timing->interactive) {
builder.SetTiming_Interactive(
timing_->interactive_timing->interactive->InMilliseconds());
}
}
builder.Record(ukm_recorder->Get());
}
void FrameData::UpdateFrameVisibility() {
visibility_ =
!is_display_none_ &&
frame_size_.GetCheckedArea().ValueOrDefault(
std::numeric_limits<int>::max()) >= kMinimumVisibleFrameArea
? FrameVisibility::kVisible
: FrameVisibility::kNonVisible;
}
FrameData::HeavyAdStatus FrameData::ComputeHeavyAdStatus(
bool use_network_threshold_noise) const {
// Check if the frame meets the peak CPU usage threshold.
if (peak_windowed_cpu_percent_ >=
heavy_ad_thresholds::kMaxPeakWindowedPercent) {
return HeavyAdStatus::kPeakCpu;
}
// Check if the frame meets the absolute CPU time threshold.
if (GetTotalCpuUsage().InMilliseconds() >= heavy_ad_thresholds::kMaxCpuTime)
return HeavyAdStatus::kTotalCpu;
size_t network_threshold =
heavy_ad_thresholds::kMaxNetworkBytes +
(use_network_threshold_noise ? heavy_ad_network_threshold_noise_ : 0);
// Check if the frame meets the network threshold, possible including noise.
if (network_bytes_ >= network_threshold)
return HeavyAdStatus::kNetwork;
return HeavyAdStatus::kNone;
}