blob: 49e7cd1eaab36bf4ab8142839f9454c54c25f54e [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/previews/content/previews_optimization_guide_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/rand_util.h"
#include "base/task/post_task.h"
#include "base/task_runner_util.h"
#include "base/time/default_clock.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h"
#include "components/optimization_guide/hint_cache_store.h"
#include "components/optimization_guide/hints_component_info.h"
#include "components/optimization_guide/hints_component_util.h"
#include "components/optimization_guide/hints_fetcher.h"
#include "components/optimization_guide/optimization_guide_constants.h"
#include "components/optimization_guide/optimization_guide_features.h"
#include "components/optimization_guide/optimization_guide_prefs.h"
#include "components/optimization_guide/optimization_guide_service.h"
#include "components/optimization_guide/optimization_guide_switches.h"
#include "components/optimization_guide/proto/hints.pb.h"
#include "components/optimization_guide/top_host_provider.h"
#include "components/prefs/pref_service.h"
#include "components/previews/content/previews_hints.h"
#include "components/previews/content/previews_user_data.h"
#include "components/previews/core/previews_switches.h"
#include "content/public/browser/navigation_handle.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "url/gurl.h"
namespace previews {
namespace {
// The component version used with a manual config. This ensures that any hint
// component received from the OptimizationGuideService on a subsequent startup
// will have a newer version than it.
constexpr char kManualConfigComponentVersion[] = "0.0.0";
// Delay between retries on failed fetch and store of hints from the remote
// Optimization Guide Service.
constexpr base::TimeDelta kFetchRetryDelay = base::TimeDelta::FromMinutes(15);
// Delay until successfully fetched hints should be updated by requesting from
// the remote Optimization Guide Service.
constexpr base::TimeDelta kUpdateFetchedHintsDelay =
base::TimeDelta::FromHours(24);
// Provides a random time delta in seconds between |kFetchRandomMinDelay| and
// |kFetchRandomMaxDelay|.
base::TimeDelta RandomFetchDelay() {
constexpr int kFetchRandomMinDelaySecs = 30;
constexpr int kFetchRandomMaxDelaySecs = 60;
return base::TimeDelta::FromSeconds(
base::RandInt(kFetchRandomMinDelaySecs, kFetchRandomMaxDelaySecs));
}
} // namespace
PreviewsOptimizationGuideImpl::PreviewsOptimizationGuideImpl(
optimization_guide::OptimizationGuideService* optimization_guide_service,
const scoped_refptr<base::SingleThreadTaskRunner>& ui_task_runner,
const scoped_refptr<base::SequencedTaskRunner>& background_task_runner,
const base::FilePath& profile_path,
PrefService* pref_service,
leveldb_proto::ProtoDatabaseProvider* database_provider,
optimization_guide::TopHostProvider* top_host_provider,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
network::NetworkQualityTracker* network_quality_tracker)
: optimization_guide_service_(optimization_guide_service),
ui_task_runner_(ui_task_runner),
background_task_runner_(background_task_runner),
hint_cache_(std::make_unique<optimization_guide::HintCache>(
std::make_unique<optimization_guide::HintCacheStore>(
database_provider,
profile_path,
background_task_runner_))),
top_host_provider_(top_host_provider),
time_clock_(base::DefaultClock::GetInstance()),
pref_service_(pref_service),
url_loader_factory_(url_loader_factory),
network_quality_tracker_(network_quality_tracker) {
DCHECK(optimization_guide_service_);
network_quality_tracker_->AddEffectiveConnectionTypeObserver(this);
hint_cache_->Initialize(
optimization_guide::switches::ShouldPurgeHintCacheStoreOnStartup(),
base::BindOnce(&PreviewsOptimizationGuideImpl::OnHintCacheInitialized,
ui_weak_ptr_factory_.GetWeakPtr()));
}
PreviewsOptimizationGuideImpl::~PreviewsOptimizationGuideImpl() {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
optimization_guide_service_->RemoveObserver(this);
network_quality_tracker_->RemoveEffectiveConnectionTypeObserver(this);
}
bool PreviewsOptimizationGuideImpl::IsReady() const {
return !!hints_;
}
bool PreviewsOptimizationGuideImpl::CanApplyPreview(
PreviewsUserData* previews_data,
content::NavigationHandle* navigation_handle,
PreviewsType type) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
if (!hints_) {
return false;
}
// Check if LITE_PAGE_REDIRECT is blacklisted or not.
if (type == PreviewsType::LITE_PAGE_REDIRECT) {
if (current_effective_connection_type_ >
params::GetECTThresholdForPreview(type)) {
return false;
}
if (params::LitePageRedirectPreviewIgnoresOptimizationGuideFilter()) {
return true;
}
return !hints_->IsBlacklisted(navigation_handle->GetURL(),
PreviewsType::LITE_PAGE_REDIRECT);
}
// Check other previews.
int inflation_percent = 0;
net::EffectiveConnectionType out_ect_threshold =
params::GetECTThresholdForPreview(type);
std::string serialized_hint_version_string;
if (!hints_->IsWhitelisted(navigation_handle->GetURL(), type,
&inflation_percent, &out_ect_threshold,
&serialized_hint_version_string)) {
return false;
}
// Also check the hint's ECT threshold against the current ECT if the type
// can be applied.
if (current_effective_connection_type_ > out_ect_threshold) {
return false;
}
if (inflation_percent != 0 && previews_data) {
previews_data->set_data_savings_inflation_percent(inflation_percent);
}
if (!serialized_hint_version_string.empty() && previews_data) {
previews_data->set_serialized_hint_version_string(
serialized_hint_version_string);
}
return true;
}
void PreviewsOptimizationGuideImpl::OnLoadedHint(
base::OnceClosure callback,
const GURL& document_url,
const optimization_guide::proto::Hint* loaded_hint) const {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
// Record that the hint finished loading. This is used as a signal during
// tests.
LOCAL_HISTOGRAM_BOOLEAN(optimization_guide::kLoadedHintLocalHistogramString,
loaded_hint);
// Run the callback now that the hint is loaded. This is used as a signal by
// tests.
std::move(callback).Run();
}
bool PreviewsOptimizationGuideImpl::MaybeLoadOptimizationHints(
content::NavigationHandle* navigation_handle,
base::OnceClosure callback) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
if (!hints_) {
return false;
}
const GURL& url = navigation_handle->GetURL();
return hints_->MaybeLoadOptimizationHints(
url, base::BindOnce(&PreviewsOptimizationGuideImpl::OnLoadedHint,
ui_weak_ptr_factory_.GetWeakPtr(),
std::move(callback), url));
}
bool PreviewsOptimizationGuideImpl::GetResourceLoadingHints(
const GURL& url,
std::vector<std::string>* out_resource_patterns_to_block) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
if (!hints_)
return false;
return hints_->GetResourceLoadingHints(url, out_resource_patterns_to_block);
}
void PreviewsOptimizationGuideImpl::LogHintCacheMatch(const GURL& url,
bool is_committed) const {
if (!hints_) {
return;
}
hints_->LogHintCacheMatch(url, is_committed);
}
void PreviewsOptimizationGuideImpl::OnHintCacheInitialized() {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
// Check if there is a valid hint proto given on the command line first. We
// don't normally expect one, but if one is provided then use that and do not
// register as an observer as the opt_guide service.
std::unique_ptr<optimization_guide::proto::Configuration> manual_config =
optimization_guide::switches::ParseComponentConfigFromCommandLine();
if (manual_config) {
// Allow |UpdateHints| to block startup so that the first navigation gets
// the hints when a command line hint proto is provided.
UpdateHints(base::OnceClosure(),
PreviewsHints::CreateFromHintsConfiguration(
std::move(manual_config),
hint_cache_->MaybeCreateUpdateDataForComponentHints(
base::Version(kManualConfigComponentVersion))));
}
// Register as an observer regardless of hint proto override usage. This is
// needed as a signal during testing.
optimization_guide_service_->AddObserver(this);
}
void PreviewsOptimizationGuideImpl::OnHintsComponentAvailable(
const optimization_guide::HintsComponentInfo& info) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
// Check for if hint component is disabled. This check is needed because the
// optimization guide still registers with the service as an observer for
// components as a signal during testing.
if (optimization_guide::switches::IsHintComponentProcessingDisabled()) {
return;
}
// Create PreviewsHints from the newly available component on a background
// thread, providing a HintUpdateData for component update from the hint
// cache, so that each hint within the component can be moved into it. In the
// case where the component's version is not newer than the hint cache store's
// component version, HintUpdateData will be a nullptr and hint
// processing will be skipped. After PreviewsHints::Create() returns the newly
// created PreviewsHints, it is initialized in UpdateHints() on the UI thread.
base::PostTaskAndReplyWithResult(
background_task_runner_.get(), FROM_HERE,
base::BindOnce(
&PreviewsHints::CreateFromHintsComponent, info,
hint_cache_->MaybeCreateUpdateDataForComponentHints(info.version)),
base::BindOnce(&PreviewsOptimizationGuideImpl::UpdateHints,
ui_weak_ptr_factory_.GetWeakPtr(),
std::move(next_update_closure_)));
}
void PreviewsOptimizationGuideImpl::FetchHints() {
base::Optional<std::vector<std::string>> top_hosts =
optimization_guide::switches::ParseHintsFetchOverrideFromCommandLine();
if (!top_hosts) {
top_hosts = top_host_provider_->GetTopHosts(
optimization_guide::features::
MaxHostsForOptimizationGuideServiceHintsFetch());
}
DCHECK_GE(optimization_guide::features::
MaxHostsForOptimizationGuideServiceHintsFetch(),
top_hosts->size());
if (!hints_fetcher_) {
hints_fetcher_ = std::make_unique<optimization_guide::HintsFetcher>(
url_loader_factory_,
optimization_guide::features::GetOptimizationGuideServiceURL(),
pref_service_);
}
if (top_hosts->size() > 0) {
hints_fetcher_->FetchOptimizationGuideServiceHints(
*top_hosts,
base::BindOnce(&PreviewsOptimizationGuideImpl::OnHintsFetched,
ui_weak_ptr_factory_.GetWeakPtr()));
}
}
void PreviewsOptimizationGuideImpl::OnHintsFetched(
base::Optional<std::unique_ptr<optimization_guide::proto::GetHintsResponse>>
get_hints_response) {
// TODO(mcrouse): this will be dropped into a backgroundtask as it will likely
// be intensive/slow storing hints.
if (get_hints_response) {
hint_cache_->UpdateFetchedHints(
std::move(*get_hints_response),
time_clock_->Now() + kUpdateFetchedHintsDelay,
base::BindOnce(&PreviewsOptimizationGuideImpl::OnFetchedHintsStored,
ui_weak_ptr_factory_.GetWeakPtr()));
} else {
// The fetch did not succeed so we will schedule to retry the fetch in
// after delaying for |kFetchRetryDelay|
// TODO(mcrouse): When the store is refactored from closures, the timer will
// be scheduled on failure of the store instead.
hints_fetch_timer_.Start(
FROM_HERE, kFetchRetryDelay, this,
&PreviewsOptimizationGuideImpl::ScheduleHintsFetch);
}
}
void PreviewsOptimizationGuideImpl::OnFetchedHintsStored() {
hints_fetch_timer_.Stop();
hints_fetch_timer_.Start(
FROM_HERE, hint_cache_->FetchedHintsUpdateTime() - time_clock_->Now(),
this, &PreviewsOptimizationGuideImpl::ScheduleHintsFetch);
// TODO(mcrouse): Purge hints now that new fetched hints have been stored.
}
void PreviewsOptimizationGuideImpl::UpdateHints(
base::OnceClosure update_closure,
std::unique_ptr<PreviewsHints> hints) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
hints_ = std::move(hints);
if (hints_) {
hints_->Initialize(
hint_cache_.get(),
base::BindOnce(&PreviewsOptimizationGuideImpl::OnHintsUpdated,
ui_weak_ptr_factory_.GetWeakPtr(),
std::move(update_closure)));
} else {
OnHintsUpdated(std::move(update_closure));
}
}
void PreviewsOptimizationGuideImpl::ClearFetchedHints() {
DCHECK(hint_cache_);
hint_cache_->ClearFetchedHints();
optimization_guide::HintsFetcher::ClearHostsSuccessfullyFetched(
pref_service_);
}
void PreviewsOptimizationGuideImpl::OnHintsUpdated(
base::OnceClosure update_closure) {
DCHECK(ui_task_runner_->BelongsToCurrentThread());
DCHECK(pref_service_);
// Record the result of updating the hints. This is used as a signal for the
// hints being fully processed in release tools and testing.
LOCAL_HISTOGRAM_BOOLEAN(
optimization_guide::kComponentHintsUpdatedResultHistogramString,
hints_ != nullptr);
if (!update_closure.is_null())
std::move(update_closure).Run();
// If the client is eligible to fetch hints, currently controlled by a feature
// flag |kOptimizationHintsFetching|, fetch hints from the remote Optimization
// Guide Service.
if (!data_reduction_proxy::DataReductionProxySettings::
IsDataSaverEnabledByUser(pref_service_)) {
return;
}
if (!optimization_guide::features::IsHintsFetchingEnabled())
return;
if (optimization_guide::switches::ParseHintsFetchOverrideFromCommandLine() ||
optimization_guide::switches::ShouldOverrideFetchHintsTimer()) {
// Skip the fetch scheduling logic and perform a hints fetch immediately
// after initialization.
SetLastHintsFetchAttemptTime(time_clock_->Now());
FetchHints();
} else {
ScheduleHintsFetch();
}
}
void PreviewsOptimizationGuideImpl::SetLastHintsFetchAttemptTime(
base::Time last_attempt_time) {
DCHECK(pref_service_);
pref_service_->SetInt64(
optimization_guide::prefs::kHintsFetcherLastFetchAttempt,
last_attempt_time.ToDeltaSinceWindowsEpoch().InMicroseconds());
}
base::Time PreviewsOptimizationGuideImpl::GetLastHintsFetchAttemptTime() const {
DCHECK(pref_service_);
return base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(pref_service_->GetInt64(
optimization_guide::prefs::kHintsFetcherLastFetchAttempt)));
}
void PreviewsOptimizationGuideImpl::ScheduleHintsFetch() {
DCHECK(!hints_fetch_timer_.IsRunning());
DCHECK(pref_service_);
if (!data_reduction_proxy::DataReductionProxySettings::
IsDataSaverEnabledByUser(pref_service_)) {
return;
}
const base::TimeDelta time_until_update_time =
hint_cache_->FetchedHintsUpdateTime() - time_clock_->Now();
const base::TimeDelta time_until_retry =
GetLastHintsFetchAttemptTime() + kFetchRetryDelay - time_clock_->Now();
base::TimeDelta fetcher_delay;
if (time_until_update_time <= base::TimeDelta() &&
time_until_retry <= base::TimeDelta()) {
// Fetched hints in the store should be updated and an attempt has not
// been made in last |kFetchRetryDelay|.
SetLastHintsFetchAttemptTime(time_clock_->Now());
hints_fetch_timer_.Start(FROM_HERE, RandomFetchDelay(), this,
&PreviewsOptimizationGuideImpl::FetchHints);
} else {
if (time_until_update_time >= base::TimeDelta()) {
// If the fetched hints in the store are still up-to-date, set a timer
// for when the hints need to be updated.
fetcher_delay = time_until_update_time;
} else {
// Otherwise, hints need to be updated but an attempt was made in last
// |kFetchRetryDelay|. Schedule the timer for after the retry
// delay.
fetcher_delay = time_until_retry;
}
hints_fetch_timer_.Start(
FROM_HERE, fetcher_delay, this,
&PreviewsOptimizationGuideImpl::ScheduleHintsFetch);
}
}
void PreviewsOptimizationGuideImpl::SetTimeClockForTesting(
const base::Clock* time_clock) {
time_clock_ = time_clock;
}
void PreviewsOptimizationGuideImpl::SetHintsFetcherForTesting(
std::unique_ptr<optimization_guide::HintsFetcher> hints_fetcher) {
hints_fetcher_ = std::move(hints_fetcher);
}
optimization_guide::HintsFetcher*
PreviewsOptimizationGuideImpl::GetHintsFetcherForTesting() {
return hints_fetcher_.get();
}
void PreviewsOptimizationGuideImpl::ListenForNextUpdateForTesting(
base::OnceClosure next_update_closure) {
DCHECK(next_update_closure_.is_null())
<< "Only one update closure is supported at a time";
next_update_closure_ = std::move(next_update_closure);
}
void PreviewsOptimizationGuideImpl::OnEffectiveConnectionTypeChanged(
net::EffectiveConnectionType effective_connection_type) {
current_effective_connection_type_ = effective_connection_type;
}
} // namespace previews