blob: 482bc3c418bd9d46e2815f9992a736cc90edb4ef [file] [log] [blame]
// Copyright 2018 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_hints.h"
#include <unordered_set>
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/optional.h"
#include "base/strings/stringprintf.h"
#include "components/optimization_guide/hints_component_info.h"
#include "components/optimization_guide/hints_component_util.h"
#include "components/previews/core/bloom_filter.h"
#include "components/previews/core/previews_features.h"
#include "components/previews/core/previews_switches.h"
#include "url/gurl.h"
namespace previews {
namespace {
// Name of sentinel file to guard potential crash loops while processing
// the config into hints. It holds the version of the config that is/was
// being processed into hints.
const base::FilePath::CharType kSentinelFileName[] =
FILE_PATH_LITERAL("previews_config_sentinel.txt");
// Creates the sentinel file (at |sentinel_path|) to persistently mark the
// beginning of processing the configuration data for Previews hints. It
// records the configuration version in the file. Returns true when the
// sentinel file is successfully created and processing should continue.
// Returns false if the processing should not continue because the
// file exists with the same version (indicating that processing that version
// failed previously (possibly crash or shutdown). Should be run in the
// background (e.g., same task as PreviewsHints::Create()).
bool CreateSentinelFile(const base::FilePath& sentinel_path,
const base::Version& version) {
DCHECK(version.IsValid());
if (base::PathExists(sentinel_path)) {
// Processing apparently did not complete previously, check its version.
std::string content;
if (!base::ReadFileToString(sentinel_path, &content)) {
DLOG(WARNING) << "Error reading previews config sentinel file";
// Attempt to delete sentinel for fresh start next time.
base::DeleteFile(sentinel_path, false /* recursive */);
return false;
}
base::Version previous_attempted_version(content);
if (!previous_attempted_version.IsValid()) {
DLOG(ERROR) << "Bad contents in previews config sentinel file";
// Attempt to delete sentinel for fresh start next time.
base::DeleteFile(sentinel_path, false /* recursive */);
return false;
}
if (previous_attempted_version.CompareTo(version) == 0) {
// Previously attempted same version without completion.
return false;
}
}
// Write config version in the sentinel file.
std::string new_sentinel_value = version.GetString();
if (base::WriteFile(sentinel_path, new_sentinel_value.data(),
new_sentinel_value.length()) <= 0) {
DLOG(ERROR) << "Failed to create sentinel file " << sentinel_path;
return false;
}
return true;
}
// Deletes the sentinel file. This should be done once processing the
// configuration is complete and should be done in the background (e.g.,
// same task as Hints.CreateFromConfig).
void DeleteSentinelFile(const base::FilePath& sentinel_path) {
if (!base::DeleteFile(sentinel_path, false /* recursive */)) {
DLOG(ERROR) << "Error deleting sentinel file";
}
}
// Enumerates the possible outcomes of processing previews hints. Used in UMA
// histograms, so the order of enumerators should not be changed.
//
// Keep in sync with PreviewsProcessHintsResult in
// tools/metrics/histograms/enums.xml.
enum class PreviewsProcessHintsResult {
kProcessedNoPreviewsHints = 0,
kProcessedPreviewsHints = 1,
kFailedFinishProcessing = 2,
kSkippedProcessingPreviewsHints = 3,
kMaxValue = kSkippedProcessingPreviewsHints
};
// Enumerates status event of processing optimization filters (such as the
// lite page redirect blacklist). Used in UMA histograms, so the order of
// enumerators should not be changed.
//
// Keep in sync with PreviewsOptimizationFilterStatus in
// tools/metrics/histograms/enums.xml.
enum class PreviewsOptimizationFilterStatus {
kFoundServerBlacklistConfig = 0,
kCreatedServerBlacklist = 1,
kFailedServerBlacklistBadConfig = 2,
kFailedServerBlacklistTooBig = 3,
kFailedServerBlacklistDuplicateConfig = 4,
kMaxValue = kFailedServerBlacklistDuplicateConfig
};
// Returns base::nullopt if |optimization_type| can't be converted.
base::Optional<PreviewsType> ConvertProtoOptimizationTypeToPreviewsType(
optimization_guide::proto::OptimizationType optimization_type) {
switch (optimization_type) {
case optimization_guide::proto::TYPE_UNSPECIFIED:
return base::nullopt;
case optimization_guide::proto::NOSCRIPT:
return PreviewsType::NOSCRIPT;
case optimization_guide::proto::RESOURCE_LOADING:
return PreviewsType::RESOURCE_LOADING_HINTS;
case optimization_guide::proto::LITE_PAGE_REDIRECT:
return PreviewsType::LITE_PAGE_REDIRECT;
}
}
// Returns whether the optimization type is enabled on this client.
bool IsEnabledOptimizationType(
optimization_guide::proto::OptimizationType optimization_type) {
switch (optimization_type) {
case optimization_guide::proto::TYPE_UNSPECIFIED:
return false;
case optimization_guide::proto::NOSCRIPT:
return previews::params::IsNoScriptPreviewsEnabled();
case optimization_guide::proto::RESOURCE_LOADING:
return previews::params::IsResourceLoadingHintsEnabled();
case optimization_guide::proto::LITE_PAGE_REDIRECT:
return previews::params::IsLitePageServerPreviewsEnabled();
}
}
net::EffectiveConnectionType ConvertProtoEffectiveConnectionType(
optimization_guide::proto::EffectiveConnectionType proto_ect) {
switch (proto_ect) {
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_UNKNOWN:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_UNKNOWN;
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_OFFLINE:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_OFFLINE;
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_SLOW_2G:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_SLOW_2G;
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_2G:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_2G;
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_3G:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_3G;
case optimization_guide::proto::EffectiveConnectionType::
EFFECTIVE_CONNECTION_TYPE_4G:
return net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_4G;
}
}
PreviewsProcessHintsResult ProcessConfigurationHints(
optimization_guide::proto::Configuration* config,
HintCacheStore::ComponentUpdateData* component_update_data) {
DCHECK(config);
// If there's no component update data, then there's nothing to do. This
// component is not newer than the one contained within the hint cache.
if (!component_update_data) {
return PreviewsProcessHintsResult::kSkippedProcessingPreviewsHints;
}
std::unordered_set<std::string> seen_host_suffixes;
size_t total_processed_hints_with_page_hints = 0;
// Process each hint in the the hint configuration. The hints are mutable
// because once processing is completed on each individual hint, it is moved
// into the component update data. This eliminates the need to make any
// additional copies of the hints.
for (auto& hint : *(config->mutable_hints())) {
// We only support host suffixes at the moment. Skip anything else.
// One |hint| applies to one host URL suffix.
if (hint.key_representation() != optimization_guide::proto::HOST_SUFFIX) {
continue;
}
const std::string& hint_key = hint.key();
// Validate configuration keys.
DCHECK(!hint_key.empty());
if (hint_key.empty()) {
continue;
}
auto seen_host_suffixes_iter = seen_host_suffixes.find(hint_key);
DCHECK(seen_host_suffixes_iter == seen_host_suffixes.end());
if (seen_host_suffixes_iter != seen_host_suffixes.end()) {
DLOG(WARNING) << "Received config with duplicate key";
continue;
}
seen_host_suffixes.insert(hint_key);
if (!hint.page_hints().empty()) {
++total_processed_hints_with_page_hints;
// Now that processing is finished on |hint|, move it into the component
// data.
// WARNING: Do not use |hint| after this call. Its contents will no
// longer be valid.
component_update_data->MoveHintIntoUpdateData(std::move(hint));
}
}
return total_processed_hints_with_page_hints > 0
? PreviewsProcessHintsResult::kProcessedPreviewsHints
: PreviewsProcessHintsResult::kProcessedNoPreviewsHints;
}
void RecordProcessHintsResult(PreviewsProcessHintsResult result) {
base::UmaHistogramEnumeration("Previews.ProcessHintsResult", result);
}
void RecordOptimizationFilterStatus(PreviewsType previews_type,
PreviewsOptimizationFilterStatus status) {
std::string histogram_name =
base::StringPrintf("Previews.OptimizationFilterStatus.%s",
GetStringNameForType(previews_type).c_str());
base::UmaHistogramEnumeration(histogram_name, status);
}
} // namespace
PreviewsHints::PreviewsHints(
std::unique_ptr<HintCacheStore::ComponentUpdateData> component_update_data)
: hint_cache_(nullptr),
component_update_data_(std::move(component_update_data)) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
PreviewsHints::~PreviewsHints() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
// static
std::unique_ptr<PreviewsHints> PreviewsHints::CreateFromHintsComponent(
const optimization_guide::HintsComponentInfo& info,
std::unique_ptr<HintCacheStore::ComponentUpdateData>
component_update_data) {
std::unique_ptr<optimization_guide::proto::Configuration> config =
ProcessHintsComponent(info);
if (!config) {
return nullptr;
}
base::FilePath sentinel_path(info.path.DirName().Append(kSentinelFileName));
if (!CreateSentinelFile(sentinel_path, info.version)) {
RecordProcessHintsResult(
PreviewsProcessHintsResult::kFailedFinishProcessing);
return nullptr;
}
std::unique_ptr<PreviewsHints> hints = CreateFromHintsConfiguration(
std::move(config), std::move(component_update_data));
// Completed processing hints data without crashing so clear sentinel.
DeleteSentinelFile(sentinel_path);
return hints;
}
// static
std::unique_ptr<PreviewsHints> PreviewsHints::CreateFromHintsConfiguration(
std::unique_ptr<optimization_guide::proto::Configuration> config,
std::unique_ptr<HintCacheStore::ComponentUpdateData>
component_update_data) {
// Process the hints within the configuration. This will move the hints from
// |config| into |component_update_data|.
PreviewsProcessHintsResult process_hints_result =
ProcessConfigurationHints(config.get(), component_update_data.get());
// Construct the PrevewsHints object with |component_update_data|, which will
// later be used to update the HintCache's component data during Initialize().
std::unique_ptr<PreviewsHints> hints(
new PreviewsHints(std::move(component_update_data)));
// Extract any supported large scale blacklists from the configuration.
hints->ParseOptimizationFilters(*config);
// Now that processing is complete, record the result.
RecordProcessHintsResult(process_hints_result);
return hints;
}
void PreviewsHints::Initialize(HintCache* hint_cache,
base::OnceClosure callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!hint_cache_);
DCHECK(hint_cache);
hint_cache_ = hint_cache;
if (component_update_data_) {
hint_cache_->UpdateComponentData(std::move(component_update_data_),
std::move(callback));
} else {
std::move(callback).Run();
}
}
void PreviewsHints::ParseOptimizationFilters(
const optimization_guide::proto::Configuration& config) {
for (const auto& blacklist : config.optimization_blacklists()) {
base::Optional<PreviewsType> previews_type =
ConvertProtoOptimizationTypeToPreviewsType(
blacklist.optimization_type());
if (previews_type == PreviewsType::LITE_PAGE_REDIRECT &&
previews::params::IsLitePageServerPreviewsEnabled() &&
blacklist.has_bloom_filter()) {
RecordOptimizationFilterStatus(
previews_type.value(),
PreviewsOptimizationFilterStatus::kFoundServerBlacklistConfig);
if (lite_page_redirect_blacklist_) {
DLOG(WARNING)
<< "Found multiple blacklist configs for LITE_PAGE_REDIRECT";
RecordOptimizationFilterStatus(
previews_type.value(), PreviewsOptimizationFilterStatus::
kFailedServerBlacklistDuplicateConfig);
continue;
}
const auto& bloom_filter_proto = blacklist.bloom_filter();
DCHECK_GT(bloom_filter_proto.num_hash_functions(), 0u);
DCHECK_GT(bloom_filter_proto.num_bits(), 0u);
DCHECK(bloom_filter_proto.has_data());
if (!bloom_filter_proto.has_data() ||
bloom_filter_proto.num_bits() <= 0 ||
bloom_filter_proto.num_bits() >
bloom_filter_proto.data().size() * 8) {
DLOG(ERROR) << "Bloom filter config issue";
RecordOptimizationFilterStatus(
previews_type.value(),
PreviewsOptimizationFilterStatus::kFailedServerBlacklistBadConfig);
continue;
}
if (static_cast<int>(bloom_filter_proto.num_bits()) >
previews::params::
LitePageRedirectPreviewMaxServerBlacklistByteSize() /
8) {
DLOG(ERROR) << "Bloom filter data exceeds maximum size of "
<< previews::params::PreviewServerLoadshedMaxSeconds()
<< " bytes";
RecordOptimizationFilterStatus(
previews_type.value(),
PreviewsOptimizationFilterStatus::kFailedServerBlacklistTooBig);
continue;
}
std::unique_ptr<BloomFilter> bloom_filter = std::make_unique<BloomFilter>(
bloom_filter_proto.num_hash_functions(),
bloom_filter_proto.num_bits(), bloom_filter_proto.data());
lite_page_redirect_blacklist_ =
std::make_unique<HostFilter>(std::move(bloom_filter));
RecordOptimizationFilterStatus(
previews_type.value(),
PreviewsOptimizationFilterStatus::kCreatedServerBlacklist);
}
}
}
bool PreviewsHints::IsWhitelisted(
const GURL& url,
PreviewsType type,
int* out_inflation_percent,
net::EffectiveConnectionType* out_ect_threshold) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(hint_cache_);
if (!url.has_host()) {
return false;
}
if (type == previews::PreviewsType::RESOURCE_LOADING_HINTS &&
!previews::params::IsResourceLoadingHintsEnabled()) {
return false;
}
// Now check HintCache for a loaded entry with a PageHint that matches |url|
// that whitelists the optimization.
const optimization_guide::proto::Hint* hint =
hint_cache_->GetHintIfLoaded(url.host());
if (!hint) {
// TODO(dougarnett): Add UMA histogram counts for both cases of HasHint().
return false;
}
const optimization_guide::proto::PageHint* matched_page_hint =
FindPageHintForURL(url, hint);
if (!matched_page_hint) {
return false;
}
for (const auto& optimization :
matched_page_hint->whitelisted_optimizations()) {
// Skip over any disabled experimental optimizations.
if (IsDisabledExperimentalOptimization(optimization)) {
continue;
}
if (!IsEnabledOptimizationType(optimization.optimization_type())) {
continue;
}
// Client should use this first whitelisted optimization it has enabled.
if (ConvertProtoOptimizationTypeToPreviewsType(
optimization.optimization_type()) != type) {
return false;
}
// |type| is the first whitelisted optimization this client supports.
*out_inflation_percent = optimization.inflation_percent();
if (matched_page_hint->has_max_ect_trigger()) {
*out_ect_threshold = ConvertProtoEffectiveConnectionType(
matched_page_hint->max_ect_trigger());
}
return true;
}
return false;
}
bool PreviewsHints::IsBlacklisted(const GURL& url, PreviewsType type) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!url.has_host()) {
return false;
}
// Check large scale blacklists received from the server.
// (At some point, we may have blacklisting to check in HintCache as well.)
if (type == PreviewsType::LITE_PAGE_REDIRECT) {
// If no bloom filter blacklist is provided by the component update, assume
// a server error and return true.
if (!lite_page_redirect_blacklist_) {
return true;
}
return lite_page_redirect_blacklist_->ContainsHostSuffix(url);
}
return false;
}
bool PreviewsHints::MaybeLoadOptimizationHints(
const GURL& url,
HintLoadedCallback callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(hint_cache_);
if (!url.has_host()) {
return false;
}
hint_cache_->LoadHint(url.host(), std::move(callback));
return hint_cache_->HasHint(url.host());
}
bool PreviewsHints::GetResourceLoadingHints(
const GURL& url,
std::vector<std::string>* out_resource_patterns_to_block) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(hint_cache_);
DCHECK(previews::params::IsResourceLoadingHintsEnabled());
// First find matched page hint.
const optimization_guide::proto::PageHint* matched_page_hint =
FindPageHintForURL(url, hint_cache_->GetHintIfLoaded(url.host()));
if (!matched_page_hint) {
return false;
}
// Now populate the resource patterns.
for (const auto& optimization :
matched_page_hint->whitelisted_optimizations()) {
if (optimization.optimization_type() !=
optimization_guide::proto::RESOURCE_LOADING) {
continue;
}
if (IsDisabledExperimentalOptimization(optimization)) {
continue;
}
for (const auto& resource_loading_hint :
optimization.resource_loading_hints()) {
if (!resource_loading_hint.resource_pattern().empty() &&
resource_loading_hint.loading_optimization_type() ==
optimization_guide::proto::LOADING_BLOCK_RESOURCE) {
out_resource_patterns_to_block->push_back(
resource_loading_hint.resource_pattern());
}
}
// Done - only use first whitelisted resource loading optimization.
return true;
}
return false;
}
void PreviewsHints::LogHintCacheMatch(const GURL& url,
bool is_committed,
net::EffectiveConnectionType ect) const {
DCHECK(hint_cache_);
if (hint_cache_->HasHint(url.host())) {
if (!is_committed) {
UMA_HISTOGRAM_ENUMERATION(
"Previews.OptimizationGuide.HintCache.HasHint.BeforeCommit", ect,
net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_LAST);
} else {
UMA_HISTOGRAM_ENUMERATION(
"Previews.OptimizationGuide.HintCache.HasHint.AtCommit", ect,
net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_LAST);
const optimization_guide::proto::Hint* hint =
hint_cache_->GetHintIfLoaded(url.host());
if (hint) {
UMA_HISTOGRAM_ENUMERATION(
"Previews.OptimizationGuide.HintCache.HostMatch.AtCommit", ect,
net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_LAST);
if (FindPageHintForURL(url, hint)) {
UMA_HISTOGRAM_ENUMERATION(
"Previews.OptimizationGuide.HintCache.PageMatch.AtCommit", ect,
net::EffectiveConnectionType::EFFECTIVE_CONNECTION_TYPE_LAST);
}
}
}
}
}
} // namespace previews