blob: 97591e8787d323a8430097ba7aeaaa520d778585 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/actor/tab_observation_controller.h"
#include "base/barrier_closure.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/actor_metrics.h"
#include "chrome/browser/actor/actor_proto_conversion.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/tools/observation_delay_controller.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/common/actor/journal_details_builder.h"
#include "chrome/common/chrome_features.h"
#include "components/actor/core/actor_features.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/web_contents.h"
#if !BUILDFLAG(SKIP_ANDROID_UNMIGRATED_ACTOR_FILES)
#include "chrome/browser/ui/browser_window/public/profile_browser_collection.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#else
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#endif // !BUILDFLAG(SKIP_ANDROID_UNMIGRATED_ACTOR_FILES)
namespace actor {
namespace {
// TODO(bokan): These should probably be shared constants.
BASE_FEATURE(kGlicReloadAfterPerformActionsCrash,
base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kGlicRetryFailedObservations, base::FEATURE_ENABLED_BY_DEFAULT);
const base::FeatureParam<base::TimeDelta> kObservationRetryDelay{
&kGlicRetryFailedObservations, "delay", base::Seconds(5)};
tabs::TabInterface* GetCrashedTab(actor::ActorTask& task) {
for (tabs::TabHandle tab_handle : task.GetLastActedTabs()) {
tabs::TabInterface* tab = tab_handle.Get();
if (!tab) {
continue;
}
content::WebContents* contents = tab->GetContents();
if (contents && contents->IsCrashed()) {
return tab;
}
}
return nullptr;
}
optimization_guide::proto::TabObservation::TabObservationResult
ToTabObservationResult(page_content_annotations::FetchPageContextError error) {
using optimization_guide::proto::TabObservation;
switch (error) {
case page_content_annotations::FetchPageContextError::kUnknown:
return TabObservation::TAB_OBSERVATION_UNKNOWN_ERROR;
case page_content_annotations::FetchPageContextError::kWebContentsChanged:
return TabObservation::TAB_OBSERVATION_WEB_CONTENTS_CHANGED;
case page_content_annotations::FetchPageContextError::
kPageContextNotEligible:
return TabObservation::TAB_OBSERVATION_PAGE_CONTEXT_NOT_ELIGIBLE;
case page_content_annotations::FetchPageContextError::kWebContentsWentAway:
return TabObservation::TAB_OBSERVATION_TAB_WENT_AWAY;
}
}
bool HasScriptToolResults(
const std::vector<actor::ActionResultWithLatencyInfo>& action_results) {
for (const auto& res : action_results) {
if (res.result && res.result->script_tool_response &&
res.result->script_tool_response->result) {
return true;
}
}
return false;
}
} // namespace
ObservationResult::ObservationResult() = default;
ObservationResult::~ObservationResult() = default;
TabObservationController::TabObservationController(
Profile* profile,
TaskId task_id,
base::TimeTicks start_time,
bool skip_async_observation_information,
std::vector<actor::ActionResultWithLatencyInfo> action_results,
TabObservationStrategy observation_strategy,
DoneCallback done_callback)
: profile_(profile),
task_id_(task_id),
start_time_(start_time),
skip_async_observation_information_(skip_async_observation_information),
action_results_(std::move(action_results)),
observation_strategy_(std::move(observation_strategy)),
done_callback_(std::move(done_callback)) {
observation_strategy_.Lock();
}
TabObservationController::~TabObservationController() = default;
void TabObservationController::Start() {
CHECK(!result_);
CHECK(GetActorTask());
result_ = std::make_unique<ObservationResult>();
StartImpl();
}
void TabObservationController::StartImpl() {
ActorTask* task = GetActorTask();
if (!task) {
OnAllObservationsDone();
return;
}
if (base::FeatureList::IsEnabled(kGlicReloadAfterPerformActionsCrash) &&
!attempted_reload_after_crash_) {
if (tabs::TabInterface* crashed_tab = GetCrashedTab(*task)) {
attempted_reload_after_crash_ = true;
if (ReloadCrashedTab(*crashed_tab)) {
return;
}
}
}
PerformObservation();
}
void TabObservationController::PerformObservation() {
ActorTask* task = GetActorTask();
if (!task) {
OnAllObservationsDone();
return;
}
// Reset the result object if this is a retry.
result_ = std::make_unique<ObservationResult>();
#if !BUILDFLAG(SKIP_ANDROID_UNMIGRATED_ACTOR_FILES)
ProfileBrowserCollection::GetForProfile(profile_)->ForEach(
[this](BrowserWindowInterface* browser) {
optimization_guide::proto::WindowObservation window_observation;
window_observation.set_id(browser->GetSessionID().id());
window_observation.set_active(browser->IsActive());
if (tabs::TabInterface* tab = browser->GetActiveTabInterface()) {
window_observation.set_activated_tab_id(tab->GetHandle().raw_value());
}
for (const tabs::TabInterface* tab : *browser->GetTabStripModel()) {
window_observation.add_tab_ids(tab->GetHandle().raw_value());
}
result_->window_observations.push_back(std::move(window_observation));
return true;
});
#else
GlobalBrowserCollection* browser_collection =
GlobalBrowserCollection::GetInstance();
browser_collection->ForEach(
[this](BrowserWindowInterface* browser) {
if (browser->GetProfile() != profile_) {
return true;
}
optimization_guide::proto::WindowObservation window_observation;
window_observation.set_id(browser->GetSessionID().id());
window_observation.set_active(result_->window_observations.empty());
if (TabModel* tab_model =
static_cast<TabModel*>(TabListInterface::From(browser))) {
if (tabs::TabInterface* active_tab = tab_model->GetActiveTab()) {
window_observation.set_activated_tab_id(
active_tab->GetHandle().raw_value());
}
for (const tabs::TabInterface* tab : tab_model->GetAllTabs()) {
window_observation.add_tab_ids(tab->GetHandle().raw_value());
}
}
result_->window_observations.push_back(std::move(window_observation));
return true;
},
BrowserCollection::Order::kActivation);
#endif
std::vector<tabs::TabInterface*> tabs_to_fetch;
ActorTask::TabHandleSet last_acted_tabs = task->GetLastActedTabs();
for (const tabs::TabHandle& handle : last_acted_tabs) {
tabs::TabInterface* tab = handle.Get();
optimization_guide::proto::TabObservation tab_observation;
tab_observation.set_id(handle.raw_value());
if (!tab) {
tab_observation.set_result(optimization_guide::proto::TabObservation::
TAB_OBSERVATION_TAB_WENT_AWAY);
} else if (!tab->GetContents()
->GetPrimaryMainFrame()
->IsRenderFrameLive()) {
tab_observation.set_result(optimization_guide::proto::TabObservation::
TAB_OBSERVATION_PAGE_CRASHED);
} else {
bool take_screenshot = ShouldTakeScreenshot(handle);
bool extract_apc = ShouldExtractPageContent(handle);
if (skip_async_observation_information_ ||
(!take_screenshot && !extract_apc)) {
tab_observation.set_result(
optimization_guide::proto::TabObservation::TAB_OBSERVATION_OK);
if (!skip_async_observation_information_ &&
HasScriptToolResults(action_results_)) {
actor::CopyScriptToolResults(
*tab_observation.mutable_annotated_page_content()
->mutable_main_frame_data(),
action_results_);
}
} else {
tabs_to_fetch.push_back(tab);
}
}
result_->tab_observations.push_back(std::move(tab_observation));
}
// Add additional observations (e.g. from LoadAndExtractTool).
for (const auto& additional_obs : task->GetAdditionalTabObservations()) {
tabs::TabHandle handle(additional_obs.id());
if (last_acted_tabs.contains(handle)) {
continue;
}
result_->tab_observations.push_back(additional_obs);
}
actor::RecordPageContextTabCount(tabs_to_fetch.size());
if (tabs_to_fetch.empty()) {
OnAllObservationsDone();
return;
}
base::RepeatingClosure barrier = base::BarrierClosure(
tabs_to_fetch.size(),
base::BindOnce(&TabObservationController::OnAllObservationsDone,
weak_ptr_factory_.GetWeakPtr()));
for (tabs::TabInterface* tab : tabs_to_fetch) {
// Find the observation we just added.
optimization_guide::proto::TabObservation* observation = nullptr;
for (auto& obs : result_->tab_observations) {
if (obs.id() == tab->GetHandle().raw_value()) {
observation = &obs;
break;
}
}
CHECK(observation);
FetchObservation(tab, observation, barrier);
}
}
void TabObservationController::FetchObservation(
tabs::TabInterface* tab,
optimization_guide::proto::TabObservation* observation,
base::RepeatingClosure barrier) {
auto* actor_service = actor::ActorKeyedService::Get(profile_);
actor_service->RequestTabObservation(
*tab, task_id_, screenshot_collection_options_,
base::BindOnce(&TabObservationController::OnTabObservationFetched,
weak_ptr_factory_.GetWeakPtr(), tab->GetHandle(),
observation, base::TimeTicks::Now(), barrier));
}
void TabObservationController::OnTabObservationFetched(
tabs::TabHandle tab_handle,
optimization_guide::proto::TabObservation* observation,
base::TimeTicks fetch_start_time,
base::RepeatingClosure barrier,
ActorKeyedService::TabObservationResult result) {
base::ScopedClosureRunner run_barrier_at_return(barrier);
tabs::TabInterface* const tab = tab_handle.Get();
if (!tab || !tab->GetContents()) {
observation->set_result(optimization_guide::proto::TabObservation::
TAB_OBSERVATION_TAB_WENT_AWAY);
} else if (tab->GetContents()->IsCrashed()) {
observation->set_result(optimization_guide::proto::TabObservation::
TAB_OBSERVATION_PAGE_CRASHED);
} else if (!result.has_value()) {
observation->set_result(ToTabObservationResult(result.error().error_code));
} else {
bool take_screenshot = ShouldTakeScreenshot(tab_handle);
bool extract_apc = ShouldExtractPageContent(tab_handle);
page_content_annotations::FetchPageContextResult& fetch_result = **result;
bool has_apc = fetch_result.annotated_page_content_result.has_value();
observation->set_annotated_page_content_result(
has_apc || !extract_apc ? optimization_guide::proto::TabObservation::
ANNOTATED_PAGE_CONTENT_OK
: optimization_guide::proto::TabObservation::
ANNOTATED_PAGE_CONTENT_ERROR);
bool has_screenshot = fetch_result.screenshot_result.has_value();
bool screenshot_required =
!base::FeatureList::IsEnabled(actor::kGlicActorSkipScreenshot) &&
take_screenshot;
observation->set_screenshot_result(
has_screenshot || !screenshot_required
? optimization_guide::proto::TabObservation::SCREENSHOT_OK
: optimization_guide::proto::TabObservation::SCREENSHOT_ERROR);
if ((!has_apc && extract_apc) || (screenshot_required && !has_screenshot)) {
observation->set_result(optimization_guide::proto::TabObservation::
TAB_OBSERVATION_FETCH_ERROR);
} else {
observation->set_result(
optimization_guide::proto::TabObservation::TAB_OBSERVATION_OK);
}
// Populate latency steps.
if (has_apc) {
optimization_guide::proto::ActionsResult_LatencyInformation_LatencyStep
latency_step;
latency_step.mutable_annotated_page_content()->set_id(observation->id());
latency_step.set_latency_start_ms(
(fetch_start_time - start_time_).InMilliseconds());
latency_step.set_latency_stop_ms(
(fetch_result.annotated_page_content_result.value().end_time -
start_time_)
.InMilliseconds());
result_->latency_steps.push_back(std::move(latency_step));
actor::RecordPageContextApcDuration(
fetch_result.annotated_page_content_result.value().end_time -
fetch_start_time);
}
if (has_screenshot) {
optimization_guide::proto::ActionsResult_LatencyInformation_LatencyStep
latency_step;
latency_step.mutable_screenshot()->set_id(observation->id());
latency_step.set_latency_start_ms(
(fetch_start_time - start_time_).InMilliseconds());
latency_step.set_latency_stop_ms(
(fetch_result.screenshot_result.value().end_time - start_time_)
.InMilliseconds());
result_->latency_steps.push_back(std::move(latency_step));
RecordPageContextScreenshotDuration(
fetch_result.screenshot_result.value().end_time - fetch_start_time);
}
if (!GetTabObservationResultOverrideForTesting().is_null()) { // IN-TEST
GetTabObservationResultOverrideForTesting().Run( // IN-TEST
observation, **result);
}
if (has_apc) {
// Copy script tool results if any.
//
// TODO(b/489841640): Remove this once migration to
// ActionsResult.script_tool_results is done.
CopyScriptToolResults(*fetch_result.annotated_page_content_result->proto
.mutable_main_frame_data(),
action_results_);
}
FillInTabObservation(fetch_result, *observation);
if (!take_screenshot) {
observation->clear_screenshot();
observation->clear_screenshot_mime_type();
}
if (!extract_apc) {
observation->clear_annotated_page_content();
if (HasScriptToolResults(action_results_)) {
actor::CopyScriptToolResults(
*observation->mutable_annotated_page_content()
->mutable_main_frame_data(),
action_results_);
}
}
}
}
void TabObservationController::OnAllObservationsDone() {
if (base::FeatureList::IsEnabled(kGlicRetryFailedObservations) &&
!attempted_observation_retry_) {
bool has_failure = false;
for (const auto& obs : result_->tab_observations) {
if (obs.result() !=
optimization_guide::proto::TabObservation::TAB_OBSERVATION_OK) {
has_failure = true;
break;
}
}
if (has_failure) {
attempted_observation_retry_ = true;
ScheduleRetry();
return;
}
}
result_->attempted_observation_retry = attempted_observation_retry_;
std::move(done_callback_).Run(this, std::move(result_));
}
bool TabObservationController::ReloadCrashedTab(
tabs::TabInterface& crashed_tab) {
content::WebContents* contents = crashed_tab.GetContents();
if (!contents) {
return false;
}
auto* actor_service = actor::ActorKeyedService::Get(profile_);
reload_observer_ = std::make_unique<actor::ObservationDelayController>(
task_id_, actor_service->GetJournal());
contents->GetController().Reload(content::ReloadType::NORMAL, true);
reload_observer_->Wait(
crashed_tab,
base::BindOnce(&TabObservationController::OnReloadDone,
weak_ptr_factory_.GetWeakPtr(), crashed_tab.GetHandle()));
return true;
}
void TabObservationController::OnReloadDone(
tabs::TabHandle tab_handle,
ObservationDelayController::Result reload_result) {
if (reload_result ==
actor::ObservationDelayController::Result::kPageNavigated) {
tabs::TabInterface* tab = tab_handle.Get();
if (tab) {
size_t last_navigation_count = reload_observer_->NavigationCount();
auto* actor_service = actor::ActorKeyedService::Get(profile_);
reload_observer_ = std::make_unique<actor::ObservationDelayController>(
task_id_, actor_service->GetJournal());
reload_observer_->SetNavigationCount(last_navigation_count + 1);
reload_observer_->Wait(
*tab, base::BindOnce(&TabObservationController::OnReloadDone,
weak_ptr_factory_.GetWeakPtr(), tab_handle));
return;
}
}
reload_observer_.reset();
StartImpl();
}
void TabObservationController::ScheduleRetry() {
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&TabObservationController::PerformObservation,
weak_ptr_factory_.GetWeakPtr()),
kObservationRetryDelay.Get());
}
ActorTask* TabObservationController::GetActorTask() const {
auto* actor_service = actor::ActorKeyedService::Get(profile_);
return actor_service ? actor_service->GetTask(task_id_) : nullptr;
}
bool TabObservationController::ShouldTakeScreenshot(
tabs::TabHandle tab_handle) const {
switch (observation_strategy_.GetScreenshotPolicy(tab_handle)) {
case ScreenshotPolicy::kSkipped:
return false;
case ScreenshotPolicy::kRequested:
return base::FeatureList::IsEnabled(
actor::kActorObserveScreenshotDefault);
case ScreenshotPolicy::kRequired:
return true;
}
}
bool TabObservationController::ShouldExtractPageContent(
tabs::TabHandle tab_handle) const {
switch (observation_strategy_.GetPageContentExtractionPolicy(tab_handle)) {
case PageContentExtractionPolicy::kSkipped:
return false;
case PageContentExtractionPolicy::kRequested:
return base::FeatureList::IsEnabled(
actor::kActorObservePageContentDefault);
case PageContentExtractionPolicy::kRequired:
return true;
}
}
} // namespace actor