blob: 86eab26ad681d6a02160c06beb766ea57c8001bd [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_PAGE_CONTENT_ANNOTATIONS_CONTENT_ANNOTATE_PAGE_CONTENT_REQUEST_H_
#define COMPONENTS_PAGE_CONTENT_ANNOTATIONS_CONTENT_ANNOTATE_PAGE_CONTENT_REQUEST_H_
#include <stdint.h>
#include <optional>
#include <vector>
#include "base/containers/flat_map.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/expected.h"
#include "components/content_extraction/content/browser/inner_text.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/page_content_annotations/content/browser/page_settled_monitor.h"
#include "components/page_content_annotations/content/page_content_extraction_service.h"
#include "components/page_content_annotations/content/page_context_fetcher.h"
#include "components/page_content_annotations/core/page_content_extraction_types.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/browser/web_contents_user_data.h"
#include "pdf/buildflags.h"
#include "third_party/blink/public/mojom/content_extraction/ai_page_content.mojom.h"
namespace base {
class ScopedClosureRunner;
}
namespace optimization_guide {
class PageContextEligibility;
namespace proto {
class AnnotatedPageContent;
} // namespace proto
} // namespace optimization_guide
namespace page_content_annotations {
using GetExtractedPageContentAndEligibilityCallback =
PageContentExtractionService::GetExtractedPageContentAndEligibilityCallback;
using GetServerUploadEligibilityCallback =
PageContentExtractionService::GetServerUploadEligibilityCallback;
using FetchPageContextCallback =
base::RepeatingCallback<void(content::WebContents&,
const FetchPageContextOptions&,
std::unique_ptr<FetchPageProgressListener>,
FetchPageContextResultCallback)>;
using GetTabIdCallback =
base::RepeatingCallback<std::optional<int64_t>(content::WebContents*)>;
// Class for deciding when a page is ready for getting page content, and
// extracts page content.
// TODO(b/490161242): Rename this class to reflect that it's the observer.
// TODO(b/487632737): Rename this class to reflect that it requests either:
// - `AnnotatedPageContent` for non-PDF pages.
// - Text for PDF pages.
class AnnotatedPageContentRequest
: public content::WebContentsObserver,
public content::WebContentsUserData<AnnotatedPageContentRequest> {
public:
// LINT.IfChange(TriggerSource)
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class TriggerSource {
kOnLoad = 0,
kOnHidden = 1,
kOnDemand = 2,
kMaxValue = kOnDemand,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/optimization/enums.xml:OptimizationGuidePageContentExtractionTriggerSource)
static std::unique_ptr<AnnotatedPageContentRequest> Create(
content::WebContents* web_contents,
PageContentExtractionService& page_content_extraction_service,
FetchPageContextCallback fetch_page_context_callback,
GetTabIdCallback get_tab_id_callback);
AnnotatedPageContentRequest(
content::WebContents* web_contents,
PageContentExtractionService& page_content_extraction_service,
blink::mojom::AIPageContentOptionsPtr request,
FetchPageContextCallback fetch_page_context_callback,
GetTabIdCallback get_tab_id_callback);
AnnotatedPageContentRequest(const AnnotatedPageContentRequest&) = delete;
AnnotatedPageContentRequest& operator=(const AnnotatedPageContentRequest&) =
delete;
~AnnotatedPageContentRequest() override;
// Creates an instance for testing that is not attached as UserData.
static std::unique_ptr<AnnotatedPageContentRequest> CreateForTesting(
content::WebContents* web_contents,
PageContentExtractionService& page_content_extraction_service,
FetchPageContextCallback fetch_page_context_callback,
GetTabIdCallback get_tab_id_callback);
// Returns the cached APC for `page` and whether it is eligible for
// server upload. Will return nullopt if not available or not supported (e.g.
// for PDFs, the text extraction result is never cached).
std::optional<ExtractedPageContentResult> GetCachedContentAndEligibility(
bool log_metrics = true);
// Returns whether the cached APC for `page` is eligible for server upload.
// Will return nullopt if not available.
std::optional<bool> GetServerUploadEligibility();
// Asynchronous versions of the getter methods above.
// These methods will resolve synchronously if the extraction is already
// complete, or wait for the initial extraction to finish if there is one
// pending. If the extraction request is cleared or reset (e.g. from a
// navigation or destruction), or if the initial extraction wasn't triggered,
// the callbacks will resolve with std::nullopt.
void GetCachedContentAndEligibilityAsync(
GetExtractedPageContentAndEligibilityCallback callback);
void GetServerUploadEligibilityAsync(
GetServerUploadEligibilityCallback callback);
// Equivalent to GetCachedContentAndEligibilityAsync, except that if the
// initial extraction has not yet been triggered, it will be.
void GetContentAndEligibilityAsync(
GetExtractedPageContentAndEligibilityCallback callback);
// Extracts a new APC for `page` and computes its eligibility for server
// upload, and caches the new result. It will wait for the initial
// extraction to complete if there is one pending. If the extraction request
// is cleared or reset (e.g. from a navigation or destruction), the callbacks
// will resolve with std::nullopt. On-demand extraction is not supported for
// PDFs and will also result in std::nullopt.
void RefreshExtractedPageContentAndEligibilityForPage(
GetExtractedPageContentAndEligibilityCallback callback);
private:
friend class content::WebContentsUserData<AnnotatedPageContentRequest>;
friend class AnnotatePageContentRequestTest;
AnnotatedPageContentRequest(
content::WebContents* web_contents,
PageContentExtractionService& page_content_extraction_service,
FetchPageContextCallback fetch_page_context_callback,
GetTabIdCallback get_tab_id_callback);
// content::WebContentsObserver:
void PrimaryPageChanged(content::Page& page) override;
void DidStartNavigation(
content::NavigationHandle* navigation_handle) override;
void DidFinishNavigation(
content::NavigationHandle* navigation_handle) override;
void DidStopLoading() override;
void OnFirstContentfulPaintInPrimaryMainFrame(
base::TimeTicks presentation_time) override;
void OnVisibilityChanged(content::Visibility visibility) override;
void DidFinishNavigationWithPageSettledMonitor(
content::NavigationHandle* navigation_handle);
bool IsPdf() const;
// Returns true if page content extraction is allowed, either because
// automatic extraction is enabled, or because we have an allowed on-demand
// request that bypasses observer requirements.
bool ShouldAllowPageContentExtraction() const;
// Returns true if the async getter should wait for the extraction to
// complete, or false if it should return immediately (with cached content or
// nullopt).
bool ShouldAsyncWaitForExtraction() const;
void ResetForNewNavigation(bool is_same_document);
// `on_hide` should be true iff this extraction is being triggered
// specifically by the tab transitioning to hidden (as opposed to, say,
// completing a page load).
void MaybeScheduleExtraction(bool on_hide = false);
// Returns the trigger source for an extraction, or nullopt if an extraction
// should not be scheduled.
[[nodiscard]] std::optional<TriggerSource> ShouldScheduleExtraction(
bool on_hide) const;
void OnExtractionTimerFired(TriggerSource trigger_source);
void StartExtraction(TriggerSource trigger_source);
void RequestAnnotatedPageContentSync(TriggerSource trigger_source);
void OnPageContextFetched(TriggerSource trigger_source,
FetchPageContextResultCallbackArg result);
void OnInnerTextReceived(
base::TimeTicks start_time,
std::unique_ptr<content_extraction::InnerTextResult> result);
void RecordAnnotatedPageContentMetrics(
const optimization_guide::proto::AnnotatedPageContent& proto);
void ResolveAllCallbacksWith(
const std::optional<ExtractedPageContentResult>& result);
// Records metrics for the time from navigation to the page being ready for
// extraction.
void MaybeRecordReadyToExtractMetrics();
bool IsPageReadyForExtraction() const;
#if BUILDFLAG(ENABLE_PDF)
void RequestPdf(TriggerSource trigger_source);
void RequestPdfPageCount(base::ScopedClosureRunner metrics_recorder);
void RequestPdfText(TriggerSource trigger_source,
base::ScopedClosureRunner metrics_recorder);
#endif // BUILDFLAG(ENABLE_PDF)
void OnPageContextEligibilityAPILoaded(
optimization_guide::PageContextEligibility* page_context_eligibility);
// Called when the page has settled, as determined by
// `active_page_settled_monitor_`.
void OnPageSettled();
void SetPageSettledCallbackForTesting(base::OnceClosure callback);
raw_ref<PageContentExtractionService> page_content_extraction_service_;
raw_ptr<optimization_guide::PageContextEligibility> page_context_eligibility_;
const blink::mojom::AIPageContentOptionsPtr options_;
const bool include_inner_text_;
const bool is_pdf_text_extraction_enabled_;
// Whether the `PageSettledMonitor` should be used to determine when to
// trigger the extraction. If false, the legacy mechanism (waiting for
// load/FCP plus a fixed delay) will be used.
const bool use_page_settled_monitor_;
// LINT.IfChange(Lifecycle)
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Tracks the state of the current extraction.
enum class Lifecycle {
// The state before any navigation has occurred. Extraction is not allowed
// until the first navigation has occurred.
kInitial = 0,
// Indicates that a new navigation occurred and we may need to schedule an
// extraction.
kNavigated = 1,
// An extraction has been scheduled and we are waiting for the delay timer
// to fire.
kScheduled = 2,
// An extraction is in progress (e.g. after the delay timer fired) and we
// are waiting for a response from the renderer.
kRunning = 3,
// An extraction has occurred and no others are currently scheduled.
kExtracted = 4,
kMaxValue = kExtracted,
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/optimization/enums.xml:OptimizationGuideOnDemandExtractionState2)
Lifecycle lifecycle_ = Lifecycle::kInitial;
// We don't record latency metrics for same-document navigations as we don't
// get reliable load signals. So, these are only set for cross-document
// navigations.
std::optional<base::ElapsedTimer> stop_loading_timer_;
std::optional<base::ElapsedTimer> extraction_timer_;
bool waiting_for_load_ = false;
bool waiting_for_fcp_ = false;
bool waiting_for_page_settled_ = false;
bool is_hidden_ = false;
bool is_same_document_navigation_ = false;
// The timer that starts when the current navigation committed. Reset to
// `std::nullopt` once the "ready to extract" metric has been recorded for the
// current navigation.
std::optional<base::ElapsedTimer> navigation_commit_timer_;
// `AnnotatedPageContentRequest` supports two different content requests:
// - `AnnotatedPageContent` for non-PDF pages.
// - Text for PDF pages.
//
// This cache only stores `AnnotatedPageContent`. PDF text is not cached here,
// and as a result, on-demand extraction and other methods that rely on this
// cache are not supported for PDFs.
// TODO(b/503685696): Support on-demand extraction for PDF, which may require
// adding PDF text extraction result to the cache.
std::optional<ExtractedPageContentResult> cached_content_;
std::vector<GetExtractedPageContentAndEligibilityCallback>
on_demand_callbacks_;
std::vector<GetExtractedPageContentAndEligibilityCallback>
pending_content_callbacks_;
std::vector<GetServerUploadEligibilityCallback>
pending_eligibility_callbacks_;
// Monitors created for pending navigations. Keyed by the NavigationHandle's
// unique ID to handle interleaved navigations.
base::flat_map<int64_t, std::unique_ptr<PageSettledMonitor>>
pending_page_settled_monitors_;
// The active monitor used to determine when the page has settled upon a
// committed navigation. Only non-null if `use_page_settled_monitor_` is true.
std::unique_ptr<PageSettledMonitor> active_page_settled_monitor_;
FetchPageContextCallback fetch_page_context_callback_;
GetTabIdCallback get_tab_id_callback_;
base::OnceClosure page_settled_callback_for_testing_;
base::WeakPtrFactory<AnnotatedPageContentRequest> weak_factory_{this};
WEB_CONTENTS_USER_DATA_KEY_DECL();
};
} // namespace page_content_annotations
#endif // COMPONENTS_PAGE_CONTENT_ANNOTATIONS_CONTENT_ANNOTATE_PAGE_CONTENT_REQUEST_H_