blob: 14c3c35f1e84f7047618c298273d88bdf094c7c3 [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/sync_bookmarks/synced_bookmark_tracker.h"
#include <set>
#include <utility>
#include "base/base64.h"
#include "base/sha1.h"
#include "base/stl_util.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/sync/base/time.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/model/entity_data.h"
#include "components/sync/protocol/proto_memory_estimations.h"
namespace sync_bookmarks {
namespace {
void HashSpecifics(const sync_pb::EntitySpecifics& specifics,
std::string* hash) {
DCHECK_GT(specifics.ByteSize(), 0);
base::Base64Encode(base::SHA1HashString(specifics.SerializeAsString()), hash);
}
} // namespace
SyncedBookmarkTracker::Entity::Entity(
const bookmarks::BookmarkNode* bookmark_node,
std::unique_ptr<sync_pb::EntityMetadata> metadata)
: bookmark_node_(bookmark_node), metadata_(std::move(metadata)) {
// TODO(crbug.com/516866): The below CHECK is added to debug some crashes.
// Should be removed after figuring out the reason for the crash.
CHECK(metadata_);
if (bookmark_node) {
DCHECK(!metadata_->is_deleted());
} else {
DCHECK(metadata_->is_deleted());
}
}
SyncedBookmarkTracker::Entity::~Entity() = default;
bool SyncedBookmarkTracker::Entity::IsUnsynced() const {
return metadata_->sequence_number() > metadata_->acked_sequence_number();
}
bool SyncedBookmarkTracker::Entity::MatchesDataIgnoringParent(
const syncer::EntityData& data) const {
if (metadata_->is_deleted() || data.is_deleted()) {
// In case of deletion, no need to check the specifics.
return metadata_->is_deleted() == data.is_deleted();
}
if (!syncer::UniquePosition::FromProto(metadata_->unique_position())
.Equals(syncer::UniquePosition::FromProto(data.unique_position))) {
return false;
}
return MatchesSpecificsHash(data.specifics);
}
bool SyncedBookmarkTracker::Entity::MatchesSpecificsHash(
const sync_pb::EntitySpecifics& specifics) const {
DCHECK(!metadata_->is_deleted());
DCHECK_GT(specifics.ByteSize(), 0);
std::string hash;
HashSpecifics(specifics, &hash);
return hash == metadata_->specifics_hash();
}
size_t SyncedBookmarkTracker::Entity::EstimateMemoryUsage() const {
using base::trace_event::EstimateMemoryUsage;
size_t memory_usage = 0;
// Include the size of the pointer to the bookmark node.
memory_usage += sizeof(bookmark_node_);
memory_usage += EstimateMemoryUsage(metadata_);
return memory_usage;
}
SyncedBookmarkTracker::SyncedBookmarkTracker(
std::vector<NodeMetadataPair> nodes_metadata,
std::unique_ptr<sync_pb::ModelTypeState> model_type_state)
: model_type_state_(std::move(model_type_state)) {
DCHECK(model_type_state_);
for (NodeMetadataPair& node_metadata : nodes_metadata) {
const std::string& sync_id = node_metadata.second->server_id();
auto entity = std::make_unique<Entity>(node_metadata.first,
std::move(node_metadata.second));
if (node_metadata.first) {
// Non-null node means it's not a tombstone.
bookmark_node_to_entities_map_[node_metadata.first] = entity.get();
} else {
// Otherwise, it must be a deletion so we must remember to deletion
// ordering.
DCHECK(entity->metadata()->is_deleted());
ordered_local_tombstones_.push_back(entity.get());
}
// TODO(crbug.com/516866): The below CHECK is added to debug some crashes.
// Should be removed after figuring out the reason for the crash.
CHECK_EQ(0U, sync_id_to_entities_map_.count(sync_id));
sync_id_to_entities_map_[sync_id] = std::move(entity);
}
}
SyncedBookmarkTracker::~SyncedBookmarkTracker() = default;
const SyncedBookmarkTracker::Entity* SyncedBookmarkTracker::GetEntityForSyncId(
const std::string& sync_id) const {
auto it = sync_id_to_entities_map_.find(sync_id);
return it != sync_id_to_entities_map_.end() ? it->second.get() : nullptr;
}
const SyncedBookmarkTracker::Entity*
SyncedBookmarkTracker::GetEntityForBookmarkNode(
const bookmarks::BookmarkNode* node) const {
auto it = bookmark_node_to_entities_map_.find(node);
return it != bookmark_node_to_entities_map_.end() ? it->second : nullptr;
}
void SyncedBookmarkTracker::Add(const std::string& sync_id,
const bookmarks::BookmarkNode* bookmark_node,
int64_t server_version,
base::Time creation_time,
const sync_pb::UniquePosition& unique_position,
const sync_pb::EntitySpecifics& specifics) {
DCHECK_GT(specifics.ByteSize(), 0);
auto metadata = std::make_unique<sync_pb::EntityMetadata>();
metadata->set_is_deleted(false);
metadata->set_server_id(sync_id);
metadata->set_server_version(server_version);
metadata->set_creation_time(syncer::TimeToProtoTime(creation_time));
metadata->set_modification_time(syncer::TimeToProtoTime(creation_time));
metadata->set_sequence_number(0);
metadata->set_acked_sequence_number(0);
metadata->mutable_unique_position()->CopyFrom(unique_position);
HashSpecifics(specifics, metadata->mutable_specifics_hash());
auto entity = std::make_unique<Entity>(bookmark_node, std::move(metadata));
bookmark_node_to_entities_map_[bookmark_node] = entity.get();
// TODO(crbug.com/516866): The below CHECK is added to debug some crashes.
// Should be removed after figuring out the reason for the crash.
CHECK_EQ(0U, sync_id_to_entities_map_.count(sync_id));
sync_id_to_entities_map_[sync_id] = std::move(entity);
}
void SyncedBookmarkTracker::Update(
const std::string& sync_id,
int64_t server_version,
base::Time modification_time,
const sync_pb::UniquePosition& unique_position,
const sync_pb::EntitySpecifics& specifics) {
DCHECK_GT(specifics.ByteSize(), 0);
auto it = sync_id_to_entities_map_.find(sync_id);
DCHECK(it != sync_id_to_entities_map_.end());
Entity* entity = it->second.get();
DCHECK(entity);
DCHECK_EQ(entity->metadata()->server_id(), sync_id);
entity->metadata()->set_server_version(server_version);
entity->metadata()->set_modification_time(
syncer::TimeToProtoTime(modification_time));
*entity->metadata()->mutable_unique_position() = unique_position;
HashSpecifics(specifics, entity->metadata()->mutable_specifics_hash());
// TODO(crbug.com/516866): in case of conflict, the entity might exist in
// |ordered_local_tombstones_| as well if it has been locally deleted.
}
void SyncedBookmarkTracker::UpdateServerVersion(const std::string& sync_id,
int64_t server_version) {
auto it = sync_id_to_entities_map_.find(sync_id);
DCHECK(it != sync_id_to_entities_map_.end());
Entity* entity = it->second.get();
DCHECK(entity);
entity->metadata()->set_server_version(server_version);
}
void SyncedBookmarkTracker::MarkDeleted(const std::string& sync_id) {
auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity = it->second.get();
DCHECK(entity);
entity->metadata()->set_is_deleted(true);
// Clear all references to the deleted bookmark node.
bookmark_node_to_entities_map_.erase(entity->bookmark_node());
entity->clear_bookmark_node();
ordered_local_tombstones_.push_back(entity);
}
void SyncedBookmarkTracker::Remove(const std::string& sync_id) {
const Entity* entity = GetEntityForSyncId(sync_id);
DCHECK(entity);
bookmark_node_to_entities_map_.erase(entity->bookmark_node());
base::Erase(ordered_local_tombstones_, entity);
sync_id_to_entities_map_.erase(sync_id);
}
void SyncedBookmarkTracker::IncrementSequenceNumber(
const std::string& sync_id) {
Entity* entity = sync_id_to_entities_map_.find(sync_id)->second.get();
DCHECK(entity);
// TODO(crbug.com/516866): Update base hash specifics here if the entity is
// not already out of sync.
entity->metadata()->set_sequence_number(
entity->metadata()->sequence_number() + 1);
}
sync_pb::BookmarkModelMetadata
SyncedBookmarkTracker::BuildBookmarkModelMetadata() const {
sync_pb::BookmarkModelMetadata model_metadata;
for (const std::pair<const std::string, std::unique_ptr<Entity>>& pair :
sync_id_to_entities_map_) {
if (pair.second->metadata()->is_deleted()) {
// Deletions will be added later because they need to maintain the same
// order as in |ordered_local_tombstones_|.
continue;
}
DCHECK(pair.second->bookmark_node());
sync_pb::BookmarkMetadata* bookmark_metadata =
model_metadata.add_bookmarks_metadata();
bookmark_metadata->set_id(pair.second->bookmark_node()->id());
*bookmark_metadata->mutable_metadata() = *pair.second->metadata();
}
// Add pending deletions.
for (const Entity* tombstone_entity : ordered_local_tombstones_) {
DCHECK(tombstone_entity->metadata()->is_deleted());
sync_pb::BookmarkMetadata* bookmark_metadata =
model_metadata.add_bookmarks_metadata();
*bookmark_metadata->mutable_metadata() = *tombstone_entity->metadata();
}
*model_metadata.mutable_model_type_state() = *model_type_state_;
return model_metadata;
}
bool SyncedBookmarkTracker::HasLocalChanges() const {
for (const std::pair<const std::string, std::unique_ptr<Entity>>& pair :
sync_id_to_entities_map_) {
Entity* entity = pair.second.get();
if (entity->IsUnsynced()) {
return true;
}
}
return false;
}
std::vector<const SyncedBookmarkTracker::Entity*>
SyncedBookmarkTracker::GetEntitiesWithLocalChanges(size_t max_entries) const {
std::vector<const SyncedBookmarkTracker::Entity*> entities_with_local_changes;
// Entities with local non deletions should be sorted such that parent
// creation/update comes before child creation/update.
for (const std::pair<const std::string, std::unique_ptr<Entity>>& pair :
sync_id_to_entities_map_) {
Entity* entity = pair.second.get();
if (entity->metadata()->is_deleted()) {
// Deletions are stored sorted in |ordered_local_tombstones_| and will be
// added later.
continue;
}
if (entity->IsUnsynced()) {
entities_with_local_changes.push_back(entity);
}
}
std::vector<const SyncedBookmarkTracker::Entity*> ordered_local_changes =
ReorderUnsyncedEntitiesExceptDeletions(entities_with_local_changes);
for (const Entity* tombstone_entity : ordered_local_tombstones_) {
ordered_local_changes.push_back(tombstone_entity);
}
if (ordered_local_changes.size() > max_entries) {
// TODO(crbug.com/516866): Should be smart and stop building the vector
// when |max_entries| is reached.
return std::vector<const SyncedBookmarkTracker::Entity*>(
ordered_local_changes.begin(),
ordered_local_changes.begin() + max_entries);
}
return ordered_local_changes;
}
std::vector<const SyncedBookmarkTracker::Entity*>
SyncedBookmarkTracker::ReorderUnsyncedEntitiesExceptDeletions(
const std::vector<const SyncedBookmarkTracker::Entity*>& entities) const {
// This method sorts the entities with local non deletions such that parent
// creation/update comes before child creation/update.
// The algorithm works by constructing a forest of all non-deletion updates
// and then traverses each tree in the forest recursively:
// 1. Iterate over all entities and collect all nodes in |nodes|.
// 2. Iterate over all entities again and node that is a child of another
// node. What's left in |nodes| are the roots of the forest.
// 3. Start at each root in |nodes|, emit the update and recurse over its
// children.
std::set<const bookmarks::BookmarkNode*> nodes;
// Collect nodes with updates
for (const SyncedBookmarkTracker::Entity* entity : entities) {
DCHECK(entity->IsUnsynced());
DCHECK(!entity->metadata()->is_deleted());
DCHECK(entity->bookmark_node());
nodes.insert(entity->bookmark_node());
}
// Remove those who are direct children of another node.
for (const SyncedBookmarkTracker::Entity* entity : entities) {
const bookmarks::BookmarkNode* node = entity->bookmark_node();
for (int i = 0; i < node->child_count(); ++i) {
nodes.erase(node->GetChild(i));
}
}
// |nodes| contains only roots of all trees in the forest all of which are
// ready to be processed because their parents have no pending updates.
std::vector<const SyncedBookmarkTracker::Entity*> ordered_entities;
for (const bookmarks::BookmarkNode* node : nodes) {
TraverseAndAppend(node, &ordered_entities);
}
return ordered_entities;
}
void SyncedBookmarkTracker::TraverseAndAppend(
const bookmarks::BookmarkNode* node,
std::vector<const SyncedBookmarkTracker::Entity*>* ordered_entities) const {
const SyncedBookmarkTracker::Entity* entity = GetEntityForBookmarkNode(node);
DCHECK(entity);
DCHECK(entity->IsUnsynced());
DCHECK(!entity->metadata()->is_deleted());
ordered_entities->push_back(entity);
// Recurse for all children.
for (int i = 0; i < node->child_count(); ++i) {
const bookmarks::BookmarkNode* child = node->GetChild(i);
const SyncedBookmarkTracker::Entity* child_entity =
GetEntityForBookmarkNode(child);
DCHECK(child_entity);
if (!child_entity->IsUnsynced()) {
// If the entity has no local change, no need to check its children. If
// any of the children would have a pending commit, it would be a root for
// a separate tree in the forest built in
// ReorderEntitiesWithLocalNonDeletions() and will be handled by another
// call to TraverseAndAppend().
continue;
}
if (child_entity->metadata()->is_deleted()) {
// Deletion are stored sorted in |ordered_local_tombstones_| and will be
// added later.
continue;
}
TraverseAndAppend(child, ordered_entities);
}
}
void SyncedBookmarkTracker::UpdateUponCommitResponse(
const std::string& old_id,
const std::string& new_id,
int64_t acked_sequence_number,
int64_t server_version) {
// TODO(crbug.com/516866): Update specifics if we decide to keep it.
auto it = sync_id_to_entities_map_.find(old_id);
Entity* entity =
it != sync_id_to_entities_map_.end() ? it->second.get() : nullptr;
if (!entity) {
DLOG(WARNING) << "Trying to update a non existing entity.";
return;
}
entity->metadata()->set_acked_sequence_number(acked_sequence_number);
entity->metadata()->set_server_version(server_version);
// If there are no pending commits, remove tombstones.
if (!entity->IsUnsynced() && entity->metadata()->is_deleted()) {
Remove(old_id);
return;
}
if (old_id != new_id) {
auto it = sync_id_to_entities_map_.find(old_id);
entity->metadata()->set_server_id(new_id);
// TODO(crbug.com/516866): The below CHECK is added to debug some crashes.
// Should be removed after figuring out the reason for the crash.
CHECK_EQ(0U, sync_id_to_entities_map_.count(new_id));
sync_id_to_entities_map_[new_id] = std::move(it->second);
sync_id_to_entities_map_.erase(old_id);
}
}
void SyncedBookmarkTracker::AckSequenceNumber(const std::string& sync_id) {
auto it = sync_id_to_entities_map_.find(sync_id);
Entity* entity =
it != sync_id_to_entities_map_.end() ? it->second.get() : nullptr;
DCHECK(entity);
entity->metadata()->set_acked_sequence_number(
entity->metadata()->sequence_number());
}
bool SyncedBookmarkTracker::IsEmpty() const {
return sync_id_to_entities_map_.empty();
}
size_t SyncedBookmarkTracker::EstimateMemoryUsage() const {
using base::trace_event::EstimateMemoryUsage;
size_t memory_usage = 0;
memory_usage += EstimateMemoryUsage(sync_id_to_entities_map_);
memory_usage += EstimateMemoryUsage(bookmark_node_to_entities_map_);
memory_usage += EstimateMemoryUsage(ordered_local_tombstones_);
memory_usage += EstimateMemoryUsage(model_type_state_);
return memory_usage;
}
size_t SyncedBookmarkTracker::TrackedEntitiesCountForTest() const {
return sync_id_to_entities_map_.size();
}
size_t SyncedBookmarkTracker::TrackedBookmarksCountForDebugging() const {
return bookmark_node_to_entities_map_.size();
}
size_t SyncedBookmarkTracker::TrackedUncommittedTombstonesCountForDebugging()
const {
return ordered_local_tombstones_.size();
}
} // namespace sync_bookmarks