blob: 9d9c279a0c3520224213ae582af086fbf00fcedf [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/feed/core/feed_content_database.h"
#include <unordered_set>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "base/system/sys_info.h"
#include "base/task/post_task.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/feed/core/feed_content_mutation.h"
#include "components/feed/core/feed_content_operation.h"
#include "components/feed/core/proto/content_storage.pb.h"
#include "components/leveldb_proto/public/proto_database_provider.h"
namespace feed {
namespace {
using StorageEntryVector =
leveldb_proto::ProtoDatabase<ContentStorageProto>::KeyEntryVector;
const char kContentDatabaseFolder[] = "content";
const size_t kDatabaseWriteBufferSizeBytes = 64 * 1024; // 64KB
const size_t kDatabaseWriteBufferSizeBytesForLowEndDevice = 32 * 1024; // 32KB
leveldb::ReadOptions CreateReadOptions() {
leveldb::ReadOptions opts;
opts.fill_cache = false;
return opts;
}
bool DatabaseKeyFilter(const std::unordered_set<std::string>& key_set,
const std::string& key) {
return key_set.find(key) != key_set.end();
}
bool DatabasePrefixFilter(const std::string& key_prefix,
const std::string& key) {
return base::StartsWith(key, key_prefix, base::CompareCase::SENSITIVE);
}
} // namespace
FeedContentDatabase::FeedContentDatabase(
leveldb_proto::ProtoDatabaseProvider* proto_database_provider,
const base::FilePath& database_folder)
: FeedContentDatabase(proto_database_provider->GetDB<ContentStorageProto>(
leveldb_proto::ProtoDbType::FEED_CONTENT_DATABASE,
database_folder.AppendASCII(kContentDatabaseFolder),
base::CreateSequencedTaskRunnerWithTraits(
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}))) {}
FeedContentDatabase::FeedContentDatabase(
std::unique_ptr<leveldb_proto::ProtoDatabase<ContentStorageProto>>
storage_database)
: database_status_(InitStatus::kNotInitialized),
storage_database_(std::move(storage_database)),
weak_ptr_factory_(this) {
leveldb_env::Options options = leveldb_proto::CreateSimpleOptions();
options.write_buffer_size = base::SysInfo::IsLowEndDevice()
? kDatabaseWriteBufferSizeBytesForLowEndDevice
: kDatabaseWriteBufferSizeBytes;
storage_database_->Init(
options, base::BindOnce(&FeedContentDatabase::OnDatabaseInitialized,
weak_ptr_factory_.GetWeakPtr()));
}
FeedContentDatabase::~FeedContentDatabase() = default;
bool FeedContentDatabase::IsInitialized() const {
return database_status_ == InitStatus::kOK;
}
void FeedContentDatabase::LoadContent(const std::vector<std::string>& keys,
ContentLoadCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::unordered_set<std::string> key_set(keys.begin(), keys.end());
storage_database_->LoadEntriesWithFilter(
base::BindRepeating(&DatabaseKeyFilter, std::move(key_set)),
CreateReadOptions(), /* target_prefix */ "",
base::BindOnce(&FeedContentDatabase::OnLoadEntriesForLoadContent,
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now(),
std::move(callback)));
}
void FeedContentDatabase::LoadContentByPrefix(const std::string& prefix,
ContentLoadCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
storage_database_->LoadEntriesWithFilter(
base::BindRepeating(&DatabasePrefixFilter, std::move(prefix)),
CreateReadOptions(), /* target_prefix */ "",
base::BindOnce(&FeedContentDatabase::OnLoadEntriesForLoadContent,
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now(),
std::move(callback)));
}
void FeedContentDatabase::LoadAllContentKeys(ContentKeyCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
storage_database_->LoadKeys(
base::BindOnce(&FeedContentDatabase::OnLoadKeysForLoadAllContentKeys,
weak_ptr_factory_.GetWeakPtr(), base::TimeTicks::Now(),
std::move(callback)));
}
void FeedContentDatabase::CommitContentMutation(
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(content_mutation);
UMA_HISTOGRAM_COUNTS_100(
"ContentSuggestions.Feed.ContentStorage.CommitMutationCount",
content_mutation->Size());
if (content_mutation->Empty()) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), true));
return;
}
PerformNextOperation(std::move(content_mutation), std::move(callback));
}
void FeedContentDatabase::PerformNextOperation(
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK(!content_mutation->Empty());
ContentOperation operation = content_mutation->TakeFristOperation();
switch (operation.type()) {
case ContentOperation::CONTENT_DELETE:
// TODO(gangwu): If deletes are continuous, we should combine them into
// one commit.
DeleteContent(std::move(operation), std::move(content_mutation),
std::move(callback));
break;
case ContentOperation::CONTENT_DELETE_BY_PREFIX:
DeleteContentByPrefix(std::move(operation), std::move(content_mutation),
std::move(callback));
break;
case ContentOperation::CONTENT_UPSERT:
// TODO(gangwu): If upserts are continuous, we should combine them into
// one commit.
UpsertContent(std::move(operation), std::move(content_mutation),
std::move(callback));
break;
case ContentOperation::CONTENT_DELETE_ALL:
DeleteAllContent(std::move(operation), std::move(content_mutation),
std::move(callback));
break;
default:
// Operation type is not supported, therefore failing immediately.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), false));
}
}
void FeedContentDatabase::UpsertContent(
ContentOperation operation,
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK_EQ(operation.type(), ContentOperation::CONTENT_UPSERT);
auto contents_to_save = std::make_unique<StorageEntryVector>();
ContentStorageProto proto;
proto.set_key(operation.key());
proto.set_content_data(operation.value());
contents_to_save->emplace_back(proto.key(), std::move(proto));
storage_database_->UpdateEntries(
std::move(contents_to_save), std::make_unique<std::vector<std::string>>(),
base::BindOnce(&FeedContentDatabase::OnOperationCommitted,
weak_ptr_factory_.GetWeakPtr(),
std::move(content_mutation), std::move(callback)));
}
void FeedContentDatabase::DeleteContent(
ContentOperation operation,
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK_EQ(operation.type(), ContentOperation::CONTENT_DELETE);
auto content_to_delete = std::make_unique<std::vector<std::string>>(
std::initializer_list<std::string>({operation.key()}));
storage_database_->UpdateEntries(
std::make_unique<StorageEntryVector>(), std::move(content_to_delete),
base::BindOnce(&FeedContentDatabase::OnOperationCommitted,
weak_ptr_factory_.GetWeakPtr(),
std::move(content_mutation), std::move(callback)));
}
void FeedContentDatabase::DeleteContentByPrefix(
ContentOperation operation,
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK_EQ(operation.type(), ContentOperation::CONTENT_DELETE_BY_PREFIX);
storage_database_->UpdateEntriesWithRemoveFilter(
std::make_unique<StorageEntryVector>(),
base::BindRepeating(&DatabasePrefixFilter, operation.prefix()),
base::BindOnce(&FeedContentDatabase::OnOperationCommitted,
weak_ptr_factory_.GetWeakPtr(),
std::move(content_mutation), std::move(callback)));
}
void FeedContentDatabase::DeleteAllContent(
ContentOperation operation,
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback) {
DCHECK_EQ(operation.type(), ContentOperation::CONTENT_DELETE_ALL);
std::string key_prefix = "";
storage_database_->UpdateEntriesWithRemoveFilter(
std::make_unique<StorageEntryVector>(),
base::BindRepeating(&DatabasePrefixFilter, std::move(key_prefix)),
base::BindOnce(&FeedContentDatabase::OnOperationCommitted,
weak_ptr_factory_.GetWeakPtr(),
std::move(content_mutation), std::move(callback)));
}
void FeedContentDatabase::OnDatabaseInitialized(InitStatus status) {
DCHECK_EQ(database_status_, InitStatus::kNotInitialized);
database_status_ = status;
}
void FeedContentDatabase::OnLoadEntriesForLoadContent(
base::TimeTicks start_time,
ContentLoadCallback callback,
bool success,
std::unique_ptr<std::vector<ContentStorageProto>> content) {
base::TimeDelta load_time = base::TimeTicks::Now() - start_time;
UMA_HISTOGRAM_TIMES("ContentSuggestions.Feed.ContentStorage.LoadTime",
load_time);
std::vector<KeyAndData> results;
if (success) {
for (const auto& proto : *content) {
DCHECK(proto.has_key());
DCHECK(proto.has_content_data());
results.emplace_back(proto.key(), proto.content_data());
}
}
std::move(callback).Run(success, std::move(results));
}
void FeedContentDatabase::OnLoadKeysForLoadAllContentKeys(
base::TimeTicks start_time,
ContentKeyCallback callback,
bool success,
std::unique_ptr<std::vector<std::string>> keys) {
if (success) {
// Typical usage has a max around 300(100 cards, 3 pieces of content per
// card), could grow forever through heavy usage of dismiss. If typically
// usage changes, 1000 maybe too small.
UMA_HISTOGRAM_COUNTS_1000("ContentSuggestions.Feed.ContentStorage.Count",
keys->size());
}
base::TimeDelta load_time = base::TimeTicks::Now() - start_time;
UMA_HISTOGRAM_TIMES("ContentSuggestions.Feed.ContentStorage.LoadKeysTime",
load_time);
// We std::move the |*keys|'s entries to |callback|, after that, |keys| become
// a pointer holding an empty vector, then we can safely delete unique_ptr
// |keys| when it out of scope.
std::move(callback).Run(success, std::move(*keys));
}
void FeedContentDatabase::OnOperationCommitted(
std::unique_ptr<ContentMutation> content_mutation,
ConfirmationCallback callback,
bool success) {
// Commit is unsuccessful, skip processing the other operations since
// ContentStorage.java requires "In the event of a failure, processing is
// halted immediately".
if (!success) {
std::move(callback).Run(success);
return;
}
// All operations were committed successfully, call |callback|.
if (content_mutation->Empty()) {
base::TimeDelta commit_time =
base::TimeTicks::Now() - content_mutation->GetStartTime();
UMA_HISTOGRAM_TIMES(
"ContentSuggestions.Feed.ContentStorage.OperationCommitTime",
commit_time);
std::move(callback).Run(success);
return;
}
PerformNextOperation(std::move(content_mutation), std::move(callback));
}
} // namespace feed