| // Copyright 2017 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/ad_metrics/ads_page_load_metrics_observer.h" |
| |
| #include <algorithm> |
| #include <limits> |
| #include <string> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/notreached.h" |
| #include "base/rand_util.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/time/default_tick_clock.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "components/heavy_ad_intervention/heavy_ad_blocklist.h" |
| #include "components/heavy_ad_intervention/heavy_ad_features.h" |
| #include "components/heavy_ad_intervention/heavy_ad_helper.h" |
| #include "components/heavy_ad_intervention/heavy_ad_service.h" |
| #include "components/page_load_metrics/browser/metrics_web_contents_observer.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_memory_tracker.h" |
| #include "components/page_load_metrics/browser/page_load_metrics_util.h" |
| #include "components/page_load_metrics/browser/resource_tracker.h" |
| #include "components/page_load_metrics/common/page_end_reason.h" |
| #include "components/subresource_filter/content/browser/content_subresource_filter_throttle_manager.h" |
| #include "components/subresource_filter/content/browser/content_subresource_filter_web_contents_helper.h" |
| #include "components/subresource_filter/core/browser/subresource_filter_features.h" |
| #include "components/subresource_filter/core/common/common_features.h" |
| #include "components/subresource_filter/core/common/load_policy.h" |
| #include "components/subresource_filter/core/mojom/subresource_filter.mojom.h" |
| #include "components/ukm/content/source_url_recorder.h" |
| #include "content/public/browser/global_request_id.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/reload_type.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/registry_controlled_domains/registry_controlled_domain.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/mojom/devtools/inspector_issue.mojom-shared.h" |
| #include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h" |
| #include "third_party/blink/public/mojom/web_feature/web_feature.mojom.h" |
| #include "ui/base/page_transition_types.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "url/gurl.h" |
| |
| namespace page_load_metrics { |
| |
| namespace features { |
| |
| // Enables or disables the restricted navigation ad tagging feature. When |
| // enabled, the AdTagging heuristic is modified to additional information to |
| // determine if a frame is an ad. If the frame's navigation url matches an allow |
| // list rule, it is not an ad. |
| // |
| // If a frame's navigation url does not match a blocked rule, but was created by |
| // ad script and is same domain to the top-level frame, it is not an ad. |
| // |
| // Currently this feature only changes AdTagging behavior for metrics recorded |
| // in AdsPageLoadMetricsObserver, and for triggering the Heavy Ad Intervention. |
| const base::Feature kRestrictedNavigationAdTagging{ |
| "RestrictedNavigationAdTagging", base::FEATURE_ENABLED_BY_DEFAULT}; |
| |
| } // namespace features |
| |
| namespace { |
| |
| #define ADS_HISTOGRAM(suffix, hist_macro, visibility, value) \ |
| switch (visibility) { \ |
| case kNonVisible: \ |
| hist_macro("PageLoad.Clients.Ads.NonVisible." suffix, value); \ |
| break; \ |
| case kVisible: \ |
| hist_macro("PageLoad.Clients.Ads.Visible." suffix, value); \ |
| break; \ |
| case kAnyVisibility: \ |
| hist_macro("PageLoad.Clients.Ads." suffix, value); \ |
| break; \ |
| } |
| |
| std::string GetHeavyAdReportMessage(const FrameTreeData& frame_data, |
| bool will_unload_adframe) { |
| const char kChromeStatusMessage[] = |
| "See " |
| "https://www.chromestatus.com/feature/" |
| "4800491902992384?utm_source=devtools"; |
| const char kReportingOnlyMessage[] = |
| "A future version of Chrome may remove this ad"; |
| const char kInterventionMessage[] = "Ad was removed"; |
| |
| base::StringPiece intervention_mode = |
| will_unload_adframe ? kInterventionMessage : kReportingOnlyMessage; |
| |
| switch (frame_data.heavy_ad_status_with_noise()) { |
| case HeavyAdStatus::kNetwork: |
| return base::StrCat({intervention_mode, |
| " because its network usage exceeded the limit. ", |
| kChromeStatusMessage}); |
| case HeavyAdStatus::kTotalCpu: |
| return base::StrCat({intervention_mode, |
| " because its total CPU usage exceeded the limit. ", |
| kChromeStatusMessage}); |
| case HeavyAdStatus::kPeakCpu: |
| return base::StrCat({intervention_mode, |
| " because its peak CPU usage exceeded the limit. ", |
| kChromeStatusMessage}); |
| case HeavyAdStatus::kNone: |
| NOTREACHED(); |
| return ""; |
| } |
| } |
| |
| const char kDisallowedByBlocklistHistogramName[] = |
| "PageLoad.Clients.Ads.HeavyAds.DisallowedByBlocklist"; |
| |
| void RecordHeavyAdInterventionDisallowedByBlocklist(bool disallowed) { |
| UMA_HISTOGRAM_BOOLEAN(kDisallowedByBlocklistHistogramName, disallowed); |
| } |
| |
| using ResourceMimeType = AdsPageLoadMetricsObserver::ResourceMimeType; |
| const char kIgnoredByReloadHistogramName[] = |
| "PageLoad.Clients.Ads.HeavyAds.IgnoredByReload"; |
| |
| blink::mojom::HeavyAdReason GetHeavyAdReason(HeavyAdStatus status) { |
| switch (status) { |
| case HeavyAdStatus::kNetwork: |
| return blink::mojom::HeavyAdReason::kNetworkTotalLimit; |
| case HeavyAdStatus::kTotalCpu: |
| return blink::mojom::HeavyAdReason::kCpuTotalLimit; |
| case HeavyAdStatus::kPeakCpu: |
| return blink::mojom::HeavyAdReason::kCpuPeakLimit; |
| case HeavyAdStatus::kNone: |
| NOTREACHED(); |
| return blink::mojom::HeavyAdReason::kNetworkTotalLimit; |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<AdsPageLoadMetricsObserver> |
| AdsPageLoadMetricsObserver::CreateIfNeeded( |
| content::WebContents* web_contents, |
| heavy_ad_intervention::HeavyAdService* heavy_ad_service, |
| const ApplicationLocaleGetter& application_locale_getter) { |
| // TODO(bokan): ContentSubresourceFilterThrottleManager is now associated |
| // with a FrameTree. When AdsPageLoadMetricsObserver becomes aware of MPArch |
| // this should use the associated page rather than the primary page. |
| if (!base::FeatureList::IsEnabled(subresource_filter::kAdTagging) || |
| !subresource_filter::ContentSubresourceFilterWebContentsHelper:: |
| FromWebContents(web_contents)) |
| return nullptr; |
| return std::make_unique<AdsPageLoadMetricsObserver>( |
| heavy_ad_service, application_locale_getter); |
| } |
| |
| // static |
| bool AdsPageLoadMetricsObserver::IsSubframeSameOriginToMainFrame( |
| content::RenderFrameHost* sub_host) { |
| DCHECK(sub_host); |
| content::RenderFrameHost* main_host = |
| content::WebContents::FromRenderFrameHost(sub_host)->GetMainFrame(); |
| url::Origin subframe_origin = sub_host->GetLastCommittedOrigin(); |
| url::Origin mainframe_origin = main_host->GetLastCommittedOrigin(); |
| return subframe_origin.IsSameOriginWith(mainframe_origin); |
| } |
| |
| AdsPageLoadMetricsObserver::FrameInstance::FrameInstance() |
| : owned_frame_data_(nullptr), unowned_frame_data_(nullptr) {} |
| |
| AdsPageLoadMetricsObserver::FrameInstance::FrameInstance( |
| std::unique_ptr<FrameTreeData> frame_data) |
| : owned_frame_data_(std::move(frame_data)), unowned_frame_data_(nullptr) {} |
| |
| AdsPageLoadMetricsObserver::FrameInstance::FrameInstance( |
| base::WeakPtr<FrameTreeData> frame_data) |
| : owned_frame_data_(nullptr), unowned_frame_data_(frame_data) {} |
| |
| AdsPageLoadMetricsObserver::FrameInstance::~FrameInstance() = default; |
| |
| FrameTreeData* AdsPageLoadMetricsObserver::FrameInstance::Get() { |
| if (owned_frame_data_) |
| return owned_frame_data_.get(); |
| if (unowned_frame_data_) |
| return unowned_frame_data_.get(); |
| |
| DCHECK(!unowned_frame_data_.WasInvalidated()); |
| return nullptr; |
| } |
| |
| FrameTreeData* AdsPageLoadMetricsObserver::FrameInstance::GetOwnedFrame() { |
| if (owned_frame_data_) |
| return owned_frame_data_.get(); |
| return nullptr; |
| } |
| |
| AdsPageLoadMetricsObserver::HeavyAdThresholdNoiseProvider:: |
| HeavyAdThresholdNoiseProvider(bool use_noise) |
| : use_noise_(use_noise) {} |
| |
| int AdsPageLoadMetricsObserver::HeavyAdThresholdNoiseProvider:: |
| GetNetworkThresholdNoiseForFrame() const { |
| return use_noise_ ? base::RandInt(0, kMaxNetworkThresholdNoiseBytes) : 0; |
| } |
| |
| AdsPageLoadMetricsObserver::AdsPageLoadMetricsObserver( |
| heavy_ad_intervention::HeavyAdService* heavy_ad_service, |
| const ApplicationLocaleGetter& application_locale_getter, |
| base::TickClock* clock, |
| heavy_ad_intervention::HeavyAdBlocklist* blocklist) |
| : clock_(clock ? clock : base::DefaultTickClock::GetInstance()), |
| restricted_navigation_ad_tagging_enabled_(base::FeatureList::IsEnabled( |
| features::kRestrictedNavigationAdTagging)), |
| heavy_ad_service_(heavy_ad_service), |
| application_locale_getter_(application_locale_getter), |
| heavy_ad_blocklist_(blocklist), |
| heavy_ad_privacy_mitigations_enabled_(base::FeatureList::IsEnabled( |
| heavy_ad_intervention::features::kHeavyAdPrivacyMitigations)), |
| heavy_ad_threshold_noise_provider_( |
| std::make_unique<HeavyAdThresholdNoiseProvider>( |
| heavy_ad_privacy_mitigations_enabled_ /* use_noise */)) { |
| // Manual setting of the heavy ad blocklist should be used only as a |
| // convenience for tests that don't create HeavyAdService. |
| DCHECK(!heavy_ad_service_ || !heavy_ad_blocklist_); |
| } |
| |
| AdsPageLoadMetricsObserver::~AdsPageLoadMetricsObserver() = default; |
| |
| PageLoadMetricsObserver::ObservePolicy AdsPageLoadMetricsObserver::OnStart( |
| content::NavigationHandle* navigation_handle, |
| const GURL& currently_committed_url, |
| bool started_in_foreground) { |
| navigation_id_ = navigation_handle->GetNavigationId(); |
| auto* observer_manager = |
| subresource_filter::SubresourceFilterObserverManager::FromWebContents( |
| navigation_handle->GetWebContents()); |
| // |observer_manager| isn't constructed if the feature for subresource |
| // filtering isn't enabled. |
| if (observer_manager) |
| subresource_observation_.Observe(observer_manager); |
| aggregate_frame_data_ = std::make_unique<AggregateFrameData>(); |
| return CONTINUE_OBSERVING; |
| } |
| |
| PageLoadMetricsObserver::ObservePolicy AdsPageLoadMetricsObserver::OnCommit( |
| content::NavigationHandle* navigation_handle, |
| ukm::SourceId source_id) { |
| DCHECK(ad_frames_data_.empty()); |
| |
| page_load_is_reload_ = |
| navigation_handle->GetReloadType() != content::ReloadType::NONE; |
| |
| // The main frame is never considered an ad, so it should reference an empty |
| // FrameInstance. |
| ad_frames_data_.emplace( |
| std::piecewise_construct, |
| std::forward_as_tuple(navigation_handle->GetFrameTreeNodeId()), |
| std::forward_as_tuple()); |
| |
| ProcessOngoingNavigationResource(navigation_handle); |
| |
| // If the frame is blocked by the subresource filter, we don't want to record |
| // any AdsPageLoad metrics. |
| return subresource_filter_is_enabled_ ? STOP_OBSERVING : CONTINUE_OBSERVING; |
| } |
| |
| void AdsPageLoadMetricsObserver::OnTimingUpdate( |
| content::RenderFrameHost* subframe_rfh, |
| const mojom::PageLoadTiming& timing) { |
| if (!subframe_rfh) |
| return; |
| |
| FrameTreeData* ancestor_data = |
| FindFrameData(subframe_rfh->GetFrameTreeNodeId()); |
| |
| if (!ancestor_data) |
| return; |
| |
| // Set paint eligiblity status. |
| ancestor_data->SetFirstEligibleToPaint( |
| timing.paint_timing->first_eligible_to_paint); |
| |
| // Update earliest FCP as needed. |
| bool has_new_fcp = ancestor_data->SetEarliestFirstContentfulPaint( |
| timing.paint_timing->first_contentful_paint); |
| |
| // If this is the earliest FCP for any frame in the root ad frame's subtree, |
| // set Creative Origin Status. |
| if (has_new_fcp) { |
| OriginStatus origin_status = |
| AdsPageLoadMetricsObserver::IsSubframeSameOriginToMainFrame( |
| subframe_rfh) |
| ? OriginStatus::kSame |
| : OriginStatus::kCross; |
| ancestor_data->set_creative_origin_status(origin_status); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::OnCpuTimingUpdate( |
| content::RenderFrameHost* subframe_rfh, |
| const mojom::CpuTiming& timing) { |
| // We should never trigger if the timing is null, no data should be sent. |
| DCHECK(!timing.task_time.is_zero()); |
| |
| // Get the current time, considered to be when this update occurred. |
| base::TimeTicks current_time = clock_->NowTicks(); |
| |
| FrameTreeData* ancestor_data = |
| FindFrameData(subframe_rfh->GetFrameTreeNodeId()); |
| aggregate_frame_data_->UpdateCpuUsage(current_time, timing.task_time, |
| ancestor_data); |
| if (ancestor_data) { |
| ancestor_data->UpdateCpuUsage(current_time, timing.task_time); |
| MaybeTriggerHeavyAdIntervention(subframe_rfh, ancestor_data); |
| } |
| } |
| |
| // Given an ad being triggered for a frame or navigation, get its FrameTreeData |
| // and record it into the appropriate data structures. |
| void AdsPageLoadMetricsObserver::UpdateAdFrameData( |
| content::NavigationHandle* navigation_handle, |
| bool is_adframe, |
| bool should_ignore_detected_ad) { |
| const FrameTreeNodeId ad_id = navigation_handle->GetFrameTreeNodeId(); |
| // If an existing subframe is navigating and it was an ad previously that |
| // hasn't navigated yet, then we need to update it. |
| const auto& id_and_data = ad_frames_data_.find(ad_id); |
| FrameTreeData* previous_data = id_and_data != ad_frames_data_.end() |
| ? id_and_data->second.Get() |
| : nullptr; |
| |
| if (previous_data) { |
| // Frames that are no longer ad frames or are ignored as ad frames due to |
| // restricted navigation ad tagging should have their tracked data reset. |
| // TODO(crbug.com/1101584): Simplify the condition when restricted |
| // navigation ad tagging is moved to subresource_filter/. |
| if (!is_adframe || (should_ignore_detected_ad && |
| (ad_id == previous_data->root_frame_tree_node_id()))) { |
| DCHECK_EQ(ad_id, previous_data->root_frame_tree_node_id()); |
| CleanupDeletedFrame(ad_id, previous_data, |
| true /* update_density_tracker */, |
| false /* record_metrics */); |
| |
| ad_frames_data_.erase(id_and_data); |
| |
| // Replace the tracked frame with null frame reference. This |
| // allows child frames to still be tracked as ads. |
| ad_frames_data_.emplace(std::piecewise_construct, |
| std::forward_as_tuple(ad_id), |
| std::forward_as_tuple()); |
| |
| if (should_ignore_detected_ad) |
| RecordAdFrameIgnoredByRestrictedAdTagging(true /* ignored */); |
| return; |
| } |
| |
| // As the frame has already navigated, we need to process the new navigation |
| // resource in the frame. |
| ProcessOngoingNavigationResource(navigation_handle); |
| return; |
| } |
| |
| // Determine who the parent frame's ad ancestor is. If we don't know who it |
| // is, return, such as with a frame from a previous navigation. |
| content::RenderFrameHost* parent_frame_host = |
| navigation_handle->GetParentFrame(); |
| const auto& parent_id_and_data = |
| parent_frame_host |
| ? ad_frames_data_.find(parent_frame_host->GetFrameTreeNodeId()) |
| : ad_frames_data_.end(); |
| bool parent_exists = parent_id_and_data != ad_frames_data_.end(); |
| if (!parent_exists) |
| return; |
| |
| FrameTreeData* ad_data = parent_id_and_data->second.Get(); |
| |
| bool should_create_new_frame_data = |
| !ad_data && is_adframe && !should_ignore_detected_ad; |
| |
| // If would've recorded a new ad data normally, record that a frame was |
| // ignored. |
| if (!ad_data && is_adframe && should_ignore_detected_ad) { |
| RecordAdFrameIgnoredByRestrictedAdTagging(true); |
| } |
| |
| // NOTE: Frame look-up only used for determining cross-origin |
| // status for metrics, not granting security permissions. |
| content::RenderFrameHost* ad_host = |
| (navigation_handle->HasCommitted() || |
| navigation_handle->IsWaitingToCommit()) |
| ? navigation_handle->GetRenderFrameHost() |
| : content::RenderFrameHost::FromID( |
| navigation_handle->GetPreviousRenderFrameHostId()); |
| |
| if (should_create_new_frame_data) { |
| // Construct a new FrameTreeData to track this ad frame, and update it for |
| // the navigation. |
| auto frame_data = std::make_unique<FrameTreeData>( |
| ad_id, |
| heavy_ad_threshold_noise_provider_->GetNetworkThresholdNoiseForFrame()); |
| frame_data->UpdateForNavigation(ad_host); |
| frame_data->MaybeUpdateFrameDepth(ad_host); |
| |
| FrameInstance frame_instance(std::move(frame_data)); |
| ad_frames_data_[ad_id] = std::move(frame_instance); |
| return; |
| } |
| |
| if (ad_data) |
| ad_data->MaybeUpdateFrameDepth(ad_host); |
| |
| // Don't overwrite the frame id if it is associated with an ad. |
| if (previous_data) |
| return; |
| |
| // Frames who are the children of ad frames should be associated with the |
| // ads FrameInstance. Otherwise, |ad_id| should be associated with an empty |
| // FrameInstance to indicate it is not associated with an ad, but that the |
| // frames navigation has been observed. |
| FrameInstance frame_instance; |
| if (ad_data) |
| frame_instance = FrameInstance(ad_data->AsWeakPtr()); |
| |
| ad_frames_data_[ad_id] = std::move(frame_instance); |
| } |
| |
| void AdsPageLoadMetricsObserver::ReadyToCommitNextNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // When the renderer receives a CommitNavigation message for the main frame, |
| // all subframes detach and become display : none. Since this is not user |
| // visible, and not reflective of the frames state during the page lifetime, |
| // ignore any such messages when a navigation is about to commit. |
| if (!navigation_handle->IsInMainFrame()) |
| return; |
| process_display_state_updates_ = false; |
| } |
| |
| // Determine if the frame is part of an existing ad, the root of a new ad, or a |
| // non-ad frame. Once a frame is labeled as an ad, it is always considered an |
| // ad, even if it navigates to a non-ad page. This function labels all of a |
| // page's frames, even those that fail to commit. |
| void AdsPageLoadMetricsObserver::OnDidFinishSubFrameNavigation( |
| content::NavigationHandle* navigation_handle) { |
| // If the AdsPageLoadMetricsObserver is created, this does not return nullptr. |
| auto* throttle_manager = |
| subresource_filter::ContentSubresourceFilterThrottleManager:: |
| FromNavigationHandle(*navigation_handle); |
| DCHECK(throttle_manager); |
| |
| const bool is_adframe = throttle_manager->IsFrameTaggedAsAd( |
| navigation_handle->GetFrameTreeNodeId()); |
| |
| // TODO(https://crbug.com/1030325): The following block is a hack to ignore |
| // certain frames that are detected by AdTagging. These frames are ignored |
| // specifically for ad metrics and for the heavy ad intervention. The frames |
| // ignored here are still considered ads by the heavy ad intervention. This |
| // logic should be moved into /subresource_filter/ and applied to all of ad |
| // tagging, rather than being implemented in AdsPLMO. |
| bool should_ignore_detected_ad = false; |
| absl::optional<subresource_filter::LoadPolicy> load_policy = |
| throttle_manager->LoadPolicyForLastCommittedNavigation( |
| navigation_handle->GetFrameTreeNodeId()); |
| |
| // Only un-tag frames as ads if the navigation has committed. This prevents |
| // frames from being untagged that have an aborted navigation to allowlist |
| // urls. |
| if (restricted_navigation_ad_tagging_enabled_ && load_policy && |
| navigation_handle->GetNetErrorCode() == net::OK && |
| navigation_handle->HasCommitted()) { |
| // If a filter list explicitly allows the rule, we should ignore a detected |
| // ad. |
| bool navigation_is_explicitly_allowed = |
| *load_policy == subresource_filter::LoadPolicy::EXPLICITLY_ALLOW; |
| |
| const GURL& last_committed_url = |
| navigation_handle->GetRenderFrameHost()->GetLastCommittedURL(); |
| const GURL& main_frame_last_committed_url = |
| navigation_handle->GetRenderFrameHost() |
| ->GetMainFrame() |
| ->GetLastCommittedURL(); |
| // If a frame is detected to be an ad, but is same domain to the top frame, |
| // and does not match a disallowed rule, ignore it. |
| bool should_ignore_same_domain_ad = |
| (*load_policy != subresource_filter::LoadPolicy::DISALLOW) && |
| (*load_policy != subresource_filter::LoadPolicy::WOULD_DISALLOW) && |
| net::registry_controlled_domains::SameDomainOrHost( |
| last_committed_url, main_frame_last_committed_url, |
| net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
| should_ignore_detected_ad = |
| navigation_is_explicitly_allowed || should_ignore_same_domain_ad; |
| } |
| |
| UpdateAdFrameData(navigation_handle, is_adframe, should_ignore_detected_ad); |
| |
| ProcessOngoingNavigationResource(navigation_handle); |
| } |
| |
| void AdsPageLoadMetricsObserver::FrameReceivedUserActivation( |
| content::RenderFrameHost* render_frame_host) { |
| FrameTreeData* ancestor_data = |
| FindFrameData(render_frame_host->GetFrameTreeNodeId()); |
| if (ancestor_data) { |
| ancestor_data->set_received_user_activation(); |
| } |
| } |
| |
| PageLoadMetricsObserver::ObservePolicy |
| AdsPageLoadMetricsObserver::FlushMetricsOnAppEnterBackground( |
| const mojom::PageLoadTiming& timing) { |
| // The browser may come back, but there is no guarantee. To be safe, record |
| // what we have now and keep tracking only for the purposes of interventions. |
| if (GetDelegate().DidCommit() && !histograms_recorded_) |
| RecordHistograms(GetDelegate().GetPageUkmSourceId()); |
| // Even if we didn't commit/record histograms, set histograms_recorded_ to |
| // true, because this preserves the behavior of not reporting after the |
| // browser app has been backgrounded. |
| histograms_recorded_ = true; |
| |
| // TODO(ericrobinson): We could potentially make this contingent on whether |
| // heavy_ads is enabled, but it's probably simpler to continue to monitor |
| // silently in case future interventions require similar behavior. |
| return CONTINUE_OBSERVING; |
| } |
| |
| void AdsPageLoadMetricsObserver::OnComplete( |
| const mojom::PageLoadTiming& timing) { |
| // If Chrome was backgrounded previously, then we have already recorded the |
| // histograms, otherwise we need to. |
| if (!histograms_recorded_) |
| RecordHistograms(GetDelegate().GetPageUkmSourceId()); |
| histograms_recorded_ = true; |
| } |
| |
| void AdsPageLoadMetricsObserver::OnResourceDataUseObserved( |
| content::RenderFrameHost* rfh, |
| const std::vector<mojom::ResourceDataUpdatePtr>& resources) { |
| for (auto const& resource : resources) { |
| ProcessResourceForPage(rfh->GetProcess()->GetID(), resource); |
| ProcessResourceForFrame(rfh, resource); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::FrameDisplayStateChanged( |
| content::RenderFrameHost* render_frame_host, |
| bool is_display_none) { |
| if (!process_display_state_updates_) |
| return; |
| FrameTreeData* ancestor_data = |
| FindFrameData(render_frame_host->GetFrameTreeNodeId()); |
| // If the frame whose display state has changed is the root of the ad ancestry |
| // chain, then update it. The display property is propagated to all child |
| // frames. |
| if (ancestor_data && render_frame_host->GetFrameTreeNodeId() == |
| ancestor_data->root_frame_tree_node_id()) { |
| ancestor_data->SetDisplayState(is_display_none); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::FrameSizeChanged( |
| content::RenderFrameHost* render_frame_host, |
| const gfx::Size& frame_size) { |
| FrameTreeData* ancestor_data = |
| FindFrameData(render_frame_host->GetFrameTreeNodeId()); |
| // If the frame whose size has changed is the root of the ad ancestry chain, |
| // then update it |
| if (ancestor_data && render_frame_host->GetFrameTreeNodeId() == |
| ancestor_data->root_frame_tree_node_id()) { |
| ancestor_data->SetFrameSize(frame_size); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::MediaStartedPlaying( |
| const content::WebContentsObserver::MediaPlayerInfo& video_type, |
| content::RenderFrameHost* render_frame_host) { |
| FrameTreeData* ancestor_data = |
| FindFrameData(render_frame_host->GetFrameTreeNodeId()); |
| if (ancestor_data) |
| ancestor_data->set_media_status(MediaStatus::kPlayed); |
| } |
| |
| void AdsPageLoadMetricsObserver::OnFrameIntersectionUpdate( |
| content::RenderFrameHost* render_frame_host, |
| const mojom::FrameIntersectionUpdate& intersection_update) { |
| if (!intersection_update.main_frame_intersection_rect) |
| return; |
| |
| int frame_tree_node_id = render_frame_host->GetFrameTreeNodeId(); |
| if (render_frame_host == GetDelegate().GetWebContents()->GetMainFrame()) { |
| page_ad_density_tracker_.UpdateMainFrameRect( |
| *intersection_update.main_frame_intersection_rect); |
| return; |
| } |
| |
| // If the frame whose size has changed is the root of the ad ancestry chain, |
| // then update it. |
| FrameTreeData* ancestor_data = FindFrameData(frame_tree_node_id); |
| if (ancestor_data && |
| frame_tree_node_id == ancestor_data->root_frame_tree_node_id()) { |
| page_ad_density_tracker_.RemoveRect(frame_tree_node_id); |
| // Only add frames if they are visible. |
| if (!ancestor_data->is_display_none()) { |
| page_ad_density_tracker_.AddRect( |
| frame_tree_node_id, |
| *intersection_update.main_frame_intersection_rect); |
| } |
| } |
| |
| CheckForAdDensityViolation(); |
| } |
| |
| // TODO(https://crbug.com/1142669): Evaluate imposing width requirements |
| // for ad density violations. |
| void AdsPageLoadMetricsObserver::CheckForAdDensityViolation() { |
| #if BUILDFLAG(IS_ANDROID) |
| const int kMaxMobileAdDensityByHeight = 30; |
| if (page_ad_density_tracker_.MaxPageAdDensityByHeight() > |
| kMaxMobileAdDensityByHeight) { |
| // TODO(bokan): ContentSubresourceFilterThrottleManager is now associated |
| // with a FrameTree. When AdsPageLoadMetricsObserver becomes aware of MPArch |
| // this should use the associated page rather than the primary page. |
| auto* throttle_manager = |
| subresource_filter::ContentSubresourceFilterThrottleManager::FromPage( |
| GetDelegate().GetWebContents()->GetPrimaryPage()); |
| // AdsPageLoadMetricsObserver is not created unless there is a |
| // throttle manager. |
| DCHECK(throttle_manager); |
| |
| // Violations can be triggered multiple times for the same page as |
| // violations after the first are ignored. Ad frame violations are |
| // attributed to the main frame url. |
| throttle_manager->OnAdsViolationTriggered( |
| GetDelegate().GetWebContents()->GetMainFrame(), |
| subresource_filter::mojom::AdsViolation:: |
| kMobileAdDensityByHeightAbove30); |
| } |
| #endif |
| } |
| |
| void AdsPageLoadMetricsObserver::OnSubFrameDeleted(int frame_tree_node_id) { |
| const auto& id_and_data = ad_frames_data_.find(frame_tree_node_id); |
| if (id_and_data == ad_frames_data_.end()) |
| return; |
| |
| FrameTreeData* ancestor_data = nullptr; |
| bool is_root_ad = false; |
| |
| if ((ancestor_data = id_and_data->second.GetOwnedFrame())) |
| is_root_ad = true; |
| else |
| ancestor_data = id_and_data->second.Get(); |
| |
| if (ancestor_data) { |
| // If an ad frame has been deleted, update the aggregate memory usage by |
| // removing the entry for this frame. |
| // Moreover, if the root ad frame has been deleted, all child frames should |
| // be deleted by this point, so flush histograms for the frame. |
| CleanupDeletedFrame(id_and_data->first, ancestor_data, |
| is_root_ad /* update_density_tracker */, |
| is_root_ad /* record_metrics */); |
| } |
| |
| // Delete the frame data. |
| ad_frames_data_.erase(id_and_data); |
| } |
| |
| void AdsPageLoadMetricsObserver::OnV8MemoryChanged( |
| const std::vector<MemoryUpdate>& memory_updates) { |
| for (const auto& update : memory_updates) { |
| memory_update_count_++; |
| |
| content::RenderFrameHost* render_frame_host = |
| content::RenderFrameHost::FromID(update.routing_id); |
| |
| if (!render_frame_host) |
| continue; |
| |
| FrameTreeNodeId frame_node_id = render_frame_host->GetFrameTreeNodeId(); |
| FrameTreeData* ad_frame_data = FindFrameData(frame_node_id); |
| |
| if (ad_frame_data) { |
| ad_frame_data->UpdateMemoryUsage(update.delta_bytes); |
| UpdateAggregateMemoryUsage(update.delta_bytes, |
| ad_frame_data->visibility()); |
| } else if (!render_frame_host->GetParent()) { |
| // |render_frame_host| is the main frame. |
| aggregate_frame_data_->update_main_frame_memory(update.delta_bytes); |
| } |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::OnSubresourceFilterGoingAway() { |
| subresource_observation_.Reset(); |
| } |
| |
| void AdsPageLoadMetricsObserver::OnPageActivationComputed( |
| content::NavigationHandle* navigation_handle, |
| const subresource_filter::mojom::ActivationState& activation_state) { |
| DCHECK(navigation_handle); |
| DCHECK_GE(navigation_id_, 0); |
| |
| // The subresource filter's activation level and navigation id is the same for |
| // all frames on a page, so we only record this for the main frame. |
| if (navigation_handle->IsInMainFrame() && |
| navigation_handle->GetNavigationId() == navigation_id_ && |
| activation_state.activation_level == |
| subresource_filter::mojom::ActivationLevel::kEnabled) { |
| DCHECK(!subresource_filter_is_enabled_); |
| subresource_filter_is_enabled_ = true; |
| } |
| } |
| |
| int AdsPageLoadMetricsObserver::GetUnaccountedAdBytes( |
| int process_id, |
| const mojom::ResourceDataUpdatePtr& resource) const { |
| if (!resource->reported_as_ad_resource) |
| return 0; |
| content::GlobalRequestID global_request_id(process_id, resource->request_id); |
| |
| // Resource just started loading. |
| if (!GetDelegate().GetResourceTracker().HasPreviousUpdateForResource( |
| global_request_id)) |
| return 0; |
| |
| // If the resource had already started loading, and is now labeled as an ad, |
| // but was not before, we need to account for all the previously received |
| // bytes. |
| auto const& previous_update = |
| GetDelegate().GetResourceTracker().GetPreviousUpdateForResource( |
| global_request_id); |
| bool is_new_ad = !previous_update->reported_as_ad_resource; |
| return is_new_ad ? resource->received_data_length - resource->delta_bytes : 0; |
| } |
| |
| void AdsPageLoadMetricsObserver::ProcessResourceForPage( |
| int process_id, |
| const mojom::ResourceDataUpdatePtr& resource) { |
| auto mime_type = ResourceLoadAggregator::GetResourceMimeType(resource); |
| int unaccounted_ad_bytes = GetUnaccountedAdBytes(process_id, resource); |
| bool is_main_frame = resource->is_main_frame_resource; |
| aggregate_frame_data_->ProcessResourceLoadInFrame(resource, is_main_frame); |
| if (unaccounted_ad_bytes) |
| aggregate_frame_data_->AdjustAdBytes(unaccounted_ad_bytes, mime_type, |
| is_main_frame); |
| } |
| |
| void AdsPageLoadMetricsObserver::ProcessResourceForFrame( |
| content::RenderFrameHost* render_frame_host, |
| const mojom::ResourceDataUpdatePtr& resource) { |
| const auto& id_and_data = |
| ad_frames_data_.find(render_frame_host->GetFrameTreeNodeId()); |
| if (id_and_data == ad_frames_data_.end()) { |
| if (resource->is_primary_frame_resource) { |
| // Only hold onto primary resources if their load has finished, otherwise |
| // we will receive a future update for them if the navigation finishes. |
| if (!resource->is_complete) |
| return; |
| |
| // This resource request is the primary resource load for a frame that |
| // hasn't yet finished navigating. Hang onto the request info and replay |
| // it once the frame finishes navigating. |
| ongoing_navigation_resources_.emplace( |
| std::piecewise_construct, |
| std::forward_as_tuple(render_frame_host->GetFrameTreeNodeId()), |
| std::forward_as_tuple(resource.Clone())); |
| } else { |
| // This is unexpected, it could be: |
| // 1. a resource from a previous navigation that started its resource |
| // load after this page started navigation. |
| // 2. possibly a resource from a document.written frame whose frame |
| // failure message has yet to arrive. (uncertain of this) |
| } |
| return; |
| } |
| |
| // Determine if the frame (or its ancestor) is an ad, if so attribute the |
| // bytes to the highest ad ancestor. |
| FrameTreeData* ancestor_data = id_and_data->second.Get(); |
| if (!ancestor_data) |
| return; |
| |
| auto mime_type = ResourceLoadAggregator::GetResourceMimeType(resource); |
| int unaccounted_ad_bytes = |
| GetUnaccountedAdBytes(render_frame_host->GetProcess()->GetID(), resource); |
| if (unaccounted_ad_bytes) |
| ancestor_data->AdjustAdBytes(unaccounted_ad_bytes, mime_type); |
| ancestor_data->ProcessResourceLoadInFrame( |
| resource, render_frame_host->GetProcess()->GetID(), |
| GetDelegate().GetResourceTracker()); |
| MaybeTriggerHeavyAdIntervention(render_frame_host, ancestor_data); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordPageResourceTotalHistograms( |
| ukm::SourceId source_id) { |
| const auto& resource_data = aggregate_frame_data_->resource_data(); |
| |
| // Only records histograms on pages that have some ad bytes. |
| if (resource_data.ad_bytes() == 0) |
| return; |
| PAGE_BYTES_HISTOGRAM("PageLoad.Clients.Ads.Resources.Bytes.Ads2", |
| resource_data.ad_network_bytes()); |
| |
| if (page_ad_density_tracker_.MaxPageAdDensityByArea() != -1) { |
| UMA_HISTOGRAM_PERCENTAGE("PageLoad.Clients.Ads.AdDensity.MaxPercentByArea", |
| page_ad_density_tracker_.MaxPageAdDensityByArea()); |
| } |
| |
| if (page_ad_density_tracker_.MaxPageAdDensityByHeight() != -1) { |
| UMA_HISTOGRAM_PERCENTAGE( |
| "PageLoad.Clients.Ads.AdDensity.MaxPercentByHeight", |
| page_ad_density_tracker_.MaxPageAdDensityByHeight()); |
| } |
| |
| // Records true if both of the density calculations succeeded on the page. |
| UMA_HISTOGRAM_BOOLEAN( |
| "PageLoad.Clients.Ads.AdDensity.Recorded", |
| page_ad_density_tracker_.MaxPageAdDensityByArea() != -1 && |
| page_ad_density_tracker_.MaxPageAdDensityByHeight() != -1); |
| |
| auto* ukm_recorder = ukm::UkmRecorder::Get(); |
| ukm::builders::AdPageLoad builder(source_id); |
| builder.SetTotalBytes(resource_data.network_bytes() >> 10) |
| .SetAdBytes(resource_data.ad_network_bytes() >> 10) |
| .SetAdJavascriptBytes(resource_data.GetAdNetworkBytesForMime( |
| ResourceMimeType::kJavascript) >> |
| 10) |
| .SetAdVideoBytes( |
| resource_data.GetAdNetworkBytesForMime(ResourceMimeType::kVideo) >> |
| 10) |
| .SetMainframeAdBytes(ukm::GetExponentialBucketMinForBytes( |
| aggregate_frame_data_->main_frame_resource_data().ad_network_bytes())) |
| .SetMaxAdDensityByArea(page_ad_density_tracker_.MaxPageAdDensityByArea()) |
| .SetMaxAdDensityByHeight( |
| page_ad_density_tracker_.MaxPageAdDensityByHeight()); |
| |
| // Record cpu metrics for the page. |
| builder.SetAdCpuTime( |
| aggregate_frame_data_->total_ad_cpu_usage().InMilliseconds()); |
| builder.Record(ukm_recorder->Get()); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordHistograms(ukm::SourceId source_id) { |
| // Record per-frame metrics for any existing frames. |
| for (auto& id_and_instance : ad_frames_data_) { |
| // We only log metrics for FrameInstance which own a FrameTreeData, |
| // otherwise we would be double counting frames. |
| if (FrameTreeData* frame_data = id_and_instance.second.GetOwnedFrame()) { |
| RecordPerFrameMetrics(*frame_data, source_id); |
| } |
| } |
| |
| RecordAggregateHistogramsForAdTagging(FrameVisibility::kNonVisible); |
| RecordAggregateHistogramsForAdTagging(FrameVisibility::kVisible); |
| RecordAggregateHistogramsForAdTagging(FrameVisibility::kAnyVisibility); |
| RecordAggregateHistogramsForCpuUsage(); |
| RecordAggregateHistogramsForHeavyAds(); |
| RecordPageResourceTotalHistograms(source_id); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordAggregateHistogramsForCpuUsage() { |
| // If the page has an ad with the relevant visibility and non-zero bytes. |
| if (aggregate_frame_data_ |
| ->get_ad_data_by_visibility(FrameVisibility::kAnyVisibility) |
| .frames == 0) { |
| return; |
| } |
| |
| // Only record cpu usage aggregate data for the AnyVisibility suffix as these |
| // numbers do not change for different visibility types. |
| FrameVisibility visibility = FrameVisibility::kAnyVisibility; |
| |
| // Record the aggregate data, which is never considered activated. |
| // TODO(crbug/1109754): Does it make sense to include an aggregate peak |
| // windowed percent? Obviously this would be a max of maxes, but might be |
| // useful to have that for comparisons as well. |
| ADS_HISTOGRAM("Cpu.AdFrames.Aggregate.TotalUsage2", PAGE_LOAD_HISTOGRAM, |
| visibility, aggregate_frame_data_->total_ad_cpu_usage()); |
| ADS_HISTOGRAM("Cpu.NonAdFrames.Aggregate.TotalUsage2", PAGE_LOAD_HISTOGRAM, |
| visibility, |
| aggregate_frame_data_->total_cpu_usage() - |
| aggregate_frame_data_->total_ad_cpu_usage()); |
| ADS_HISTOGRAM("Cpu.NonAdFrames.Aggregate.PeakWindowedPercent2", |
| UMA_HISTOGRAM_PERCENTAGE, visibility, |
| aggregate_frame_data_->peak_windowed_non_ad_cpu_percent()); |
| ADS_HISTOGRAM("Cpu.FullPage.TotalUsage2", PAGE_LOAD_HISTOGRAM, visibility, |
| aggregate_frame_data_->total_cpu_usage()); |
| ADS_HISTOGRAM("Cpu.FullPage.PeakWindowedPercent2", UMA_HISTOGRAM_PERCENTAGE, |
| visibility, aggregate_frame_data_->peak_windowed_cpu_percent()); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordAggregateHistogramsForAdTagging( |
| FrameVisibility visibility) { |
| const auto& resource_data = aggregate_frame_data_->resource_data(); |
| |
| if (resource_data.bytes() == 0) |
| return; |
| |
| const auto& visibility_data = |
| aggregate_frame_data_->get_ad_data_by_visibility(visibility); |
| |
| ADS_HISTOGRAM("FrameCounts.AdFrames.Total", UMA_HISTOGRAM_COUNTS_1000, |
| visibility, visibility_data.frames); |
| |
| // Only record AllPages histograms for the AnyVisibility suffix as these |
| // numbers do not change for different visibility types. |
| if (visibility == FrameVisibility::kAnyVisibility) { |
| ADS_HISTOGRAM("AllPages.PercentTotalBytesAds", UMA_HISTOGRAM_PERCENTAGE, |
| visibility, |
| resource_data.ad_bytes() * 100 / resource_data.bytes()); |
| if (resource_data.network_bytes()) { |
| ADS_HISTOGRAM("AllPages.PercentNetworkBytesAds", UMA_HISTOGRAM_PERCENTAGE, |
| visibility, |
| resource_data.ad_network_bytes() * 100 / |
| resource_data.network_bytes()); |
| } |
| ADS_HISTOGRAM( |
| "AllPages.NonAdNetworkBytes", PAGE_BYTES_HISTOGRAM, visibility, |
| resource_data.network_bytes() - resource_data.ad_network_bytes()); |
| } |
| |
| // Only post AllPages and FrameCounts UMAs for pages that don't have ads. |
| if (visibility_data.frames == 0) |
| return; |
| |
| ADS_HISTOGRAM("Bytes.NonAdFrames.Aggregate.Total2", PAGE_BYTES_HISTOGRAM, |
| visibility, resource_data.bytes() - visibility_data.bytes); |
| |
| ADS_HISTOGRAM("Bytes.FullPage.Total2", PAGE_BYTES_HISTOGRAM, visibility, |
| resource_data.bytes()); |
| ADS_HISTOGRAM("Bytes.FullPage.Network", PAGE_BYTES_HISTOGRAM, visibility, |
| resource_data.network_bytes()); |
| |
| if (resource_data.bytes()) { |
| ADS_HISTOGRAM("Bytes.FullPage.Total2.PercentAdFrames", |
| UMA_HISTOGRAM_PERCENTAGE, visibility, |
| visibility_data.bytes * 100 / resource_data.bytes()); |
| } |
| if (resource_data.network_bytes()) { |
| ADS_HISTOGRAM( |
| "Bytes.FullPage.Network.PercentAdFrames", UMA_HISTOGRAM_PERCENTAGE, |
| visibility, |
| visibility_data.network_bytes * 100 / resource_data.network_bytes()); |
| } |
| |
| ADS_HISTOGRAM("Bytes.AdFrames.Aggregate.Total2", PAGE_BYTES_HISTOGRAM, |
| visibility, visibility_data.bytes); |
| ADS_HISTOGRAM("Bytes.AdFrames.Aggregate.Network", PAGE_BYTES_HISTOGRAM, |
| visibility, visibility_data.network_bytes); |
| if (base::FeatureList::IsEnabled(::features::kV8PerFrameMemoryMonitoring)) { |
| ADS_HISTOGRAM("Memory.Aggregate.Max", PAGE_BYTES_HISTOGRAM, visibility, |
| visibility_data.memory.max_bytes_used()); |
| } |
| |
| // Only record same origin and main frame totals for the AnyVisibility suffix |
| // as these numbers do not change for different visibility types. |
| if (visibility != FrameVisibility::kAnyVisibility) |
| return; |
| |
| const auto& main_frame_resource_data = |
| aggregate_frame_data_->main_frame_resource_data(); |
| ADS_HISTOGRAM("Bytes.MainFrame.Network", PAGE_BYTES_HISTOGRAM, visibility, |
| main_frame_resource_data.network_bytes()); |
| ADS_HISTOGRAM("Bytes.MainFrame.Total2", PAGE_BYTES_HISTOGRAM, visibility, |
| main_frame_resource_data.bytes()); |
| ADS_HISTOGRAM("Bytes.MainFrame.Ads.Network", PAGE_BYTES_HISTOGRAM, visibility, |
| main_frame_resource_data.ad_network_bytes()); |
| ADS_HISTOGRAM("Bytes.MainFrame.Ads.Total2", PAGE_BYTES_HISTOGRAM, visibility, |
| main_frame_resource_data.ad_bytes()); |
| if (base::FeatureList::IsEnabled(::features::kV8PerFrameMemoryMonitoring)) { |
| PAGE_BYTES_HISTOGRAM("PageLoad.Clients.Ads.Memory.MainFrame.Max", |
| aggregate_frame_data_->main_frame_max_memory()); |
| UMA_HISTOGRAM_COUNTS_10000("PageLoad.Clients.Ads.Memory.UpdateCount", |
| memory_update_count_); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordAggregateHistogramsForHeavyAds() { |
| if (!heavy_ad_on_page_) |
| return; |
| |
| UMA_HISTOGRAM_BOOLEAN("PageLoad.Clients.Ads.HeavyAds.UserDidReload", |
| GetDelegate().GetPageEndReason() == END_RELOAD); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordPerFrameMetrics( |
| const FrameTreeData& ad_frame_data, |
| ukm::SourceId source_id) { |
| // If we've previously recorded histograms, then don't do anything. |
| if (histograms_recorded_) |
| return; |
| RecordPerFrameHistogramsForCpuUsage(ad_frame_data); |
| RecordPerFrameHistogramsForAdTagging(ad_frame_data); |
| RecordPerFrameHistogramsForHeavyAds(ad_frame_data); |
| ad_frame_data.RecordAdFrameLoadUkmEvent(source_id); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordPerFrameHistogramsForCpuUsage( |
| const FrameTreeData& ad_frame_data) { |
| // This aggregate gets reported regardless of whether the frame used bytes. |
| aggregate_frame_data_->update_ad_cpu_usage(ad_frame_data.GetTotalCpuUsage()); |
| |
| if (!ad_frame_data.ShouldRecordFrameForMetrics()) |
| return; |
| |
| // Record per-frame histograms to the appropriate visibility prefixes. |
| for (const auto visibility : |
| {FrameVisibility::kAnyVisibility, ad_frame_data.visibility()}) { |
| // Report the peak windowed usage, which is independent of activation status |
| // (measured only for the unactivated period). |
| ADS_HISTOGRAM("Cpu.AdFrames.PerFrame.PeakWindowedPercent2", |
| UMA_HISTOGRAM_PERCENTAGE, visibility, |
| ad_frame_data.peak_windowed_cpu_percent()); |
| |
| if (ad_frame_data.user_activation_status() == |
| UserActivationStatus::kNoActivation) { |
| ADS_HISTOGRAM("Cpu.AdFrames.PerFrame.TotalUsage2.Unactivated", |
| PAGE_LOAD_HISTOGRAM, visibility, |
| ad_frame_data.GetTotalCpuUsage()); |
| } else { |
| base::TimeDelta task_duration_pre = ad_frame_data.GetActivationCpuUsage( |
| UserActivationStatus::kNoActivation); |
| base::TimeDelta task_duration_post = ad_frame_data.GetActivationCpuUsage( |
| UserActivationStatus::kReceivedActivation); |
| base::TimeDelta task_duration_total = |
| task_duration_pre + task_duration_post; |
| ADS_HISTOGRAM("Cpu.AdFrames.PerFrame.TotalUsage2.Activated", |
| PAGE_LOAD_HISTOGRAM, visibility, task_duration_total); |
| ADS_HISTOGRAM("Cpu.AdFrames.PerFrame.TotalUsage2.Activated.PreActivation", |
| PAGE_LOAD_HISTOGRAM, visibility, task_duration_pre); |
| ADS_HISTOGRAM( |
| "Cpu.AdFrames.PerFrame.TotalUsage2.Activated.PostActivation", |
| PAGE_LOAD_HISTOGRAM, visibility, task_duration_post); |
| } |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordPerFrameHistogramsForAdTagging( |
| const FrameTreeData& ad_frame_data) { |
| if (!ad_frame_data.ShouldRecordFrameForMetrics()) |
| return; |
| |
| RecordAdFrameIgnoredByRestrictedAdTagging(false /*ignored */); |
| |
| // Record per-frame histograms to the appropriate visibility prefixes. |
| for (const auto visibility : |
| {FrameVisibility::kAnyVisibility, ad_frame_data.visibility()}) { |
| const auto& resource_data = ad_frame_data.resource_data(); |
| |
| // Update aggregate ad information. |
| aggregate_frame_data_->update_ad_bytes_by_visibility(visibility, |
| resource_data.bytes()); |
| aggregate_frame_data_->update_ad_network_bytes_by_visibility( |
| visibility, resource_data.network_bytes()); |
| aggregate_frame_data_->update_ad_frames_by_visibility(visibility, 1); |
| |
| ADS_HISTOGRAM("Bytes.AdFrames.PerFrame.Total2", PAGE_BYTES_HISTOGRAM, |
| visibility, resource_data.bytes()); |
| ADS_HISTOGRAM("Bytes.AdFrames.PerFrame.Network", PAGE_BYTES_HISTOGRAM, |
| visibility, resource_data.network_bytes()); |
| if (base::FeatureList::IsEnabled(::features::kV8PerFrameMemoryMonitoring)) { |
| ADS_HISTOGRAM("Memory.PerFrame.Max", PAGE_BYTES_HISTOGRAM, visibility, |
| ad_frame_data.v8_max_memory_bytes_used()); |
| } |
| ADS_HISTOGRAM("FrameCounts.AdFrames.PerFrame.OriginStatus", |
| UMA_HISTOGRAM_ENUMERATION, visibility, |
| ad_frame_data.origin_status()); |
| |
| ADS_HISTOGRAM("FrameCounts.AdFrames.PerFrame.CreativeOriginStatus", |
| UMA_HISTOGRAM_ENUMERATION, visibility, |
| ad_frame_data.creative_origin_status()); |
| |
| ADS_HISTOGRAM( |
| "FrameCounts.AdFrames.PerFrame.CreativeOriginStatusWithThrottling", |
| UMA_HISTOGRAM_ENUMERATION, visibility, |
| ad_frame_data.GetCreativeOriginStatusWithThrottling()); |
| |
| ADS_HISTOGRAM("FrameCounts.AdFrames.PerFrame.UserActivation", |
| UMA_HISTOGRAM_ENUMERATION, visibility, |
| ad_frame_data.user_activation_status()); |
| |
| if (auto first_contentful_paint = |
| ad_frame_data.earliest_first_contentful_paint()) { |
| ADS_HISTOGRAM("AdPaintTiming.NavigationToFirstContentfulPaint3", |
| PAGE_LOAD_LONG_HISTOGRAM, visibility, |
| first_contentful_paint.value()); |
| } |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordPerFrameHistogramsForHeavyAds( |
| const FrameTreeData& ad_frame_data) { |
| if (!ad_frame_data.ShouldRecordFrameForMetrics()) |
| return; |
| |
| // Record per-frame histograms to the appropriate visibility prefixes. |
| for (const auto visibility : |
| {FrameVisibility::kAnyVisibility, ad_frame_data.visibility()}) { |
| ADS_HISTOGRAM("HeavyAds.ComputedTypeWithThresholdNoise", |
| UMA_HISTOGRAM_ENUMERATION, visibility, |
| ad_frame_data.heavy_ad_status_with_noise()); |
| } |
| |
| // Only record the following histograms if the frame was a heavy ad. |
| if (ad_frame_data.heavy_ad_status_with_noise() == HeavyAdStatus::kNone) |
| return; |
| |
| heavy_ad_on_page_ = true; |
| |
| // Record whether the frame was removed prior to the page being unloaded. |
| UMA_HISTOGRAM_BOOLEAN( |
| "PageLoad.Clients.Ads.HeavyAds.FrameRemovedPriorToPageEnd", |
| GetDelegate().GetPageEndReason() == END_NONE); |
| } |
| |
| void AdsPageLoadMetricsObserver::ProcessOngoingNavigationResource( |
| content::NavigationHandle* navigation_handle) { |
| const auto& frame_id_and_request = ongoing_navigation_resources_.find( |
| navigation_handle->GetFrameTreeNodeId()); |
| if (frame_id_and_request == ongoing_navigation_resources_.end()) |
| return; |
| |
| // NOTE: Frame look-up is not for granting security permissions. |
| content::RenderFrameHost* rfh = |
| (navigation_handle->HasCommitted() || |
| navigation_handle->IsWaitingToCommit()) |
| ? navigation_handle->GetRenderFrameHost() |
| : content::RenderFrameHost::FromID( |
| navigation_handle->GetPreviousRenderFrameHostId()); |
| |
| ProcessResourceForFrame(rfh, frame_id_and_request->second); |
| ongoing_navigation_resources_.erase(frame_id_and_request); |
| } |
| |
| void AdsPageLoadMetricsObserver::RecordAdFrameIgnoredByRestrictedAdTagging( |
| bool ignored) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "PageLoad.Clients.Ads.FrameCounts.IgnoredByRestrictedAdTagging", ignored); |
| } |
| |
| FrameTreeData* AdsPageLoadMetricsObserver::FindFrameData(FrameTreeNodeId id) { |
| const auto& id_and_data = ad_frames_data_.find(id); |
| if (id_and_data == ad_frames_data_.end()) |
| return nullptr; |
| |
| return id_and_data->second.Get(); |
| } |
| |
| void AdsPageLoadMetricsObserver::MaybeTriggerStrictHeavyAdIntervention() { |
| DCHECK(heavy_ads_blocklist_reason_.has_value()); |
| if (heavy_ads_blocklist_reason_ != |
| blocklist::BlocklistReason::kUserOptedOutOfHost) |
| return; |
| |
| // TODO(bokan): ContentSubresourceFilterThrottleManager is now associated |
| // with a FrameTree. When AdsPageLoadMetricsObserver becomes aware of MPArch |
| // this should use the associated page rather than the primary page. |
| auto* throttle_manager = |
| subresource_filter::ContentSubresourceFilterThrottleManager::FromPage( |
| GetDelegate().GetWebContents()->GetPrimaryPage()); |
| // AdsPageLoadMetricsObserver is not created unless there is a |
| // throttle manager. |
| DCHECK(throttle_manager); |
| |
| // Violations can be triggered multiple times for the same page as |
| // violations after the first are ignored. Ad frame violations are |
| // attributed to the main frame url. |
| throttle_manager->OnAdsViolationTriggered( |
| GetDelegate().GetWebContents()->GetMainFrame(), |
| subresource_filter::mojom::AdsViolation:: |
| kHeavyAdsInterventionAtHostLimit); |
| } |
| |
| void AdsPageLoadMetricsObserver::MaybeTriggerHeavyAdIntervention( |
| content::RenderFrameHost* render_frame_host, |
| FrameTreeData* frame_data) { |
| DCHECK(render_frame_host); |
| HeavyAdAction action = frame_data->MaybeTriggerHeavyAdIntervention(); |
| if (action == HeavyAdAction::kNone) |
| return; |
| |
| // Don't trigger the heavy ad intervention on reloads. Gate this behind the |
| // privacy mitigations flag to help developers debug (otherwise they need to |
| // trigger new navigations to the site to test it). |
| if (heavy_ad_privacy_mitigations_enabled_) { |
| UMA_HISTOGRAM_BOOLEAN(kIgnoredByReloadHistogramName, page_load_is_reload_); |
| // Skip firing the intervention, but mark that an action occurred on the |
| // frame. |
| if (page_load_is_reload_) { |
| frame_data->set_heavy_ad_action(HeavyAdAction::kIgnored); |
| return; |
| } |
| } |
| |
| // Check to see if we are allowed to activate on this host. |
| if (IsBlocklisted(true)) { |
| frame_data->set_heavy_ad_action(HeavyAdAction::kIgnored); |
| return; |
| } |
| |
| // We should always unload the root of the ad subtree. Find the |
| // RenderFrameHost of the root ad frame associated with |frame_data|. |
| // |render_frame_host| may be the frame host for a subframe of the ad which we |
| // received a resource update for. Traversing the tree here guarantees |
| // that the frame we unload is an ancestor of |render_frame_host|. We cannot |
| // check if render frame hosts are ads so we rely on matching the |
| // root_frame_tree_node_id of |frame_data|. It is possible that this frame no |
| // longer exists. We do not care if the frame has moved to a new process |
| // because once the frame has been tagged as an ad, it is always considered an |
| // ad by our heuristics. |
| while (render_frame_host && render_frame_host->GetFrameTreeNodeId() != |
| frame_data->root_frame_tree_node_id()) { |
| render_frame_host = render_frame_host->GetParent(); |
| } |
| if (!render_frame_host) { |
| frame_data->set_heavy_ad_action(HeavyAdAction::kIgnored); |
| return; |
| } |
| |
| // Ensure that this RenderFrameHost is a subframe. |
| DCHECK(render_frame_host->GetParent()); |
| |
| frame_data->set_heavy_ad_action(action); |
| |
| // Add an inspector issue for the root of the ad subtree. |
| auto issue = blink::mojom::InspectorIssueInfo::New(); |
| issue->code = blink::mojom::InspectorIssueCode::kHeavyAdIssue; |
| issue->details = blink::mojom::InspectorIssueDetails::New(); |
| auto heavy_ad_details = blink::mojom::HeavyAdIssueDetails::New(); |
| heavy_ad_details->resolution = |
| action == HeavyAdAction::kUnload |
| ? blink::mojom::HeavyAdResolutionStatus::kHeavyAdBlocked |
| : blink::mojom::HeavyAdResolutionStatus::kHeavyAdWarning; |
| heavy_ad_details->reason = |
| GetHeavyAdReason(frame_data->heavy_ad_status_with_policy()); |
| heavy_ad_details->frame = blink::mojom::AffectedFrame::New(); |
| heavy_ad_details->frame->frame_id = |
| render_frame_host->GetDevToolsFrameToken().ToString(); |
| issue->details->heavy_ad_issue_details = std::move(heavy_ad_details); |
| render_frame_host->ReportInspectorIssue(std::move(issue)); |
| |
| // Report to all child frames that will be unloaded. Once all reports are |
| // queued, the frame will be unloaded. Because the IPC messages are ordered |
| // wrt to each frames unload, we do not need to wait before loading the |
| // error page. Reports will be added to ReportingObserver queues |
| // synchronously when the IPC message is handled, which guarantees they will |
| // be available in the the unload handler. |
| std::string report_message = |
| GetHeavyAdReportMessage(*frame_data, action == HeavyAdAction::kUnload); |
| render_frame_host->ForEachRenderFrameHost(base::BindRepeating( |
| [](const std::string& report_message, const content::Page* page, |
| content::RenderFrameHost* frame) { |
| // If `frame`'s page doesn't match the one we are associated with (for |
| // fenced frames or portals) skip the subtree. |
| if (page != &frame->GetPage()) |
| return content::RenderFrameHost::FrameIterationAction::kSkipChildren; |
| const char kReportId[] = "HeavyAdIntervention"; |
| if (frame->IsRenderFrameLive()) |
| frame->SendInterventionReport(kReportId, report_message); |
| return content::RenderFrameHost::FrameIterationAction::kContinue; |
| }, |
| report_message, &render_frame_host->GetPage())); |
| |
| // Report intervention to the blocklist. |
| if (auto* blocklist = GetHeavyAdBlocklist()) { |
| blocklist->AddEntry( |
| GetDelegate().GetWebContents()->GetLastCommittedURL().host(), |
| true /* opt_out */, |
| static_cast<int>( |
| heavy_ad_intervention::HeavyAdBlocklistType::kHeavyAdOnlyType)); |
| // Once we report, we need to check and see if we are now blocklisted. |
| // If we are, then we might trigger stricter interventions. |
| // TODO(ericrobinson): This does a couple fetches of the blocklist. It |
| // might be simpler to fetch it once at the start of this function and use |
| // it throughout. |
| if (IsBlocklisted(false)) { |
| MaybeTriggerStrictHeavyAdIntervention(); |
| } |
| } |
| |
| // Record this UMA regardless of if we actually unload or not, as sending |
| // reports is subject to the same noise and throttling as the intervention. |
| MetricsWebContentsObserver::RecordFeatureUsage( |
| render_frame_host, blink::mojom::WebFeature::kHeavyAdIntervention); |
| |
| ADS_HISTOGRAM("HeavyAds.InterventionType2", UMA_HISTOGRAM_ENUMERATION, |
| FrameVisibility::kAnyVisibility, |
| frame_data->heavy_ad_status_with_policy()); |
| ADS_HISTOGRAM("HeavyAds.InterventionType2", UMA_HISTOGRAM_ENUMERATION, |
| frame_data->visibility(), |
| frame_data->heavy_ad_status_with_policy()); |
| |
| if (action != HeavyAdAction::kUnload) |
| return; |
| |
| // Record heavy ad network size only when an ad is unloaded as a result of |
| // network usage. |
| if (frame_data->heavy_ad_status_with_noise() == HeavyAdStatus::kNetwork) { |
| ADS_HISTOGRAM("HeavyAds.NetworkBytesAtFrameUnload", PAGE_BYTES_HISTOGRAM, |
| kAnyVisibility, frame_data->resource_data().network_bytes()); |
| } |
| |
| GetDelegate().GetWebContents()->GetController().LoadPostCommitErrorPage( |
| render_frame_host, render_frame_host->GetLastCommittedURL(), |
| heavy_ad_intervention::PrepareHeavyAdPage( |
| application_locale_getter_.Run()), |
| net::ERR_BLOCKED_BY_CLIENT); |
| } |
| |
| bool AdsPageLoadMetricsObserver::IsBlocklisted(bool report) { |
| if (!heavy_ad_privacy_mitigations_enabled_) |
| return false; |
| |
| auto* blocklist = GetHeavyAdBlocklist(); |
| |
| // Treat instances where the blocklist is unavailable as blocklisted. |
| if (!blocklist) { |
| heavy_ads_blocklist_reason_ = |
| blocklist::BlocklistReason::kBlocklistNotLoaded; |
| return true; |
| } |
| |
| // If we haven't computed a blocklist reason previously or it was allowed |
| // previously, we need to compute/re-compute the value and store it. |
| if (!heavy_ads_blocklist_reason_.has_value() || |
| heavy_ads_blocklist_reason_ == blocklist::BlocklistReason::kAllowed) { |
| std::vector<blocklist::BlocklistReason> passed_reasons; |
| heavy_ads_blocklist_reason_ = blocklist->IsLoadedAndAllowed( |
| GetDelegate().GetWebContents()->GetLastCommittedURL().host(), |
| static_cast<int>( |
| heavy_ad_intervention::HeavyAdBlocklistType::kHeavyAdOnlyType), |
| false /* opt_out */, &passed_reasons); |
| } |
| |
| // Record whether this intervention hit the blocklist. |
| if (report) { |
| RecordHeavyAdInterventionDisallowedByBlocklist( |
| heavy_ads_blocklist_reason_ != blocklist::BlocklistReason::kAllowed); |
| } |
| |
| return heavy_ads_blocklist_reason_ != blocklist::BlocklistReason::kAllowed; |
| } |
| |
| heavy_ad_intervention::HeavyAdBlocklist* |
| AdsPageLoadMetricsObserver::GetHeavyAdBlocklist() { |
| if (heavy_ad_blocklist_) |
| return heavy_ad_blocklist_; |
| if (!heavy_ad_service_) |
| return nullptr; |
| |
| return heavy_ad_service_->heavy_ad_blocklist(); |
| } |
| |
| void AdsPageLoadMetricsObserver::UpdateAggregateMemoryUsage( |
| int64_t delta_bytes, |
| FrameVisibility frame_visibility) { |
| // For both the given |frame_visibility| and kAnyVisibility, update the |
| // current aggregate memory usage by adding the needed delta, and then |
| // if the current aggregate usage is greater than the recorded |
| // max aggregate usage, update the max aggregate usage. |
| for (const auto visibility : |
| {FrameVisibility::kAnyVisibility, frame_visibility}) { |
| aggregate_frame_data_->update_ad_memory_by_visibility(visibility, |
| delta_bytes); |
| } |
| } |
| |
| void AdsPageLoadMetricsObserver::CleanupDeletedFrame( |
| FrameTreeNodeId id, |
| FrameTreeData* frame_data, |
| bool update_density_tracker, |
| bool record_metrics) { |
| if (!frame_data) |
| return; |
| |
| if (record_metrics) |
| RecordPerFrameMetrics(*frame_data, GetDelegate().GetPageUkmSourceId()); |
| |
| if (update_density_tracker) |
| page_ad_density_tracker_.RemoveRect(id); |
| } |
| |
| } // namespace page_load_metrics |