| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/lcp_critical_path_predictor/lcp_critical_path_predictor.h" |
| |
| #include "base/metrics/histogram_functions.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/loader/lcp_critical_path_predictor_util.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/html/html_image_element.h" |
| #include "third_party/blink/renderer/core/lcp_critical_path_predictor/element_locator.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_origin.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| size_t GetLCPPFontURLPredictorMaxUrlLength() { |
| return features::kLCPPFontURLPredictorMaxUrlLength.Get(); |
| } |
| |
| bool IsTimingPredictorEnabled() { |
| if (base::FeatureList::IsEnabled( |
| blink::features::kLCPTimingPredictorPrerender2)) { |
| return true; |
| } |
| if (base::FeatureList::IsEnabled(blink::features::kLCPPDeferUnusedPreload)) { |
| switch (features::kLcppDeferUnusedPreloadTiming.Get()) { |
| case features::LcppDeferUnusedPreloadTiming::kPostTask: |
| return false; |
| case features::LcppDeferUnusedPreloadTiming::kLcpTimingPredictor: |
| case features::LcppDeferUnusedPreloadTiming:: |
| kLcpTimingPredictorWithPostTask: |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| LCPCriticalPathPredictor::LCPCriticalPathPredictor(LocalFrame& frame) |
| : frame_(&frame), |
| host_(frame.DomWindow()), |
| task_runner_(frame.GetTaskRunner(TaskType::kInternalLoading)) { |
| CHECK(LcppEnabled()); |
| } |
| |
| LCPCriticalPathPredictor::~LCPCriticalPathPredictor() = default; |
| |
| bool LCPCriticalPathPredictor::HasAnyHintData() const { |
| return !lcp_element_locators_.empty() || !lcp_influencer_scripts_.empty() || |
| !preconnected_origins_.empty(); |
| } |
| |
| void LCPCriticalPathPredictor::set_lcp_element_locators( |
| const std::vector<std::string>& lcp_element_locator_strings, |
| const std::vector<std::string>& lcp_element_locator_all_strings) { |
| // Clear current set of locators before receiving replacements. |
| lcp_element_locators_.clear(); |
| lcp_element_locator_strings_.clear(); |
| const wtf_size_t reserved_size = |
| base::checked_cast<wtf_size_t>(lcp_element_locator_strings.size()); |
| lcp_element_locators_.reserve(reserved_size); |
| lcp_element_locator_strings_.reserve(reserved_size); |
| for (const std::string& serialized_locator : lcp_element_locator_strings) { |
| lcp_element_locators_.push_back(ElementLocator()); |
| bool result = |
| lcp_element_locators_.back().ParseFromString(serialized_locator); |
| if (!result) { |
| // This can happen when the host LCPP database is corrupted or we |
| // updated the ElementLocator schema in an incompatible way. |
| LOG(INFO) << "Ignoring an invalid lcp_element_locator hint."; |
| lcp_element_locators_.pop_back(); |
| } else { |
| lcp_element_locator_strings_.push_back(std::move(serialized_locator)); |
| } |
| } |
| CHECK_EQ(lcp_element_locators_.size(), lcp_element_locator_strings_.size()); |
| |
| lcp_element_locator_all_strings_.clear(); |
| lcp_element_locator_all_strings_.reserve( |
| base::checked_cast<wtf_size_t>(lcp_element_locator_all_strings.size())); |
| for (const std::string& serialized_locator : |
| lcp_element_locator_all_strings) { |
| bool result = ElementLocator().ParseFromString(serialized_locator); |
| if (!result) { |
| // This can happen when the host LCPP database is corrupted or we |
| // updated the ElementLocator schema in an incompatible way. |
| LOG(INFO) << "Ignoring an invalid lcp_element_locator hint."; |
| } else { |
| lcp_element_locator_all_strings_.push_back(std::move(serialized_locator)); |
| } |
| } |
| } |
| |
| void LCPCriticalPathPredictor::set_lcp_influencer_scripts( |
| HashSet<KURL> scripts) { |
| lcp_influencer_scripts_ = std::move(scripts); |
| } |
| |
| void LCPCriticalPathPredictor::set_fetched_fonts(Vector<KURL> fonts) { |
| fetched_fonts_ = std::move(fonts); |
| } |
| |
| void LCPCriticalPathPredictor::set_preconnected_origins( |
| const Vector<url::Origin>& origins) { |
| preconnected_origins_ = std::move(origins); |
| } |
| |
| void LCPCriticalPathPredictor::set_unused_preloads(Vector<KURL> preloads) { |
| unused_preloads_ = std::move(preloads); |
| } |
| |
| void LCPCriticalPathPredictor::enable_testing() { |
| report_timing_predictor_for_testing_ = true; |
| } |
| |
| void LCPCriticalPathPredictor::Reset() { |
| lcp_element_locators_.clear(); |
| lcp_element_locator_strings_.clear(); |
| lcp_influencer_scripts_.clear(); |
| fetched_fonts_.clear(); |
| preconnected_origins_.clear(); |
| unused_preloads_.clear(); |
| |
| lcp_predicted_callbacks_.clear(); |
| are_predicted_callbacks_called_ = false; |
| has_lcp_occurred_ = false; |
| is_outermost_main_frame_document_loaded_ = false; |
| has_sent_unused_preloads_ = false; |
| |
| report_timing_predictor_for_testing_ = false; |
| } |
| |
| void LCPCriticalPathPredictor::AddLCPPredictedCallback(LCPCallback callback) { |
| CHECK(IsTimingPredictorEnabled()); |
| if (are_predicted_callbacks_called_) { |
| std::move(callback).Run(/*lcp_element=*/nullptr); |
| return; |
| } |
| lcp_predicted_callbacks_.push_back(std::move(callback)); |
| } |
| |
| void LCPCriticalPathPredictor::AddLCPPredictedCallback( |
| base::OnceClosure callback) { |
| LCPCallback lcp_callback = |
| blink::BindOnce([](base::OnceClosure callback, |
| const Element*) { std::move(callback).Run(); }, |
| std::move(callback)); |
| AddLCPPredictedCallback(std::move(lcp_callback)); |
| } |
| |
| void LCPCriticalPathPredictor::MayRunPredictedCallbacks( |
| const Element* lcp_element) { |
| if (are_predicted_callbacks_called_) { |
| return; |
| } |
| are_predicted_callbacks_called_ = true; |
| // TODO(crbug.com/1493255): Trigger callbacks for the entire frame tree. |
| Vector<LCPCallback> callbacks; |
| callbacks.swap(lcp_predicted_callbacks_); |
| for (auto& callback : callbacks) { |
| std::move(callback).Run(lcp_element); |
| } |
| |
| if (report_timing_predictor_for_testing_) { |
| const std::optional<std::string> lcp_element_locator_string = |
| lcp_element |
| ? std::optional<std::string>( |
| element_locator::OfElement(*lcp_element).SerializeAsString()) |
| : std::nullopt; |
| GetHost().OnLcpTimingPredictedForTesting(lcp_element_locator_string); |
| } |
| } |
| |
| bool LCPCriticalPathPredictor::IsElementMatchingLocator( |
| const Element& element) { |
| std::string lcp_element_locator_string = |
| element_locator::OfElement(element).SerializeAsString(); |
| return lcp_element_locator_strings_.Contains(lcp_element_locator_string); |
| } |
| |
| void LCPCriticalPathPredictor::OnLargestContentfulPaintUpdated( |
| const Element& lcp_element, |
| std::optional<const KURL> maybe_image_url) { |
| if (base::FeatureList::IsEnabled(features::kLCPCriticalPathPredictor) || |
| base::FeatureList::IsEnabled(features::kLCPPLazyLoadImagePreload) || |
| IsTimingPredictorEnabled()) { |
| const std::string lcp_element_locator_string = |
| element_locator::OfElement(lcp_element).SerializeAsString(); |
| |
| has_lcp_occurred_ = true; |
| // Regard `lcp_element` is the candidate if its locator is found in |
| // set_lcp_element_locators(lcp_element_locator_strings). |
| // See PredictLcpElementLocators() for the contents detail. |
| const wtf_size_t predicted_lcp_index = |
| lcp_element_locator_all_strings_.Find(lcp_element_locator_string); |
| if (predicted_lcp_index != kNotFound) { |
| MayRunPredictedCallbacks(&lcp_element); |
| } |
| if (is_outermost_main_frame_document_loaded_) { |
| // Call callbacks as fallback regardless of prediction because |
| // This LCP is much too late. |
| MayRunPredictedCallbacks(nullptr); |
| } |
| |
| base::UmaHistogramCounts10000( |
| "Blink.LCPP.LCPElementLocatorSize", |
| base::checked_cast<int>(lcp_element_locator_string.size())); |
| const bool is_recordable = |
| (lcp_element_locator_string.size() <= |
| features::kLCPCriticalPathPredictorMaxElementLocatorLength.Get()); |
| GetHost().OnLcpUpdated(mojom::blink::LcpElement::New( |
| is_recordable ? std::optional<std::string>(lcp_element_locator_string) |
| : std::nullopt, |
| IsA<HTMLImageElement>(lcp_element), |
| predicted_lcp_index == kNotFound |
| ? std::nullopt |
| : std::optional<wtf_size_t>(predicted_lcp_index))); |
| } |
| |
| if (base::FeatureList::IsEnabled(features::kLCPPAutoPreconnectLcpOrigin)) { |
| auto root_origin = |
| url::Origin::Create((GURL)lcp_element.GetDocument().Url()); |
| if (maybe_image_url.has_value()) { |
| const KURL& lcp_image_url = *maybe_image_url; |
| if (!lcp_image_url.IsEmpty() && lcp_image_url.IsValid() && |
| lcp_image_url.ProtocolIsInHttpFamily()) { |
| auto lcp_origin = url::Origin::Create((GURL)lcp_image_url); |
| bool is_lcp_cross_origin = !lcp_origin.IsSameOriginWith(root_origin); |
| base::UmaHistogramBoolean("Blink.LCPP.CrossOriginLcpImage", |
| is_lcp_cross_origin); |
| if (is_lcp_cross_origin) { |
| GetHost().AddPreconnectOrigin(SecurityOrigin::Create(lcp_image_url)); |
| } |
| |
| // Calculate accuracy against predicted. |
| int count_prediction_matches = 0; |
| for (const auto& predicted_origin : preconnected_origins_) { |
| if (lcp_origin.IsSameOriginWith(predicted_origin)) { |
| count_prediction_matches++; |
| } |
| } |
| |
| base::UmaHistogramCounts1000( |
| "Blink.LCPP.PreconnectPredictionMatchCount", |
| base::checked_cast<int>(preconnected_origins_.size())); |
| if (!preconnected_origins_.empty()) { |
| base::UmaHistogramCounts100( |
| "Blink.LCPP.PreconnectPredictionMatchPercent", |
| base::checked_cast<int>((double)count_prediction_matches / |
| preconnected_origins_.size() * 100)); |
| } |
| } |
| } |
| } |
| |
| if (blink::LcppScriptObserverEnabled()) { |
| if (const HTMLImageElement* image_element = |
| DynamicTo<HTMLImageElement>(lcp_element)) { |
| auto& creators = image_element->creator_scripts(); |
| size_t max_allowed_url_length = |
| features::kLCPScriptObserverMaxUrlLength.Get(); |
| size_t max_allowed_url_count = |
| features::kLCPScriptObserverMaxUrlCountPerOrigin.Get(); |
| size_t max_url_length_encountered = 0; |
| size_t prediction_match_count = 0; |
| |
| Vector<KURL> filtered_script_urls; |
| |
| for (auto& url : creators) { |
| max_url_length_encountered = |
| std::max<size_t>(max_url_length_encountered, url.length()); |
| if (url.length() >= max_allowed_url_length) { |
| continue; |
| } |
| KURL parsed_url(url); |
| if (parsed_url.IsEmpty() || !parsed_url.IsValid() || |
| !parsed_url.ProtocolIsInHttpFamily()) { |
| continue; |
| } |
| filtered_script_urls.push_back(parsed_url); |
| if (lcp_influencer_scripts_.Contains(parsed_url)) { |
| prediction_match_count++; |
| } |
| if (filtered_script_urls.size() >= max_allowed_url_count) { |
| break; |
| } |
| } |
| GetHost().SetLcpInfluencerScriptUrls(filtered_script_urls); |
| |
| base::UmaHistogramCounts10000( |
| "Blink.LCPP.LCPInfluencerUrlsCount", |
| base::checked_cast<int>(filtered_script_urls.size())); |
| base::UmaHistogramCounts10000( |
| "Blink.LCPP.LCPInfluencerUrlsMaxLength", |
| base::checked_cast<int>(max_url_length_encountered)); |
| base::UmaHistogramCounts10000( |
| "Blink.LCPP.LCPInfluencerUrlsPredictionMatchCount", |
| base::checked_cast<int>(prediction_match_count)); |
| if (!lcp_influencer_scripts_.empty()) { |
| base::UmaHistogramCounts10000( |
| "Blink.LCPP.LCPInfluencerUrlsPredictionMatchPercent", |
| base::checked_cast<int>((double)prediction_match_count / |
| lcp_influencer_scripts_.size() * 100)); |
| } |
| } |
| } |
| } |
| |
| void LCPCriticalPathPredictor::OnFontFetched(const KURL& url) { |
| if (!base::FeatureList::IsEnabled(blink::features::kLCPPFontURLPredictor)) { |
| return; |
| } |
| if (!url.ProtocolIsInHttpFamily()) { |
| return; |
| } |
| if (url.GetString().length() > GetLCPPFontURLPredictorMaxUrlLength()) { |
| return; |
| } |
| GetHost().NotifyFetchedFont(url, fetched_fonts_.Contains(url)); |
| } |
| |
| void LCPCriticalPathPredictor::OnStartPreload( |
| const KURL& url, |
| const ResourceType& resource_type) { |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kHttpDiskCachePrewarming) && |
| !base::FeatureList::IsEnabled( |
| blink::features::kLCPPPrefetchSubresource)) { |
| return; |
| } |
| if (!frame_->IsOutermostMainFrame()) { |
| return; |
| } |
| if (!url.ProtocolIsInHttpFamily()) { |
| return; |
| } |
| if (url.GetString().length() > |
| features::kHttpDiskCachePrewarmingMaxUrlLength.Get()) { |
| return; |
| } |
| Document* document = frame_->GetDocument(); |
| if (!document || !document->Loader()) { |
| return; |
| } |
| base::TimeDelta resource_load_start = |
| base::TimeTicks::Now() - |
| document->Loader()->GetTiming().NavigationStart(); |
| CHECK_GE(resource_load_start, base::Seconds(0)); |
| GetHost().NotifyFetchedSubresource( |
| url, resource_load_start, |
| ResourceFetcher::DetermineRequestDestination(resource_type)); |
| } |
| |
| mojom::blink::LCPCriticalPathPredictorHost& |
| LCPCriticalPathPredictor::GetHost() { |
| if (!host_.is_bound() || !host_.is_connected()) { |
| host_.reset(); |
| GetFrame().GetBrowserInterfaceBroker().GetInterface( |
| host_.BindNewPipeAndPassReceiver(task_runner_)); |
| } |
| return *host_.get(); |
| } |
| |
| bool LCPCriticalPathPredictor::IsLcpInfluencerScript(const KURL& url) { |
| return lcp_influencer_scripts_.Contains(url); |
| } |
| |
| void LCPCriticalPathPredictor::OnOutermostMainFrameDocumentLoad() { |
| if (!IsTimingPredictorEnabled()) { |
| return; |
| } |
| is_outermost_main_frame_document_loaded_ = true; |
| // Call callbacks as fallback because we can not detect |
| // which is lcp in the lcps before onload. |
| if (has_lcp_occurred_ || lcp_element_locator_all_strings_.empty()) { |
| MayRunPredictedCallbacks(nullptr); |
| } |
| } |
| |
| void LCPCriticalPathPredictor::OnWarnedUnusedPreloads( |
| const Vector<KURL>& unused_preloads) { |
| // This should be sent in the outermost main frame. It's fine without checking |
| // |frame_->IsOutermostMainFrame()| here because the caller side |
| // LocalFrame::GetLCPP() has the outermost main frame check. |
| if (!base::FeatureList::IsEnabled(features::kLCPPDeferUnusedPreload) || |
| has_sent_unused_preloads_) { |
| return; |
| } |
| // Limit the list of preload requests to be sent once. This function can be |
| // called after the load event, but we only take care of unused preloads |
| // dispatched before LCP. |
| has_sent_unused_preloads_ = true; |
| GetHost().SetUnusedPreloads(unused_preloads); |
| } |
| |
| void LCPCriticalPathPredictor::Trace(Visitor* visitor) const { |
| visitor->Trace(frame_); |
| visitor->Trace(host_); |
| } |
| |
| } // namespace blink |