| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/optimization_guide/model/optimization_guide_service.h" |
| |
| #import "base/apple/bundle_locations.h" |
| #import "base/files/file_util.h" |
| #import "base/functional/bind.h" |
| #import "base/functional/callback.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/path_service.h" |
| #import "base/system/sys_info.h" |
| #import "base/task/thread_pool.h" |
| #import "base/time/default_clock.h" |
| #import "components/component_updater/pref_names.h" |
| #import "components/optimization_guide/core/hints/command_line_top_host_provider.h" |
| #import "components/optimization_guide/core/hints/hints_processing_util.h" |
| #import "components/optimization_guide/core/hints/optimization_guide_navigation_data.h" |
| #import "components/optimization_guide/core/hints/optimization_guide_store.h" |
| #import "components/optimization_guide/core/hints/top_host_provider.h" |
| #import "components/optimization_guide/core/model_execution/model_execution_manager.h" |
| #import "components/optimization_guide/core/model_execution/on_device_model_service_controller.h" |
| #import "components/optimization_guide/core/optimization_guide_constants.h" |
| #import "components/optimization_guide/core/optimization_guide_features.h" |
| #import "components/optimization_guide/core/optimization_guide_logger.h" |
| #import "components/optimization_guide/core/optimization_guide_util.h" |
| #import "components/optimization_guide/core/prediction_manager.h" |
| #import "components/prefs/pref_service.h" |
| #import "components/services/unzip/in_process_unzipper.h" |
| #import "components/signin/public/identity_manager/identity_manager.h" |
| #import "components/variations/synthetic_trials.h" |
| #import "ios/chrome/browser/metrics/model/ios_chrome_metrics_service_accessor.h" |
| #import "ios/chrome/browser/optimization_guide/model/ios_chrome_hints_manager.h" |
| #import "ios/chrome/browser/optimization_guide/model/ios_chrome_prediction_model_store.h" |
| #import "ios/chrome/browser/optimization_guide/model/optimization_guide_service_factory.h" |
| #import "ios/chrome/browser/optimization_guide/model/tab_url_provider_impl.h" |
| #import "ios/chrome/browser/shared/model/application_context/application_context.h" |
| #import "ios/chrome/browser/shared/model/paths/paths.h" |
| #import "ios/web/public/navigation/navigation_context.h" |
| #import "ios/web/public/thread/web_thread.h" |
| #import "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| #import "components/optimization_guide/core/model_execution/on_device_asset_manager.h" |
| #import "components/optimization_guide/core/model_execution/on_device_model_component.h" |
| #import "ios/chrome/browser/optimization_guide/model/on_device_model_service_controller_ios.h" |
| #endif // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| |
| namespace { |
| |
| using ModelExecutionError = optimization_guide:: |
| OptimizationGuideModelExecutionError::ModelExecutionError; |
| |
| #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| using ::optimization_guide::OnDeviceModelComponentStateManager; |
| #endif // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| |
| // Deletes old store paths that were written in incorrect locations. |
| void DeleteOldStorePaths(const base::FilePath& profile_path) { |
| // Added 11/2023 |
| // |
| // Delete the old profile-wide model download store path, since |
| // the install-wide model store is enabled now. |
| base::ThreadPool::PostTask( |
| FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, |
| base::GetDeletePathRecursivelyCallback(profile_path.Append( |
| optimization_guide::kOldOptimizationGuidePredictionModelDownloads))); |
| } |
| |
| #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| class OnDeviceModelComponentStateManagerDelegate |
| : public OnDeviceModelComponentStateManager::Delegate { |
| public: |
| ~OnDeviceModelComponentStateManagerDelegate() override = default; |
| |
| base::FilePath GetInstallDirectory() override { |
| // The model is located in the app bundle. |
| return base::apple::OuterBundlePath(); |
| } |
| |
| void GetFreeDiskSpace(const base::FilePath& path, |
| base::OnceCallback<void(int64_t)> callback) override { |
| base::TaskTraits traits = {base::MayBlock(), |
| base::TaskPriority::BEST_EFFORT}; |
| if (optimization_guide::switches:: |
| ShouldGetFreeDiskSpaceWithUserVisiblePriorityTask()) { |
| traits.UpdatePriority(base::TaskPriority::USER_VISIBLE); |
| } |
| |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, traits, |
| base::BindOnce(&base::SysInfo::AmountOfFreeDiskSpace, path), |
| std::move(callback)); |
| } |
| |
| void RegisterInstaller( |
| scoped_refptr<OnDeviceModelComponentStateManager> state_manager, |
| bool is_already_installing) override { |
| // If a model is bundled with the app, call SetReady() and treat |
| // it as an override. Otherwise return and do nothing. |
| base::FilePath model_path = |
| base::apple::OuterBundlePath().Append("on_device_model"); |
| LOG(ERROR) << "model_file_path: " << model_path; |
| |
| state_manager->SetReady( |
| base::Version("override"), model_path, |
| base::Value::Dict().Set("BaseModelSpec", base::Value::Dict() |
| .Set("version", "override") |
| .Set("name", "override"))); |
| } |
| |
| void Uninstall(scoped_refptr<OnDeviceModelComponentStateManager> |
| state_manager) override { |
| // Do nothing since the model is bundled with the app. |
| } |
| }; |
| #endif // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| |
| } // namespace |
| |
| OptimizationGuideService::OptimizationGuideService( |
| leveldb_proto::ProtoDatabaseProvider* proto_db_provider, |
| const base::FilePath& profile_path, |
| bool off_the_record, |
| const std::string& application_locale, |
| base::WeakPtr<optimization_guide::OptimizationGuideStore> hint_store, |
| PrefService* pref_service, |
| BrowserList* browser_list, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| BackgroundDownloadServiceProvider background_download_service_provider, |
| signin::IdentityManager* identity_manager) |
| : pref_service_(pref_service), off_the_record_(off_the_record) { |
| DCHECK(optimization_guide::features::IsOptimizationHintsEnabled()); |
| |
| // In off the record profile, the stores of normal profile should be |
| // passed to the constructor. In normal profile, they will be created. |
| DCHECK(!off_the_record_ || hint_store); |
| base::FilePath models_dir; |
| if (!off_the_record_) { |
| // Only create a top host provider from the command line if provided. |
| top_host_provider_ = |
| optimization_guide::CommandLineTopHostProvider::CreateIfEnabled(); |
| tab_url_provider_ = std::make_unique<TabUrlProviderImpl>( |
| browser_list, base::DefaultClock::GetInstance()); |
| hint_store_ = |
| optimization_guide::features::ShouldPersistHintsToDisk() |
| ? std::make_unique<optimization_guide::OptimizationGuideStore>( |
| proto_db_provider, |
| profile_path.Append( |
| optimization_guide::kOptimizationGuideHintStore), |
| base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT}), |
| pref_service) |
| : nullptr; |
| hint_store = hint_store_ ? hint_store_->AsWeakPtr() : nullptr; |
| } |
| optimization_guide_logger_ = OptimizationGuideLogger::GetInstance(); |
| DCHECK(optimization_guide_logger_); |
| hints_manager_ = std::make_unique<optimization_guide::IOSChromeHintsManager>( |
| off_the_record_, application_locale, pref_service, hint_store, |
| top_host_provider_.get(), tab_url_provider_.get(), url_loader_factory, |
| identity_manager, optimization_guide_logger_.get()); |
| |
| if (optimization_guide::features::IsOptimizationTargetPredictionEnabled()) { |
| prediction_manager_ = |
| std::make_unique<optimization_guide::PredictionManager>( |
| optimization_guide::IOSChromePredictionModelStore::GetInstance(), |
| url_loader_factory, pref_service, off_the_record_, |
| application_locale, models_dir, optimization_guide_logger_.get(), |
| std::move(background_download_service_provider), |
| base::BindRepeating([]() { |
| return GetApplicationContext()->GetLocalState()->GetBoolean( |
| ::prefs::kComponentUpdatesEnabled); |
| }), |
| base::BindRepeating(&unzip::LaunchInProcessUnzipper)); |
| } |
| |
| if (!off_the_record_) { |
| #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| PrefService* local_state = GetApplicationContext()->GetLocalState(); |
| |
| // Create and startup the on-device model's state manager. |
| on_device_model_state_manager_ = |
| optimization_guide::OnDeviceModelComponentStateManager::CreateOrGet( |
| local_state, |
| std::make_unique<OnDeviceModelComponentStateManagerDelegate>()); |
| on_device_model_state_manager_->OnStartup(); |
| |
| // TODO(crbug.com/387509291): Always set a high perfomance class for |
| // prototyping. |
| on_device_model_state_manager_->DevicePerformanceClassChanged( |
| optimization_guide::OnDeviceModelPerformanceClass::kHigh); |
| |
| // Create the manager for on-device model execution. |
| scoped_refptr<optimization_guide::OnDeviceModelServiceController> |
| on_device_model_service_controller = |
| GetApplicationContext()->GetOnDeviceModelServiceController( |
| on_device_model_state_manager_->GetWeakPtr()); |
| on_device_asset_manager_ = |
| std::make_unique<optimization_guide::OnDeviceAssetManager>( |
| local_state, on_device_model_service_controller->GetWeakPtr(), |
| on_device_model_state_manager_->GetWeakPtr(), this); |
| model_execution_manager_ = |
| std::make_unique<optimization_guide::ModelExecutionManager>( |
| url_loader_factory, identity_manager, |
| std::move(on_device_model_service_controller), |
| optimization_guide_logger_.get(), nullptr); |
| #else // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| model_execution_manager_ = |
| std::make_unique<optimization_guide::ModelExecutionManager>( |
| url_loader_factory, identity_manager, nullptr, |
| optimization_guide_logger_.get(), nullptr); |
| #endif // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| } |
| |
| // Some previous paths were written in incorrect locations. Delete the |
| // old paths. |
| // |
| // TODO(crbug.com/40842340): Remove this code in 05/2023 since it should be |
| // assumed that all clients that had the previous path have had their previous |
| // stores deleted. |
| DeleteOldStorePaths(profile_path); |
| |
| OPTIMIZATION_GUIDE_LOG( |
| optimization_guide_common::mojom::LogSource::SERVICE_AND_SETTINGS, |
| optimization_guide_logger_, |
| "OptimizationGuide: KeyedService is initalized"); |
| |
| optimization_guide::LogFeatureFlagsInfo(optimization_guide_logger_.get(), |
| off_the_record_, pref_service); |
| } |
| |
| OptimizationGuideService::~OptimizationGuideService() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void OptimizationGuideService::DoFinalInit( |
| download::BackgroundDownloadService* background_download_service) { |
| if (!off_the_record_) { |
| bool optimization_guide_fetching_enabled = |
| optimization_guide::IsUserPermittedToFetchFromRemoteOptimizationGuide( |
| off_the_record_, pref_service_); |
| base::UmaHistogramBoolean("OptimizationGuide.RemoteFetchingEnabled", |
| optimization_guide_fetching_enabled); |
| IOSChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial( |
| "SyntheticOptimizationGuideRemoteFetching", |
| optimization_guide_fetching_enabled ? "Enabled" : "Disabled", |
| variations::SyntheticTrialAnnotationMode::kCurrentLog); |
| if (background_download_service) { |
| prediction_manager_->MaybeInitializeModelDownloads( |
| background_download_service); |
| } |
| } |
| } |
| |
| optimization_guide::HintsManager* OptimizationGuideService::GetHintsManager() { |
| return hints_manager_.get(); |
| } |
| |
| optimization_guide::PredictionManager* |
| OptimizationGuideService::GetPredictionManager() { |
| return prediction_manager_.get(); |
| } |
| |
| void OptimizationGuideService::AddHintForTesting( |
| const GURL& url, |
| optimization_guide::proto::OptimizationType optimization_type, |
| const std::optional<optimization_guide::OptimizationMetadata>& metadata) { |
| hints_manager_->AddHintForTesting(url, optimization_type, // IN-TEST |
| metadata); |
| } |
| |
| void OptimizationGuideService::OnNavigationStartOrRedirect( |
| OptimizationGuideNavigationData* navigation_data) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(navigation_data); |
| |
| base::flat_set<optimization_guide::proto::OptimizationType> |
| registered_optimization_types = |
| hints_manager_->registered_optimization_types(); |
| if (!registered_optimization_types.empty()) { |
| hints_manager_->OnNavigationStartOrRedirect(navigation_data, |
| base::DoNothing()); |
| } |
| |
| navigation_data->set_registered_optimization_types( |
| hints_manager_->registered_optimization_types()); |
| } |
| |
| void OptimizationGuideService::OnNavigationFinish( |
| const std::vector<GURL>& navigation_redirect_chain) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| hints_manager_->OnNavigationFinish(navigation_redirect_chain); |
| } |
| |
| void OptimizationGuideService::RegisterOptimizationTypes( |
| const std::vector<optimization_guide::proto::OptimizationType>& |
| optimization_types) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| hints_manager_->RegisterOptimizationTypes(optimization_types); |
| } |
| |
| void OptimizationGuideService::CanApplyOptimization( |
| const GURL& url, |
| optimization_guide::proto::OptimizationType optimization_type, |
| optimization_guide::OptimizationGuideDecisionCallback callback) { |
| hints_manager_->CanApplyOptimization(url, optimization_type, |
| std::move(callback)); |
| } |
| |
| optimization_guide::OptimizationGuideDecision |
| OptimizationGuideService::CanApplyOptimization( |
| const GURL& url, |
| optimization_guide::proto::OptimizationType optimization_type, |
| optimization_guide::OptimizationMetadata* optimization_metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| optimization_guide::OptimizationTypeDecision optimization_type_decision = |
| hints_manager_->CanApplyOptimization(url, optimization_type, |
| optimization_metadata); |
| base::UmaHistogramEnumeration( |
| "OptimizationGuide.ApplyDecision." + |
| optimization_guide::GetStringNameForOptimizationType( |
| optimization_type), |
| optimization_type_decision); |
| return optimization_guide::HintsManager:: |
| GetOptimizationGuideDecisionFromOptimizationTypeDecision( |
| optimization_type_decision); |
| } |
| |
| void OptimizationGuideService::CanApplyOptimizationOnDemand( |
| const std::vector<GURL>& urls, |
| const base::flat_set<optimization_guide::proto::OptimizationType>& |
| optimization_types, |
| optimization_guide::proto::RequestContext request_context, |
| optimization_guide::OnDemandOptimizationGuideDecisionRepeatingCallback |
| callback, |
| std::optional<optimization_guide::proto::RequestContextMetadata> |
| request_context_metadata) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(request_context != |
| optimization_guide::proto::RequestContext::CONTEXT_UNSPECIFIED); |
| |
| hints_manager_->CanApplyOptimizationOnDemand( |
| urls, optimization_types, request_context, callback, std::nullopt); |
| } |
| |
| void OptimizationGuideService::Shutdown() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| hints_manager_->Shutdown(); |
| } |
| |
| void OptimizationGuideService::OnBrowsingDataRemoved() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| hints_manager_->ClearFetchedHints(); |
| } |
| |
| std::string OptimizationGuideService::ResponseForErrorCode(int error_code) { |
| ModelExecutionError model_execution_error = |
| static_cast<ModelExecutionError>(error_code); |
| switch (model_execution_error) { |
| case ModelExecutionError::kUnknown: |
| return "Unknown error (error code 0)"; |
| case ModelExecutionError::kInvalidRequest: |
| return "Invalid request (error code 1)"; |
| case ModelExecutionError::kRequestThrottled: |
| return "Request throttled (error code 2)"; |
| case ModelExecutionError::kPermissionDenied: |
| return "Permission denied (error code 3)"; |
| case ModelExecutionError::kGenericFailure: |
| return "Generic failure (error code 4)"; |
| case ModelExecutionError::kRetryableError: |
| return "Retryable error in server (error code 5)"; |
| case ModelExecutionError::kNonRetryableError: |
| return "Non-retryable error in server (error code 6)"; |
| case ModelExecutionError::kUnsupportedLanguage: |
| return "Unsupported language (error code 7)"; |
| case ModelExecutionError::kFiltered: |
| return "Request was filtered (error code 8)"; |
| case ModelExecutionError::kDisabled: |
| return "Response was disabled (error code 9)"; |
| case ModelExecutionError::kCancelled: |
| return "Response was cancelled (error code 10)"; |
| case ModelExecutionError::kResponseLowQuality: |
| return "Low quality response (error code 11)"; |
| } |
| } |
| |
| #pragma mark - optimization_guide::OptimizationGuideModelProvider implementation |
| |
| void OptimizationGuideService::AddObserverForOptimizationTargetModel( |
| optimization_guide::proto::OptimizationTarget optimization_target, |
| const std::optional<optimization_guide::proto::Any>& model_metadata, |
| optimization_guide::OptimizationTargetModelObserver* observer) { |
| if (optimization_guide::features::IsOptimizationTargetPredictionEnabled()) { |
| prediction_manager_->AddObserverForOptimizationTargetModel( |
| optimization_target, model_metadata, observer); |
| } |
| } |
| |
| void OptimizationGuideService::RemoveObserverForOptimizationTargetModel( |
| optimization_guide::proto::OptimizationTarget optimization_target, |
| optimization_guide::OptimizationTargetModelObserver* observer) { |
| if (optimization_guide::features::IsOptimizationTargetPredictionEnabled()) { |
| prediction_manager_->RemoveObserverForOptimizationTargetModel( |
| optimization_target, observer); |
| } |
| } |
| |
| #pragma mark - optimization_guide::OptimizationGuideModelExecutor implementation |
| |
| std::unique_ptr<optimization_guide::OptimizationGuideModelExecutor::Session> |
| OptimizationGuideService::StartSession( |
| optimization_guide::ModelBasedCapabilityKey feature, |
| const std::optional<optimization_guide::SessionConfigParams>& |
| config_params) { |
| if (!model_execution_manager_) { |
| return nullptr; |
| } |
| return model_execution_manager_->StartSession(feature, config_params); |
| } |
| |
| void OptimizationGuideService::ExecuteModel( |
| optimization_guide::ModelBasedCapabilityKey feature, |
| const google::protobuf::MessageLite& request_metadata, |
| const std::optional<base::TimeDelta>& execution_timeout, |
| optimization_guide::OptimizationGuideModelExecutionResultCallback |
| callback) { |
| DCHECK_CURRENTLY_ON(web::WebThread::UI); |
| if (!model_execution_manager_) { |
| std::move(callback).Run( |
| optimization_guide::OptimizationGuideModelExecutionResult( |
| base::unexpected( |
| optimization_guide::OptimizationGuideModelExecutionError:: |
| FromModelExecutionError( |
| optimization_guide:: |
| OptimizationGuideModelExecutionError:: |
| ModelExecutionError::kGenericFailure)), |
| /*model_execution_info=*/nullptr), |
| nullptr); |
| return; |
| } |
| model_execution_manager_->ExecuteModel( |
| feature, request_metadata, execution_timeout, |
| /*log_ai_data_request=*/nullptr, std::move(callback)); |
| } |
| |
| #if BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |
| |
| void OptimizationGuideService::AddOnDeviceModelAvailabilityChangeObserver( |
| optimization_guide::ModelBasedCapabilityKey feature, |
| optimization_guide::OnDeviceModelAvailabilityObserver* observer) { |
| if (!on_device_model_state_manager_) { |
| return; |
| } |
| optimization_guide::OnDeviceModelServiceController* service_controller = |
| GetApplicationContext()->GetOnDeviceModelServiceController( |
| on_device_model_state_manager_->GetWeakPtr()); |
| if (service_controller) { |
| service_controller->AddOnDeviceModelAvailabilityChangeObserver(feature, |
| observer); |
| } |
| } |
| |
| void OptimizationGuideService::RemoveOnDeviceModelAvailabilityChangeObserver( |
| optimization_guide::ModelBasedCapabilityKey feature, |
| optimization_guide::OnDeviceModelAvailabilityObserver* observer) { |
| if (!on_device_model_state_manager_) { |
| return; |
| } |
| optimization_guide::OnDeviceModelServiceController* service_controller = |
| GetApplicationContext()->GetOnDeviceModelServiceController( |
| on_device_model_state_manager_->GetWeakPtr()); |
| if (service_controller) { |
| service_controller->RemoveOnDeviceModelAvailabilityChangeObserver(feature, |
| observer); |
| } |
| } |
| |
| #pragma mark - optimization_guide::OptimizationGuideOnDeviceCapabilityProvider implementation |
| |
| optimization_guide::OnDeviceModelEligibilityReason |
| OptimizationGuideService::GetOnDeviceModelEligibility( |
| optimization_guide::ModelBasedCapabilityKey feature) { |
| if (!model_execution_manager_) { |
| return optimization_guide::OnDeviceModelEligibilityReason:: |
| kFeatureNotEnabled; |
| } |
| |
| return model_execution_manager_->GetOnDeviceModelEligibility(feature); |
| } |
| |
| std::optional<optimization_guide::SamplingParamsConfig> |
| OptimizationGuideService::GetSamplingParamsConfig( |
| optimization_guide::ModelBasedCapabilityKey feature) { |
| if (!model_execution_manager_) { |
| return std::nullopt; |
| } |
| |
| return model_execution_manager_->GetSamplingParamsConfig(feature); |
| } |
| |
| std::optional<const optimization_guide::proto::Any> |
| OptimizationGuideService::GetFeatureMetadata( |
| optimization_guide::ModelBasedCapabilityKey feature) { |
| if (!model_execution_manager_) { |
| return std::nullopt; |
| } |
| |
| return model_execution_manager_->GetFeatureMetadata(feature); |
| } |
| #endif // BUILDFLAG(BUILD_WITH_INTERNAL_OPTIMIZATION_GUIDE) |