blob: ba9c8d16c2428b1fb6947c0cbdde381ce3fee631 [file] [log] [blame]
// 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.
#include "chrome/browser/actor/browser_action_util.h"
#include <algorithm>
#include <cstdint>
#include <memory>
#include <optional>
#include "base/barrier_closure.h"
#include "base/base64.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "base/numerics/safe_conversions.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/aggregated_journal.h"
#include "chrome/browser/actor/shared_types.h"
#include "chrome/browser/actor/tools/attempt_login_tool_request.h"
#include "chrome/browser/actor/tools/click_tool_request.h"
#include "chrome/browser/actor/tools/drag_and_release_tool_request.h"
#include "chrome/browser/actor/tools/history_tool_request.h"
#include "chrome/browser/actor/tools/move_mouse_tool_request.h"
#include "chrome/browser/actor/tools/navigate_tool_request.h"
#include "chrome/browser/actor/tools/script_tool_request.h"
#include "chrome/browser/actor/tools/scroll_to_tool_request.h"
#include "chrome/browser/actor/tools/scroll_tool_request.h"
#include "chrome/browser/actor/tools/select_tool_request.h"
#include "chrome/browser/actor/tools/tab_management_tool_request.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/tools/type_tool_request.h"
#include "chrome/browser/actor/tools/wait_tool_request.h"
#include "chrome/browser/actor/tools/window_management_tool_request.h"
#include "chrome/browser/page_content_annotations/multi_source_page_context_fetcher.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/actor/actor_constants.h"
#include "chrome/common/actor/actor_logging.h"
#include "chrome/common/actor/journal_details_builder.h"
#include "components/optimization_guide/content/browser/page_content_proto_provider.h"
#include "components/optimization_guide/proto/features/actions_data.pb.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/window_open_disposition.h"
namespace actor {
// Alias the namespace to make the long enums a bit more readable in
// implementations.
namespace apc = ::optimization_guide::proto;
using apc::Action;
using apc::ActionTarget;
using apc::ActivateTabAction;
using apc::ActivateWindowAction;
using apc::AttemptLoginAction;
using apc::ClickAction;
using apc::CloseTabAction;
using apc::CloseWindowAction;
using apc::CreateTabAction;
using apc::CreateWindowAction;
using apc::DragAndReleaseAction;
using apc::HistoryBackAction;
using apc::HistoryForwardAction;
using apc::MoveMouseAction;
using apc::NavigateAction;
using apc::ScriptToolAction;
using apc::ScrollAction;
using apc::ScrollToAction;
using apc::SelectAction;
using apc::TypeAction;
using apc::WaitAction;
using ::optimization_guide::DocumentIdentifierUserData;
using ::page_content_annotations::FetchPageContextOptions;
using ::page_content_annotations::FetchPageContextResult;
using ::page_content_annotations::FetchPageContextResultCallbackArg;
using ::tabs::TabHandle;
using ::tabs::TabInterface;
namespace {
struct PageScopedParams {
std::string document_identifier;
TabHandle tab_handle;
};
template <class T>
TabHandle GetTabHandle(const T& action) {
if (!action.has_tab_id()) {
return TabHandle::Null();
}
return TabHandle(action.tab_id());
}
std::optional<PageTarget> ToPageTarget(
const optimization_guide::proto::ActionTarget& target) {
// A valid target must have either a coordinate or a
// document_identifier-dom_node_id pair.
if (target.has_coordinate()) {
return PageTarget(
gfx::Point(target.coordinate().x(), target.coordinate().y()));
} else {
if (!target.has_content_node_id() || !target.has_document_identifier()) {
return std::nullopt;
}
return PageTarget(
DomNode{.node_id = target.content_node_id(),
.document_identifier =
target.document_identifier().serialized_token()});
}
}
std::unique_ptr<ToolRequest> CreateClickRequest(const ClickAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_target() || !action.has_click_count() ||
!action.has_click_type() || tab_handle == TabHandle::Null()) {
return nullptr;
}
MouseClickCount count;
switch (action.click_count()) {
case apc::ClickAction_ClickCount_SINGLE:
count = MouseClickCount::kSingle;
break;
case apc::ClickAction_ClickCount_DOUBLE:
count = MouseClickCount::kDouble;
break;
case apc::ClickAction_ClickCount_UNKNOWN_CLICK_COUNT:
case apc::
ClickAction_ClickCount_ClickAction_ClickCount_INT_MIN_SENTINEL_DO_NOT_USE_:
case apc::
ClickAction_ClickCount_ClickAction_ClickCount_INT_MAX_SENTINEL_DO_NOT_USE_:
// TODO(crbug.com/412700289): Revert once this is set.
count = MouseClickCount::kSingle;
break;
}
MouseClickType type;
switch (action.click_type()) {
case apc::ClickAction_ClickType_LEFT:
type = MouseClickType::kLeft;
break;
case apc::ClickAction_ClickType_RIGHT:
type = MouseClickType::kRight;
break;
case apc::
ClickAction_ClickType_ClickAction_ClickType_INT_MIN_SENTINEL_DO_NOT_USE_:
case apc::
ClickAction_ClickType_ClickAction_ClickType_INT_MAX_SENTINEL_DO_NOT_USE_:
case apc::ClickAction_ClickType_UNKNOWN_CLICK_TYPE:
// TODO(crbug.com/412700289): Revert once this is set.
type = MouseClickType::kLeft;
break;
}
auto target = ToPageTarget(action.target());
if (!target.has_value()) {
return nullptr;
}
return std::make_unique<ClickToolRequest>(tab_handle, target.value(), type,
count);
}
std::unique_ptr<ToolRequest> CreateTypeRequest(const TypeAction& action) {
using TypeMode = TypeToolRequest::Mode;
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_target() || !action.has_text() || !action.has_mode() ||
!action.has_follow_by_enter() || tab_handle == TabHandle::Null()) {
return nullptr;
}
TypeMode mode;
switch (action.mode()) {
case apc::TypeAction_TypeMode_DELETE_EXISTING:
mode = TypeMode::kReplace;
break;
case apc::TypeAction_TypeMode_PREPEND:
mode = TypeMode::kPrepend;
break;
case apc::TypeAction_TypeMode_APPEND:
mode = TypeMode::kAppend;
break;
case apc::TypeAction_TypeMode_UNKNOWN_TYPE_MODE:
case apc::
TypeAction_TypeMode_TypeAction_TypeMode_INT_MIN_SENTINEL_DO_NOT_USE_:
case apc::
TypeAction_TypeMode_TypeAction_TypeMode_INT_MAX_SENTINEL_DO_NOT_USE_:
// TODO(crbug.com/412700289): Revert once this is set.
mode = TypeMode::kReplace;
break;
}
auto target = ToPageTarget(action.target());
if (!target.has_value()) {
return nullptr;
}
return std::make_unique<TypeToolRequest>(tab_handle, target.value(),
action.text(),
action.follow_by_enter(), mode);
}
std::unique_ptr<ToolRequest> CreateScrollRequest(const ScrollAction& action) {
using Direction = ScrollToolRequest::Direction;
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_direction() || !action.has_distance() ||
tab_handle == TabHandle::Null()) {
return nullptr;
}
std::optional<PageTarget> target;
if (action.has_target()) {
target = ToPageTarget(action.target());
} else {
// Scroll action may omit a target which means "target the viewport".
TabInterface* tab = tab_handle.Get();
if (!tab) {
return nullptr;
}
std::string document_identifier =
DocumentIdentifierUserData::GetOrCreateForCurrentDocument(
tab->GetContents()->GetPrimaryMainFrame())
->serialized_token();
target.emplace(
PageTarget(DomNode{.node_id = kRootElementDomNodeId,
.document_identifier = document_identifier}));
}
if (!target) {
return nullptr;
}
Direction direction;
switch (action.direction()) {
case apc::ScrollAction_ScrollDirection_LEFT:
direction = Direction::kLeft;
break;
case apc::ScrollAction_ScrollDirection_RIGHT:
direction = Direction::kRight;
break;
case apc::ScrollAction_ScrollDirection_UP:
direction = Direction::kUp;
break;
case apc::ScrollAction_ScrollDirection_DOWN:
direction = Direction::kDown;
break;
case apc::ScrollAction_ScrollDirection_UNKNOWN_SCROLL_DIRECTION:
case apc::
ScrollAction_ScrollDirection_ScrollAction_ScrollDirection_INT_MIN_SENTINEL_DO_NOT_USE_:
case apc::
ScrollAction_ScrollDirection_ScrollAction_ScrollDirection_INT_MAX_SENTINEL_DO_NOT_USE_:
// TODO(crbug.com/412700289): Revert once this is set.
direction = Direction::kDown;
break;
}
return std::make_unique<ScrollToolRequest>(tab_handle, target.value(),
direction, action.distance());
}
std::unique_ptr<ToolRequest> CreateMoveMouseRequest(
const MoveMouseAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_target() || tab_handle == TabHandle::Null()) {
return nullptr;
}
auto target = ToPageTarget(action.target());
if (!target.has_value()) {
return nullptr;
}
return std::make_unique<MoveMouseToolRequest>(tab_handle, target.value());
}
std::unique_ptr<ToolRequest> CreateScrollToRequest(
const ScrollToAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_target() || tab_handle == TabHandle::Null()) {
return nullptr;
}
auto target = ToPageTarget(action.target());
if (!target.has_value()) {
return nullptr;
}
return std::make_unique<ScrollToToolRequest>(tab_handle, target.value());
}
std::unique_ptr<ToolRequest> CreateDragAndReleaseRequest(
const DragAndReleaseAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_from_target() || !action.has_to_target() ||
tab_handle == TabHandle::Null()) {
return nullptr;
}
auto from_target = ToPageTarget(action.from_target());
if (!from_target.has_value()) {
return nullptr;
}
auto to_target = ToPageTarget(action.to_target());
if (!to_target.has_value()) {
return nullptr;
}
return std::make_unique<DragAndReleaseToolRequest>(
tab_handle, from_target.value(), to_target.value());
}
std::unique_ptr<ToolRequest> CreateSelectRequest(const SelectAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_value() || !action.has_target() ||
tab_handle == TabHandle::Null()) {
return nullptr;
}
auto target = ToPageTarget(action.target());
if (!target.has_value()) {
return nullptr;
}
return std::make_unique<SelectToolRequest>(tab_handle, target.value(),
action.value());
}
std::unique_ptr<ToolRequest> CreateNavigateRequest(
const NavigateAction& action) {
TabHandle tab_handle = GetTabHandle(action);
if (!action.has_url() || tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<NavigateToolRequest>(tab_handle, GURL(action.url()));
}
std::unique_ptr<ToolRequest> CreateCreateTabRequest(
const CreateTabAction& action) {
if (!action.has_window_id()) {
return nullptr;
}
int32_t window_id = action.window_id();
// TODO(bokan): Is the foreground bit always set? If not, should this return
// an error or default to what? For now we default to foreground.
WindowOpenDisposition disposition =
!action.has_foreground() || action.foreground()
? WindowOpenDisposition::NEW_FOREGROUND_TAB
: WindowOpenDisposition::NEW_BACKGROUND_TAB;
return std::make_unique<CreateTabToolRequest>(window_id, disposition);
}
std::unique_ptr<ToolRequest> CreateActivateTabRequest(
const ActivateTabAction& action) {
tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<ActivateTabToolRequest>(tab_handle);
}
std::unique_ptr<ToolRequest> CreateCloseTabRequest(
const CloseTabAction& action) {
tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<CloseTabToolRequest>(tab_handle);
}
std::unique_ptr<ToolRequest> CreateCreateWindowRequest(
const CreateWindowAction& action) {
return std::make_unique<CreateWindowToolRequest>();
}
std::unique_ptr<ToolRequest> CreateCloseWindowRequest(
const CloseWindowAction& action) {
if (!action.has_window_id()) {
return nullptr;
}
return std::make_unique<CloseWindowToolRequest>(action.window_id());
}
std::unique_ptr<ToolRequest> CreateActivateWindowRequest(
const ActivateWindowAction& action) {
if (!action.has_window_id()) {
return nullptr;
}
return std::make_unique<ActivateWindowToolRequest>(action.window_id());
}
std::unique_ptr<ToolRequest> CreateBackRequest(
const HistoryBackAction& action) {
tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<HistoryToolRequest>(
tab_handle, HistoryToolRequest::Direction::kBack);
}
std::unique_ptr<ToolRequest> CreateForwardRequest(
const HistoryForwardAction& action) {
tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<HistoryToolRequest>(
tab_handle, HistoryToolRequest::Direction::kForward);
}
std::unique_ptr<ToolRequest> CreateWaitRequest(const WaitAction& action) {
const base::TimeDelta wait_time =
action.has_wait_time_ms() ? base::Milliseconds(action.wait_time_ms())
: base::Seconds(3);
return std::make_unique<WaitToolRequest>(wait_time);
}
std::unique_ptr<ToolRequest> CreateAttemptLoginRequest(
const AttemptLoginAction& action) {
const tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
return std::make_unique<AttemptLoginToolRequest>(tab_handle);
}
std::unique_ptr<ToolRequest> CreateScriptToolRequest(
const ScriptToolAction& action) {
const tabs::TabHandle tab_handle = GetTabHandle(action);
if (tab_handle == TabHandle::Null()) {
return nullptr;
}
// TODO(khushalsagar): Remove once the callers are setting up this ID
// correctly.
std::string document_identifier;
if (action.has_document_identifier()) {
document_identifier = action.document_identifier().serialized_token();
} else {
auto* main_rfh = tab_handle.Get()->GetContents()->GetPrimaryMainFrame();
document_identifier = DocumentIdentifierUserData::GetDocumentIdentifier(
main_rfh->GetGlobalFrameToken())
.value_or("");
}
return std::make_unique<ScriptToolRequest>(
tab_handle,
PageTarget(DomNode{.node_id = kRootElementDomNodeId,
.document_identifier = document_identifier}),
action.tool_name(), action.input_arguments());
}
class ActorJournalFetchPageProgressListener
: public page_content_annotations::FetchPageProgressListener {
public:
ActorJournalFetchPageProgressListener(
base::SafeRef<AggregatedJournal> journal,
const GURL& url,
TaskId task_id)
: journal_(journal), url_(url), task_id_(task_id) {}
~ActorJournalFetchPageProgressListener() override = default;
void BeginScreenshot() override {
screenshot_entry_ = journal_->CreatePendingAsyncEntry(
url_, task_id_, mojom::JournalTrack::kActor, "GrabScreenshot", {});
}
void EndScreenshot(std::optional<std::string> error) override {
if (error.has_value()) {
screenshot_entry_->EndEntry(
JournalDetailsBuilder().AddError(*error).Build());
} else {
screenshot_entry_->EndEntry({});
}
}
void BeginAPC() override {
apc_entry_ = journal_->CreatePendingAsyncEntry(
url_, task_id_, mojom::JournalTrack::kActor, "GrabAPC", {});
}
void EndAPC(std::optional<std::string> error) override {
if (error.has_value()) {
apc_entry_->EndEntry(JournalDetailsBuilder().AddError(*error).Build());
} else {
apc_entry_->EndEntry({});
}
}
private:
base::SafeRef<AggregatedJournal> journal_;
GURL url_;
TaskId task_id_;
std::unique_ptr<AggregatedJournal::PendingAsyncEntry> screenshot_entry_;
std::unique_ptr<AggregatedJournal::PendingAsyncEntry> apc_entry_;
};
std::unique_ptr<ToolRequest> CreateToolRequest(
const optimization_guide::proto::Action& action) {
TRACE_EVENT1("actor", "CreateToolRequest", "action_type",
static_cast<int>(action.action_case()));
switch (action.action_case()) {
case optimization_guide::proto::Action::kClick: {
const ClickAction& click_action = action.click();
return CreateClickRequest(click_action);
}
case optimization_guide::proto::Action::kType: {
const TypeAction& type_action = action.type();
return CreateTypeRequest(type_action);
}
case optimization_guide::proto::Action::kScroll: {
const ScrollAction& scroll_action = action.scroll();
return CreateScrollRequest(scroll_action);
}
case optimization_guide::proto::Action::kMoveMouse: {
const MoveMouseAction& move_mouse_action = action.move_mouse();
return CreateMoveMouseRequest(move_mouse_action);
}
case optimization_guide::proto::Action::kDragAndRelease: {
const DragAndReleaseAction& drag_action = action.drag_and_release();
return CreateDragAndReleaseRequest(drag_action);
}
case optimization_guide::proto::Action::kSelect: {
const SelectAction& select_action = action.select();
return CreateSelectRequest(select_action);
}
case optimization_guide::proto::Action::kNavigate: {
const NavigateAction& navigate_action = action.navigate();
return CreateNavigateRequest(navigate_action);
}
case optimization_guide::proto::Action::kBack: {
const HistoryBackAction& back_action = action.back();
return CreateBackRequest(back_action);
}
case optimization_guide::proto::Action::kForward: {
const HistoryForwardAction& forward_action = action.forward();
return CreateForwardRequest(forward_action);
}
case optimization_guide::proto::Action::kWait: {
const WaitAction& wait_action = action.wait();
return CreateWaitRequest(wait_action);
}
case optimization_guide::proto::Action::kCreateTab: {
const CreateTabAction& create_tab_action = action.create_tab();
return CreateCreateTabRequest(create_tab_action);
}
case optimization_guide::proto::Action::kCloseTab: {
const CloseTabAction& close_tab_action = action.close_tab();
return CreateCloseTabRequest(close_tab_action);
}
case optimization_guide::proto::Action::kActivateTab: {
const ActivateTabAction& activate_tab_action = action.activate_tab();
return CreateActivateTabRequest(activate_tab_action);
}
case optimization_guide::proto::Action::kAttemptLogin: {
const AttemptLoginAction& attempt_login_action = action.attempt_login();
return CreateAttemptLoginRequest(attempt_login_action);
}
case optimization_guide::proto::Action::kScriptTool: {
const ScriptToolAction& script_tool_action = action.script_tool();
return CreateScriptToolRequest(script_tool_action);
}
case optimization_guide::proto::Action::kScrollTo: {
const ScrollToAction& scroll_to_action = action.scroll_to();
return CreateScrollToRequest(scroll_to_action);
}
case optimization_guide::proto::Action::kCreateWindow: {
const CreateWindowAction& create_window_action = action.create_window();
return CreateCreateWindowRequest(create_window_action);
}
case optimization_guide::proto::Action::kCloseWindow: {
const CloseWindowAction& close_window_action = action.close_window();
return CreateCloseWindowRequest(close_window_action);
}
case optimization_guide::proto::Action::kActivateWindow: {
const ActivateWindowAction& activate_window_action =
action.activate_window();
return CreateActivateWindowRequest(activate_window_action);
}
case optimization_guide::proto::Action::kYieldToUser:
NOTIMPLEMENTED();
break;
case optimization_guide::proto::Action::ACTION_NOT_SET:
ACTOR_LOG() << "Action Type Not Set!";
break;
default:
NOTIMPLEMENTED();
break;
}
return nullptr;
}
} // namespace
base::expected<std::vector<std::unique_ptr<ToolRequest>>, size_t>
BuildToolRequest(const optimization_guide::proto::Actions& actions) {
TRACE_EVENT0("actor", "BuildToolRequest");
std::vector<std::unique_ptr<ToolRequest>> requests;
requests.reserve(actions.actions_size());
for (int i = 0; i < actions.actions_size(); ++i) {
std::unique_ptr<ToolRequest> request =
CreateToolRequest(actions.actions().at(i));
if (request) {
requests.push_back(std::move(request));
} else {
return base::unexpected(base::checked_cast<size_t>(i));
}
}
return requests;
}
void FillInTabObservation(
const page_content_annotations::FetchPageContextResult& fetch_result,
apc::TabObservation& tab_observation) {
TRACE_EVENT0("actor", "FillInTabObservation");
if (fetch_result.screenshot_result.has_value()) {
auto& data = fetch_result.screenshot_result->jpeg_data;
if (data.size() != 0) {
tab_observation.set_screenshot_mime_type(kMimeTypeJpeg);
// TODO(bokan): Can we avoid a copy here?
tab_observation.set_screenshot(data.data(), data.size());
}
}
if (fetch_result.annotated_page_content_result) {
*tab_observation.mutable_annotated_page_content() =
fetch_result.annotated_page_content_result->proto;
if (fetch_result.annotated_page_content_result->metadata) {
auto* proto_metadata = tab_observation.mutable_metadata();
const auto& mojom_metadata =
*fetch_result.annotated_page_content_result->metadata;
for (const auto& mojom_frame_metadata : mojom_metadata.frame_metadata) {
auto* proto_frame_metadata = proto_metadata->add_frame_metadata();
proto_frame_metadata->set_url(mojom_frame_metadata->url.spec());
for (const auto& mojom_meta_tag : mojom_frame_metadata->meta_tags) {
auto* proto_meta_tag = proto_frame_metadata->add_meta_tags();
proto_meta_tag->set_name(mojom_meta_tag->name);
proto_meta_tag->set_content(mojom_meta_tag->content);
}
}
}
}
}
namespace {
void FetchCallback(
base::WeakPtr<Profile> profile,
TaskId task_id,
base::RepeatingClosure barrier,
apc::TabObservation* tab_observation,
std::vector<actor::ActionResultWithLatencyInfo> action_results,
base::TimeTicks start_time,
base::TimeTicks fetch_context_time,
apc::ActionsResult_LatencyInformation* latency_info,
ActorKeyedService::TabObservationResult result) {
TRACE_EVENT0("actor", "FetchCallback");
CHECK(tab_observation);
CHECK(latency_info);
base::ScopedClosureRunner run_barrier_at_return(barrier);
if (!profile) {
return;
}
if (!result.has_value()) {
auto* actor_service = actor::ActorKeyedService::Get(profile.get());
actor_service->GetJournal().Log(
GURL(), task_id, actor::mojom::JournalTrack::kActor, result.error(),
JournalDetailsBuilder().Add("tabId", tab_observation->id()).Build());
// For now record everything as a timeout.
tab_observation->set_result(
apc::TabObservation::TAB_OBSERVATION_SCREENSHOT_TIMEOUT);
return;
}
FetchPageContextResult& fetch_result = **result;
// RequestTabObservation should return an error if these aren't filled in.
CHECK(fetch_result.screenshot_result.has_value());
CHECK(fetch_result.annotated_page_content_result.has_value());
tab_observation->set_result(apc::TabObservation::TAB_OBSERVATION_OK);
{
apc::ActionsResult_LatencyInformation_LatencyStep* latency_step =
latency_info->add_latency_steps();
latency_step->mutable_annotated_page_content()->set_id(
tab_observation->id());
latency_step->set_latency_start_ms(
(fetch_context_time - start_time).InMilliseconds());
latency_step->set_latency_stop_ms(
(fetch_result.annotated_page_content_result.value().end_time -
start_time)
.InMilliseconds());
base::UmaHistogramMediumTimes(
"Actor.PageContext.APC.Duration",
fetch_result.annotated_page_content_result.value().end_time -
fetch_context_time);
}
{
apc::ActionsResult_LatencyInformation_LatencyStep* latency_step =
latency_info->add_latency_steps();
latency_step->mutable_screenshot()->set_id(tab_observation->id());
latency_step->set_latency_start_ms(
(fetch_context_time - start_time).InMilliseconds());
latency_step->set_latency_stop_ms(
(fetch_result.screenshot_result.value().end_time - start_time)
.InMilliseconds());
base::UmaHistogramMediumTimes(
"Actor.PageContext.Screenshot.Duration",
fetch_result.screenshot_result.value().end_time - fetch_context_time);
}
// TODO(khushalsagar): Remove this once consumers use ActionResults for script
// tool results.
CopyScriptToolResults(*fetch_result.annotated_page_content_result->proto
.mutable_main_frame_data(),
action_results);
FillInTabObservation(fetch_result, *tab_observation);
}
} // namespace
void BuildActionsResultWithObservations(
content::BrowserContext& browser_context,
base::TimeTicks actions_start_time,
mojom::ActionResultCode result_code,
std::optional<size_t> index_of_failed_action,
std::vector<actor::ActionResultWithLatencyInfo> action_results,
const ActorTask& task,
bool skip_async_observation_information,
base::OnceCallback<
void(std::unique_ptr<apc::ActionsResult>,
std::unique_ptr<actor::AggregatedJournal::PendingAsyncEntry>)>
callback) {
TRACE_EVENT0("actor", "BuildActionsResultWithObservations");
auto* profile = Profile::FromBrowserContext(&browser_context);
auto* actor_service = actor::ActorKeyedService::Get(profile);
CHECK(actor_service);
std::unique_ptr<actor::AggregatedJournal::PendingAsyncEntry> journal_entry =
actor_service->GetJournal().CreatePendingAsyncEntry(
GURL(), task.id(), actor::mojom::JournalTrack::kActor,
"BuildActionsResultWithObservations", {});
auto response = std::make_unique<apc::ActionsResult>();
response->set_action_result(static_cast<int32_t>(result_code));
if (index_of_failed_action) {
response->set_index_of_failed_action(*index_of_failed_action);
}
CopyScriptToolResults(*response, action_results);
apc::ActionsResult_LatencyInformation* latency_info =
response->mutable_latency_information();
for (size_t i = 0; i < action_results.size(); ++i) {
auto& action_result = action_results.at(i);
CHECK(action_result.result->execution_end_time);
{
apc::ActionsResult_LatencyInformation_LatencyStep* latency_step =
latency_info->add_latency_steps();
latency_step->mutable_action()->set_action_index(i);
latency_step->set_latency_start_ms(
(action_result.start_time - actions_start_time).InMilliseconds());
latency_step->set_latency_stop_ms(
(*action_result.result->execution_end_time - actions_start_time)
.InMilliseconds());
}
// Don't report a page stabilization time if the start and end
// are the same. Not every tool needs stabilization.
if (*action_result.result->execution_end_time != action_result.end_time) {
apc::ActionsResult_LatencyInformation_LatencyStep* latency_step =
latency_info->add_latency_steps();
latency_step->mutable_page_stabilization()->set_action_index(i);
latency_step->set_latency_start_ms(
(*action_result.result->execution_end_time - actions_start_time)
.InMilliseconds());
latency_step->set_latency_stop_ms(
(action_result.end_time - actions_start_time).InMilliseconds());
}
}
std::vector<Browser*> browsers = chrome::FindAllTabbedBrowsersWithProfile(
profile, /*ignore_closing_browsers=*/true);
for (Browser* browser : browsers) {
apc::WindowObservation* window_observation = response->add_windows();
window_observation->set_id(browser->session_id().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());
}
}
absl::flat_hash_map<tabs::TabInterface*, apc::TabObservation*> tabs_to_fetch;
for (const tabs::TabHandle& handle : task.GetLastActedTabs()) {
// Include a TabObservation entry for acted on tabs. If the tab no longer
// exists or the fetch context failed, the observation will be empty.
// TODO(crbug.com/434263095): We should probably avoid capturing
// observations if an action fails with kUrlBlocked. That might be better
// implemented by not putting the tab into the LastActedTabs set.
TabInterface* tab = handle.Get();
if (!tab) {
apc::TabObservation* tab_observation = response->add_tabs();
tab_observation->set_id(handle.raw_value());
tab_observation->set_result(
apc::TabObservation::TAB_OBSERVATION_TAB_WENT_AWAY);
actor_service->GetJournal().Log(GURL(), task.id(),
actor::mojom::JournalTrack::kActor,
"TabObservationFailed",
JournalDetailsBuilder()
.Add("tabId", handle.raw_value())
.AddError("TabWentAway")
.Build());
} else if (!tab->GetContents()
->GetPrimaryMainFrame()
->IsRenderFrameLive()) {
// TODO(crbug.com/392167142): We should also handle the crashed subframes.
// However we don't want unrelated subframe crash to terminate the task.
apc::TabObservation* tab_observation = response->add_tabs();
tab_observation->set_id(handle.raw_value());
tab_observation->set_result(
apc::TabObservation::TAB_OBSERVATION_PAGE_CRASHED);
actor_service->GetJournal().Log(GURL(), task.id(),
actor::mojom::JournalTrack::kActor,
"TabObservationFailed",
JournalDetailsBuilder()
.Add("tabId", handle.raw_value())
.AddError("Page crashed")
.Build());
} else {
apc::TabObservation* tab_observation = response->add_tabs();
tabs_to_fetch.emplace(tab, tab_observation);
tab_observation->set_id(tab->GetHandle().raw_value());
if (skip_async_observation_information) {
tab_observation->set_result(apc::TabObservation::TAB_OBSERVATION_OK);
}
}
}
base::UmaHistogramCounts1000("Actor.PageContext.TabCount",
tabs_to_fetch.size());
if (skip_async_observation_information) {
std::move(callback).Run(std::move(response), std::move(journal_entry));
return;
}
base::RepeatingClosure barrier = base::BarrierClosure(
tabs_to_fetch.size(),
base::BindOnce(std::move(callback), std::move(response),
std::move(journal_entry)));
for (auto& [tab, tab_observation] : tabs_to_fetch) {
// tab_observation can be Unretained because the underlying APC is owned
// by the barrier which is ref-counted.
actor_service->RequestTabObservation(
*tab, task.id(),
base::BindOnce(FetchCallback, profile->GetWeakPtr(), task.id(), barrier,
base::Unretained(tab_observation),
action_results, actions_start_time,
base::TimeTicks::Now(), base::Unretained(latency_info)));
}
}
apc::ActionsResult BuildErrorActionsResult(
mojom::ActionResultCode result_code,
std::optional<size_t> index_of_failed_action) {
TRACE_EVENT0("actor", "BuildErrorActionsResult");
apc::ActionsResult response;
CHECK(!IsOk(result_code));
response.set_action_result(static_cast<int32_t>(result_code));
if (index_of_failed_action) {
response.set_index_of_failed_action(*index_of_failed_action);
}
return response;
}
std::string ToBase64(const optimization_guide::proto::Actions& actions) {
TRACE_EVENT0("actor", "ActionsToBase64");
size_t size = actions.ByteSizeLong();
std::vector<uint8_t> buffer(size);
actions.SerializeToArray(buffer.data(), size);
return base::Base64Encode(buffer);
}
std::unique_ptr<page_content_annotations::FetchPageProgressListener>
CreateActorJournalFetchPageProgressListener(
base::SafeRef<AggregatedJournal> journal,
const GURL& url,
TaskId task_id) {
return std::make_unique<ActorJournalFetchPageProgressListener>(journal, url,
task_id);
}
} // namespace actor