blob: a51052b07001e98e459d3b180523cc147e1d236d [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/permissions/prediction_service/permissions_aiv1_handler.h"
#include "base/check_is_test.h"
#include "base/containers/fixed_flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/sequence_checker.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/optimization_guide/proto/features/permissions_ai.pb.h"
#include "components/permissions/request_type.h"
namespace permissions {
namespace {
using ::optimization_guide::ModelBasedCapabilityKey;
using ::optimization_guide::SessionConfigParams;
using ::optimization_guide::proto::PermissionsAiRequest;
using ::optimization_guide::proto::PermissionsAiResponse;
using ::optimization_guide::proto::PermissionType;
using EligibilityReason = ::optimization_guide::OnDeviceModelEligibilityReason;
constexpr ModelBasedCapabilityKey kFeatureKey =
ModelBasedCapabilityKey::kPermissionsAi;
constexpr SessionConfigParams kSessionConfigParams = SessionConfigParams{
.execution_mode = SessionConfigParams::ExecutionMode::kOnDeviceOnly,
};
// Max delay for permissions AI model execution; inquiries that need more time
// get cancelled.
constexpr base::TimeDelta kMaxModelExecutionDuration = base::Seconds(5);
void LogOnDeviceModelPreviousSessionFinishedInTime(bool success) {
base::UmaHistogramBoolean("Permissions.AIv1.PreviousSessionFinishedInTime",
success);
}
void LogOnDeviceModelSessionCreationSuccess(bool session_created) {
base::UmaHistogramBoolean("Permissions.AIv1.SessionCreationSuccess",
session_created);
}
void LogOnDeviceModelExecutionSuccessAndTime(
bool success,
base::TimeTicks session_execution_start_time) {
base::UmaHistogramBoolean("Permissions.AIv1.ExecutionSuccess", success);
if (success) {
base::UmaHistogramMediumTimes(
"Permissions.AIv1.ExecutionDuration",
base::TimeTicks::Now() - session_execution_start_time);
}
}
void LogOnDeviceModelExecutionParse(bool success) {
base::UmaHistogramBoolean("Permissions.AIv1.ResponseParseSuccess", success);
}
void LogOnDeviceModelAvailabilityAtInquiryTime(bool model_available) {
base::UmaHistogramBoolean("Permissions.AIv1.AvailableAtInquiryTime",
model_available);
}
void LogOnDeviceModelExecutionTimedOut(bool timed_out) {
base::UmaHistogramBoolean("Permissions.AIv1.ExecutionTimedOut", timed_out);
}
PermissionType GetPermissionType(permissions::RequestType request_type) {
switch (request_type) {
case permissions::RequestType::kNotifications:
return PermissionType::PERMISSION_TYPE_NOTIFICATIONS;
case permissions::RequestType::kGeolocation:
return PermissionType::PERMISSION_TYPE_GEOLOCATION;
default:
return PermissionType::PERMISSION_TYPE_NOT_SPECIFIED;
}
}
bool IsOnDeviceModelAvailable(EligibilityReason reason) {
return reason == EligibilityReason::kSuccess;
}
} // namespace
// Manages sessions and related data. This will gift us more flexibility, i.e.
// it can allow us to have multiple sessions at some point in the future at the
// same time and also to easily cancel session without fearing asynchronous
// calls to OnModelExecutionComplete meddling with a potentially new permission
// request.
class PermissionsAiv1Handler::EvaluationTask {
public:
explicit EvaluationTask(OptimizationGuideKeyedService* optimization_guide) {
DETACH_FROM_SEQUENCE(sequence_checker_);
session_ =
optimization_guide->StartSession(kFeatureKey, kSessionConfigParams);
}
~EvaluationTask() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Explicitly invalidate weak pointers to prevent callbacks that may be
// triggered by the destructor logic.
weak_ptr_factory_.InvalidateWeakPtrs();
if (inquire_on_device_model_callback_) {
std::move(inquire_on_device_model_callback_).Run(std::nullopt);
}
}
bool IsActive() { return session_ != nullptr; }
void ExecuteModel(
std::string rendered_text,
permissions::RequestType request_type,
base::OnceCallback<void(std::optional<PermissionsAiResponse>)> callback,
base::OneShotTimer* execution_timer) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!session_) {
std::move(callback).Run(std::nullopt);
return;
}
inquire_on_device_model_callback_ = std::move(callback);
PermissionsAiRequest request;
request.set_rendered_text(std::move(rendered_text));
request.set_permission_type(GetPermissionType(request_type));
session_execution_start_time_ = base::TimeTicks::Now();
session_->ExecuteModel(
request,
base::BindRepeating(
&PermissionsAiv1Handler::EvaluationTask::OnModelExecutionComplete,
weak_ptr_factory_.GetWeakPtr(), execution_timer));
}
private:
void OnModelExecutionComplete(
base::OneShotTimer* execution_timer,
optimization_guide::OptimizationGuideModelStreamingExecutionResult
result) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// This is a non-error response, but since the result is not fully available
// yet, we defer callback execution until we get a complete response.
if (result.response.has_value() && !result.response->is_complete) {
return;
}
if (execution_timer) {
execution_timer->Stop();
LogOnDeviceModelExecutionTimedOut(/*timed_out=*/false);
}
// Since we do not want to reuse a once used session, lets make sure that we
// destroy it when the model execution has finished.
session_.reset();
// If there is no callback available anymore, we do not need to do any work
// here.
if (!inquire_on_device_model_callback_) {
return;
}
LogOnDeviceModelExecutionSuccessAndTime(/*success=*/
result.response.has_value(),
session_execution_start_time_);
if (!result.response.has_value()) {
VLOG(1)
<< "[PermissionsAIv1] OnModelExecutionComplete failed with error: "
<< static_cast<int>(result.response.error().error());
std::move(inquire_on_device_model_callback_).Run(std::nullopt);
return;
}
std::optional<PermissionsAiResponse> permissions_ai_response =
optimization_guide::ParsedAnyMetadata<PermissionsAiResponse>(
result.response->response);
LogOnDeviceModelExecutionParse(
/*success=*/permissions_ai_response.has_value());
if (!permissions_ai_response.has_value()) {
VLOG(1) << "[PermissionsAIv1] OnModelExecutionComplete failed while "
"parsing the response proto.";
std::move(inquire_on_device_model_callback_).Run(std::nullopt);
return;
}
std::move(inquire_on_device_model_callback_).Run(permissions_ai_response);
}
SEQUENCE_CHECKER(sequence_checker_);
std::unique_ptr<optimization_guide::OptimizationGuideModelExecutor::Session>
session_;
base::TimeTicks session_execution_start_time_;
base::OnceCallback<void(
std::optional<optimization_guide::proto::PermissionsAiResponse>)>
inquire_on_device_model_callback_;
base::WeakPtrFactory<PermissionsAiv1Handler::EvaluationTask>
weak_ptr_factory_{this};
};
PermissionsAiv1Handler::PermissionsAiv1Handler(
OptimizationGuideKeyedService* optimization_guide)
: optimization_guide_(optimization_guide),
execution_timer_(std::make_unique<base::OneShotTimer>()) {}
PermissionsAiv1Handler::~PermissionsAiv1Handler() {
execution_timer_->Stop();
}
bool PermissionsAiv1Handler::IsModelExecutionInProgress() {
return evaluation_task_ && evaluation_task_->IsActive();
}
void PermissionsAiv1Handler::InquireAiOnDeviceModel(
std::string rendered_text,
permissions::RequestType request_type,
base::OnceCallback<void(std::optional<PermissionsAiResponse>)> callback) {
if (!optimization_guide_) {
// If optimization_guide_ is a nullptr then we cannot do anything here at
// all.
std::move(callback).Run(std::nullopt);
return;
}
EligibilityReason model_state =
optimization_guide_->GetOnDeviceModelEligibility(kFeatureKey);
bool model_available_at_inquiry_time = IsOnDeviceModelAvailable(model_state);
LogOnDeviceModelAvailabilityAtInquiryTime(model_available_at_inquiry_time);
if (model_available_at_inquiry_time) {
if (IsModelExecutionInProgress()) {
LogOnDeviceModelPreviousSessionFinishedInTime(/*success=*/false);
// TODO(crbug.com/382447738): It can happen that a new inquiry comes
// before the previous finishes its execution. To avoid unexpected
// behavior return `std::nullopt` which means another type of prediction_service logic
// will be executed.
std::move(callback).Run(std::nullopt);
return;
}
LogOnDeviceModelPreviousSessionFinishedInTime(/*success=*/true);
}
// We make sure by recreating the session on every inquiry that no
// callback executions or timing data linked to the previous request gets
// accidentally mixed up with the new request when we cancel lengthy model
// executions.
evaluation_task_ = std::make_unique<EvaluationTask>(optimization_guide_);
LogOnDeviceModelSessionCreationSuccess(
/*session_created=*/evaluation_task_->IsActive());
if (evaluation_task_->IsActive()) {
evaluation_task_->ExecuteModel(std::move(rendered_text), request_type,
std::move(callback), execution_timer_.get());
// We check again to make sure the callback did not immediately return.
if (evaluation_task_->IsActive()) {
execution_timer_->Start(
FROM_HERE, kMaxModelExecutionDuration,
base::BindOnce(&PermissionsAiv1Handler::CancelModelExecution,
weak_ptr_factory_.GetWeakPtr()));
}
return;
}
// We end up here if the model download has not started/ended yet or
// there is a transient error. If we did not download the model yet, trying
// to create the session will have started the download already so we do
// nothing here and just return. We also return for the other two cases,
// hoping the next inquiry will have better luck.
std::move(callback).Run(std::nullopt);
}
void PermissionsAiv1Handler::CancelModelExecution() {
evaluation_task_.reset();
execution_timer_->Stop();
LogOnDeviceModelExecutionTimedOut(/*timed_out=*/true);
}
void PermissionsAiv1Handler::set_execution_timer_for_testing(
std::unique_ptr<base::OneShotTimer> execution_timer) {
CHECK_IS_TEST();
execution_timer_ = std::move(execution_timer);
}
} // namespace permissions