1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212
|
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/sync_bookmarks/bookmark_model_merger.h"
#include <algorithm>
#include <string>
#include <utility>
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/uuid.h"
#include "build/build_config.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_uuids.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/time.h"
#include "components/sync/protocol/bookmark_specifics.pb.h"
#include "components/sync/protocol/entity_metadata.pb.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync_bookmarks/bookmark_model_view.h"
#include "components/sync_bookmarks/bookmark_specifics_conversions.h"
#include "components/sync_bookmarks/switches.h"
#include "components/sync_bookmarks/synced_bookmark_tracker.h"
#include "components/sync_bookmarks/synced_bookmark_tracker_entity.h"
#include "ui/base/models/tree_node_iterator.h"
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_CHROMEOS)
#include "components/sync_bookmarks/bookmark_model_merger_comparison_metrics.h"
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
// !BUILDFLAG(IS_CHROMEOS)
namespace sync_bookmarks {
namespace {
using syncer::EntityData;
using syncer::UpdateResponseData;
using syncer::UpdateResponseDataList;
static const size_t kInvalidIndex = -1;
// The sync protocol identifies top-level entities by means of well-known tags,
// (aka server defined tags) which should not be confused with titles or client
// tags that aren't supported by bookmarks (at the time of writing). Each tag
// corresponds to a singleton instance of a particular top-level node in a
// user's share; the tags are consistent across users. The tags allow us to
// locate the specific folders whose contents we care about synchronizing,
// without having to do a lookup by name or path. The tags should not be made
// user-visible. For example, the tag "bookmark_bar" represents the permanent
// node for bookmarks bar in Chrome. The tag "other_bookmarks" represents the
// permanent folder Other Bookmarks in Chrome.
//
// It is the responsibility of something upstream (at time of writing, the sync
// server) to create these tagged nodes when initializing sync for the first
// time for a user. Thus, once the backend finishes initializing, the
// SyncService can rely on the presence of tagged nodes.
const char kBookmarkBarTag[] = "bookmark_bar";
const char kMobileBookmarksTag[] = "synced_bookmarks";
const char kOtherBookmarksTag[] = "other_bookmarks";
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_CHROMEOS)
// Enabled by default, intended as a kill switch.
BASE_FEATURE(kSyncRecordBookmarkComparisonMetrics,
"SyncRecordBookmarkComparisonMetrics",
base::FEATURE_ENABLED_BY_DEFAULT);
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
// !BUILDFLAG(IS_CHROMEOS)
// Maximum depth to sync bookmarks tree to protect against stack overflow.
// Keep in sync with |base::internal::kAbsoluteMaxDepth| in json_common.h.
const size_t kMaxBookmarkTreeDepth = 200;
// The value must be a list since there is a container using pointers to its
// elements.
using UpdatesPerParentUuid =
std::unordered_map<base::Uuid,
std::list<syncer::UpdateResponseData>,
base::UuidHash>;
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. When adding values, be certain to also
// update the corresponding definition in enums.xml and the
// ExpectedBookmarksUuidDuplicates in unittests.
// LINT.IfChange(BookmarksGUIDDuplicates)
enum class BookmarksUuidDuplicates {
// Both entities are URLs with matching URLs in specifics. Entities may have
// different titles or parents.
kMatchingUrls = 0,
// Both entities are folders with matching titles. Entities may have different
// parents.
kMatchingFolders = 1,
// Both entities are URLs, but they have different URLs.
kDifferentUrls = 2,
// Both entities are folders with different titles.
kDifferentFolders = 3,
// Entities have different types.
kDifferentTypes = 4,
kMaxValue = kDifferentTypes,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/sync/enums.xml:BookmarksGUIDDuplicates)
// Used in metrics: "Sync.ProblematicServerSideBookmarksDuringMerge". These
// values are persisted to logs. Entries should not be renumbered and numeric
// values should never be reused. Note the existence of gaps because the
// metric enum is reused for another UMA metric,
// Sync.ProblematicServerSideBookmarks, which logs the analogous error cases
// for non-initial updates.
// LINT.IfChange(RemoteBookmarkUpdateError)
enum class RemoteBookmarkUpdateError {
// Invalid specifics.
kInvalidSpecifics = 1,
// Invalid unique position.
kInvalidUniquePosition = 2,
// Parent entity not found in server.
kMissingParentEntity = 4,
// The bookmark's UUID did not match the originator client item ID.
kUnexpectedGuid = 9,
// Parent is not a folder.
kParentNotFolder = 10,
// Unknown/unsupported permanent folder.
kUnsupportedPermanentFolder = 13,
// A bookmark that is not contained in any permanent folder and is instead
// hanging (directly or indirectly) from the root node.
// kDeprecatedDescendantOfRootNodeWithoutPermanentFolder = 14,
kMaxValue = kUnsupportedPermanentFolder,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/sync/enums.xml:RemoteBookmarkUpdateError)
void LogProblematicBookmark(RemoteBookmarkUpdateError problem) {
base::UmaHistogramEnumeration(
"Sync.ProblematicServerSideBookmarksDuringMerge", problem);
}
void LogBookmarkReuploadNeeded(bool is_reupload_needed) {
base::UmaHistogramBoolean("Sync.BookmarkEntityReuploadNeeded.OnInitialMerge",
is_reupload_needed);
}
// Gets the bookmark node corresponding to a permanent folder identified by
// |server_defined_unique_tag| or null of the tag is unknown. |bookmark_model|
// must not be null and |server_defined_unique_tag| must not be empty.
const bookmarks::BookmarkNode* GetPermanentFolderForServerDefinedUniqueTag(
const BookmarkModelView* bookmark_model,
const std::string& server_defined_unique_tag) {
DCHECK(bookmark_model);
DCHECK(!server_defined_unique_tag.empty());
// WARNING: Keep this logic consistent with the analogous in
// GetPermanentFolderUuidForServerDefinedUniqueTag().
if (server_defined_unique_tag == kBookmarkBarTag) {
return bookmark_model->bookmark_bar_node();
}
if (server_defined_unique_tag == kOtherBookmarksTag) {
return bookmark_model->other_node();
}
if (server_defined_unique_tag == kMobileBookmarksTag) {
return bookmark_model->mobile_node();
}
return nullptr;
}
// Gets the bookmark UUID corresponding to a permanent folder identified by
// |served_defined_unique_tag| or an invalid UUID if the tag is unknown.
// |server_defined_unique_tag| must not be empty.
base::Uuid GetPermanentFolderUuidForServerDefinedUniqueTag(
const std::string& server_defined_unique_tag) {
DCHECK(!server_defined_unique_tag.empty());
// WARNING: Keep this logic consistent with the analogous in
// GetPermanentFolderForServerDefinedUniqueTag().
if (server_defined_unique_tag == kBookmarkBarTag) {
return base::Uuid::ParseLowercase(bookmarks::kBookmarkBarNodeUuid);
}
if (server_defined_unique_tag == kOtherBookmarksTag) {
return base::Uuid::ParseLowercase(bookmarks::kOtherBookmarksNodeUuid);
}
if (server_defined_unique_tag == kMobileBookmarksTag) {
return base::Uuid::ParseLowercase(bookmarks::kMobileBookmarksNodeUuid);
}
return base::Uuid();
}
std::string LegacyCanonicalizedTitleFromSpecifics(
const sync_pb::BookmarkSpecifics& specifics) {
if (specifics.has_full_title()) {
return FullTitleToLegacyCanonicalizedTitle(specifics.full_title());
}
return specifics.legacy_canonicalized_title();
}
// Heuristic to consider two nodes (local and remote) a match by semantics for
// the purpose of merging. Two folders match by semantics if they have the same
// title, two bookmarks match by semantics if they have the same title and url.
// A folder and a bookmark never match.
bool NodeSemanticsMatch(const bookmarks::BookmarkNode* local_node,
const std::string& remote_canonicalized_title,
const GURL& remote_url,
sync_pb::BookmarkSpecifics::Type remote_type) {
if (GetProtoTypeFromBookmarkNode(local_node) != remote_type) {
return false;
}
if (remote_type == sync_pb::BookmarkSpecifics::URL &&
local_node->url() != remote_url) {
return false;
}
const std::string local_title = base::UTF16ToUTF8(local_node->GetTitle());
// Titles match if they are identical or the remote one is the canonical form
// of the local one. The latter is the case when a legacy client has
// canonicalized the same local title before committing it. Modern clients
// don't canonicalize titles anymore.
// TODO(rushans): the comment above is off.
if (local_title != remote_canonicalized_title &&
sync_bookmarks::FullTitleToLegacyCanonicalizedTitle(local_title) !=
remote_canonicalized_title) {
return false;
}
return true;
}
BookmarksUuidDuplicates MatchBookmarksUuidDuplicates(
const UpdateResponseData& update,
const UpdateResponseData& duplicate_update) {
if (update.entity.specifics.bookmark().type() !=
duplicate_update.entity.specifics.bookmark().type()) {
return BookmarksUuidDuplicates::kDifferentTypes;
}
switch (update.entity.specifics.bookmark().type()) {
case sync_pb::BookmarkSpecifics::UNSPECIFIED:
NOTREACHED();
case sync_pb::BookmarkSpecifics::URL: {
const bool matching_urls =
update.entity.specifics.bookmark().url() ==
duplicate_update.entity.specifics.bookmark().url();
return matching_urls ? BookmarksUuidDuplicates::kMatchingUrls
: BookmarksUuidDuplicates::kDifferentUrls;
}
case sync_pb::BookmarkSpecifics::FOLDER: {
// Both entities are folders.
const bool matching_titles =
LegacyCanonicalizedTitleFromSpecifics(
update.entity.specifics.bookmark()) ==
LegacyCanonicalizedTitleFromSpecifics(
duplicate_update.entity.specifics.bookmark());
return matching_titles ? BookmarksUuidDuplicates::kMatchingFolders
: BookmarksUuidDuplicates::kDifferentFolders;
}
}
NOTREACHED();
}
// Returns true the |next_update| is selected to keep and the |previous_update|
// should be removed. False is returned otherwise. |next_update| and
// |previous_update| must have the same UUID.
bool CompareDuplicateUpdates(const UpdateResponseData& next_update,
const UpdateResponseData& previous_update) {
DCHECK_EQ(next_update.entity.specifics.bookmark().guid(),
previous_update.entity.specifics.bookmark().guid());
DCHECK_NE(next_update.entity.id, previous_update.entity.id);
if (next_update.entity.specifics.bookmark().type() !=
previous_update.entity.specifics.bookmark().type()) {
// There are two entities, one of them is a folder and another one is a
// URL. Prefer to save the folder as it may contain many bookmarks.
return next_update.entity.specifics.bookmark().type() ==
sync_pb::BookmarkSpecifics::FOLDER;
}
// Choose the latest element to keep if both updates have the same type.
return next_update.entity.creation_time >
previous_update.entity.creation_time;
}
void DeduplicateValidUpdatesByUuid(
UpdatesPerParentUuid* updates_per_parent_uuid) {
DCHECK(updates_per_parent_uuid);
std::unordered_map<base::Uuid, std::list<UpdateResponseData>::iterator,
base::UuidHash>
uuid_to_update;
for (auto& [parent_uuid, updates] : *updates_per_parent_uuid) {
auto updates_iter = updates.begin();
while (updates_iter != updates.end()) {
const UpdateResponseData& update = *updates_iter;
DCHECK(!update.entity.is_deleted());
DCHECK(update.entity.server_defined_unique_tag.empty());
const base::Uuid uuid_in_specifics =
base::Uuid::ParseLowercase(update.entity.specifics.bookmark().guid());
DCHECK(uuid_in_specifics.is_valid());
auto [it, success] =
uuid_to_update.emplace(uuid_in_specifics, updates_iter);
if (success) {
++updates_iter;
continue;
}
const auto& [uuid, previous_update_it] = *it;
DCHECK_EQ(uuid_in_specifics.AsLowercaseString(),
previous_update_it->entity.specifics.bookmark().guid());
DLOG(ERROR) << "Duplicate uuid for new sync ID " << update.entity.id
<< " and original sync ID " << previous_update_it->entity.id;
const BookmarksUuidDuplicates match_result =
MatchBookmarksUuidDuplicates(update, *previous_update_it);
base::UmaHistogramEnumeration("Sync.BookmarksGUIDDuplicates",
match_result);
if (CompareDuplicateUpdates(/*next_update=*/update,
/*previous_update=*/*previous_update_it)) {
updates.erase(previous_update_it);
uuid_to_update[uuid_in_specifics] = updates_iter;
++updates_iter;
} else {
updates_iter = updates.erase(updates_iter);
}
}
}
}
// Checks that the |update| is valid and returns false otherwise. It is used to
// verify non-deletion updates. |update| must not be a deletion and a permanent
// node (they are processed in a different way).
bool IsValidUpdate(const UpdateResponseData& update) {
const EntityData& update_entity = update.entity;
DCHECK(!update_entity.is_deleted());
DCHECK(update_entity.server_defined_unique_tag.empty());
if (!IsValidBookmarkSpecifics(update_entity.specifics.bookmark())) {
// Ignore updates with invalid specifics.
DLOG(ERROR) << "Remote update with invalid specifics";
LogProblematicBookmark(RemoteBookmarkUpdateError::kInvalidSpecifics);
return false;
}
if (!HasExpectedBookmarkGuid(update_entity.specifics.bookmark(),
update_entity.client_tag_hash,
update_entity.originator_cache_guid,
update_entity.originator_client_item_id)) {
// Ignore updates with an unexpected UUID.
DLOG(ERROR) << "Remote update with unexpected Uuid";
LogProblematicBookmark(RemoteBookmarkUpdateError::kUnexpectedGuid);
return false;
}
return true;
}
// Returns the UUID determined by a remote update, which may be an update for a
// permanent folder or a regular bookmark node.
base::Uuid GetUuidForUpdate(const UpdateResponseData& update) {
if (!update.entity.server_defined_unique_tag.empty()) {
return GetPermanentFolderUuidForServerDefinedUniqueTag(
update.entity.server_defined_unique_tag);
}
DCHECK(IsValidUpdate(update));
return base::Uuid::ParseLowercase(update.entity.specifics.bookmark().guid());
}
struct GroupedUpdates {
// |updates_per_parent_uuid| contains all valid updates grouped by their
// |parent_uuid|. Permanent nodes and deletions are filtered out. Permanent
// nodes are stored in a dedicated list |permanent_node_updates|.
UpdatesPerParentUuid updates_per_parent_uuid;
UpdateResponseDataList permanent_node_updates;
};
// Groups all valid updates by the UUID of their parent. Permanent nodes are
// grouped in a dedicated |permanent_node_updates| list in a returned value.
GroupedUpdates GroupValidUpdates(UpdateResponseDataList updates) {
GroupedUpdates grouped_updates;
int num_valid_updates = 0;
for (UpdateResponseData& update : updates) {
const EntityData& update_entity = update.entity;
if (update_entity.is_deleted()) {
continue;
}
// Special-case the root folder to avoid recording
// |RemoteBookmarkUpdateError::kUnsupportedPermanentFolder|.
if (update_entity.server_defined_unique_tag ==
syncer::DataTypeToProtocolRootTag(syncer::BOOKMARKS)) {
++num_valid_updates;
continue;
}
// Non-root permanent folders don't need further validation.
if (!update_entity.server_defined_unique_tag.empty()) {
++num_valid_updates;
grouped_updates.permanent_node_updates.push_back(std::move(update));
continue;
}
// Regular (non-permanent) node updates must pass IsValidUpdate().
if (!IsValidUpdate(update)) {
continue;
}
++num_valid_updates;
const base::Uuid parent_uuid = base::Uuid::ParseLowercase(
update_entity.specifics.bookmark().parent_guid());
DCHECK(parent_uuid.is_valid());
grouped_updates.updates_per_parent_uuid[parent_uuid].push_back(
std::move(update));
}
base::UmaHistogramCounts100000("Sync.BookmarkModelMerger.ValidInputUpdates",
num_valid_updates);
return grouped_updates;
}
int GetNumUnsyncedEntities(const SyncedBookmarkTracker* tracker) {
DCHECK(tracker);
int num_unsynced_entities = 0;
for (const SyncedBookmarkTrackerEntity* entity : tracker->GetAllEntities()) {
if (entity->IsUnsynced()) {
++num_unsynced_entities;
}
}
return num_unsynced_entities;
}
} // namespace
BookmarkModelMerger::RemoteTreeNode::RemoteTreeNode() = default;
BookmarkModelMerger::RemoteTreeNode::~RemoteTreeNode() = default;
BookmarkModelMerger::RemoteTreeNode::RemoteTreeNode(
BookmarkModelMerger::RemoteTreeNode&&) = default;
BookmarkModelMerger::RemoteTreeNode&
BookmarkModelMerger::RemoteTreeNode::operator=(
BookmarkModelMerger::RemoteTreeNode&&) = default;
void BookmarkModelMerger::RemoteTreeNode::EmplaceSelfAndDescendantsByUuid(
std::unordered_map<base::Uuid, const RemoteTreeNode*, base::UuidHash>*
uuid_to_remote_node_map) const {
DCHECK(uuid_to_remote_node_map);
if (entity().server_defined_unique_tag.empty()) {
const base::Uuid uuid =
base::Uuid::ParseLowercase(entity().specifics.bookmark().guid());
DCHECK(uuid.is_valid());
// Duplicate UUIDs have been sorted out before.
bool success = uuid_to_remote_node_map->emplace(uuid, this).second;
DCHECK(success);
}
for (const RemoteTreeNode& child : children_) {
child.EmplaceSelfAndDescendantsByUuid(uuid_to_remote_node_map);
}
}
// static
bool BookmarkModelMerger::RemoteTreeNode::UniquePositionLessThan(
const RemoteTreeNode& lhs,
const RemoteTreeNode& rhs) {
return lhs.unique_position_.LessThan(rhs.unique_position_);
}
// static
BookmarkModelMerger::RemoteTreeNode
BookmarkModelMerger::RemoteTreeNode::BuildTree(
UpdateResponseData update,
size_t max_depth,
UpdatesPerParentUuid* updates_per_parent_uuid) {
DCHECK(updates_per_parent_uuid);
DCHECK(!update.entity.server_defined_unique_tag.empty() ||
IsValidUpdate(update));
// |uuid| may be invalid for unsupported permanent nodes.
const base::Uuid uuid = GetUuidForUpdate(update);
RemoteTreeNode node;
node.update_ = std::move(update);
node.unique_position_ = syncer::UniquePosition::FromProto(
node.update_.entity.specifics.bookmark().unique_position());
// Ensure we have not reached the maximum tree depth to guard against stack
// overflows.
if (max_depth == 0) {
return node;
}
// Check to prevent creating empty lists in |updates_per_parent_uuid| and
// unnecessary rehashing.
auto updates_per_parent_uuid_iter = updates_per_parent_uuid->find(uuid);
if (updates_per_parent_uuid_iter == updates_per_parent_uuid->end()) {
return node;
}
DCHECK(!updates_per_parent_uuid_iter->second.empty());
DCHECK(uuid.is_valid());
// Only folders may have descendants (ignore them otherwise). Treat
// permanent nodes as folders explicitly.
if (node.update_.entity.specifics.bookmark().type() !=
sync_pb::BookmarkSpecifics::FOLDER &&
node.update_.entity.server_defined_unique_tag.empty()) {
// Children of a non-folder are ignored.
for (UpdateResponseData& child_update :
updates_per_parent_uuid_iter->second) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kParentNotFolder);
// To avoid double-counting later for bucket |kMissingParentEntity|,
// clear the update from the list as if it would have been moved.
child_update.entity = EntityData();
}
return node;
}
// Populate descendants recursively.
node.children_.reserve(updates_per_parent_uuid_iter->second.size());
for (UpdateResponseData& child_update :
updates_per_parent_uuid_iter->second) {
DCHECK_EQ(child_update.entity.specifics.bookmark().parent_guid(),
uuid.AsLowercaseString());
DCHECK(IsValidBookmarkSpecifics(child_update.entity.specifics.bookmark()));
node.children_.push_back(BuildTree(std::move(child_update), max_depth - 1,
updates_per_parent_uuid));
}
// Sort the children according to their unique position.
std::ranges::sort(node.children_, UniquePositionLessThan);
return node;
}
// static
BookmarkModelMerger::RemoteTreeNode
BookmarkModelMerger::RemoteTreeNode::BuildForTesting(
syncer::UpdateResponseData update,
std::vector<RemoteTreeNode> children) {
RemoteTreeNode node;
node.update_ = std::move(update);
node.children_ = std::move(children);
return node;
}
BookmarkModelMerger::BookmarkModelMerger(
UpdateResponseDataList updates,
BookmarkModelView* bookmark_model,
favicon::FaviconService* favicon_service,
SyncedBookmarkTracker* bookmark_tracker,
syncer::PreviouslySyncingGaiaIdInfoForMetrics
previously_syncing_gaia_id_info)
: bookmark_model_(bookmark_model),
favicon_service_(favicon_service),
bookmark_tracker_(bookmark_tracker),
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_CHROMEOS)
previously_syncing_gaia_id_info_(previously_syncing_gaia_id_info),
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
// !BUILDFLAG(IS_CHROMEOS)
remote_updates_size_(updates.size()),
remote_forest_(BuildRemoteForest(std::move(updates), bookmark_tracker)),
uuid_to_match_map_(
FindGuidMatchesOrReassignLocal(remote_forest_, bookmark_model_)) {
CHECK(bookmark_tracker_->IsEmpty());
CHECK(favicon_service);
CHECK(bookmark_model);
CHECK(bookmark_model->bookmark_bar_node());
CHECK(bookmark_model->mobile_node());
CHECK(bookmark_model->other_node());
int num_updates_in_forest = 0;
for (const auto& [server_defined_unique_tag, root] : remote_forest_) {
num_updates_in_forest += 1 + CountRemoteTreeNodeDescendantsForUma(root);
}
base::UmaHistogramCounts100000(
"Sync.BookmarkModelMerger.ReachableInputUpdates", num_updates_in_forest);
}
BookmarkModelMerger::~BookmarkModelMerger() = default;
void BookmarkModelMerger::Merge() {
TRACE_EVENT0("sync", "BookmarkModelMerger::Merge");
// These metrics are only recorded on desktop platforms, so there is no need
// to increase the binary size elsewhere.
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_CHROMEOS)
if (previously_syncing_gaia_id_info_ !=
syncer::PreviouslySyncingGaiaIdInfoForMetrics::kUnspecified) {
if (base::FeatureList::IsEnabled(kSyncRecordBookmarkComparisonMetrics)) {
metrics::CompareBookmarkModelAndLogHistograms(
*bookmark_model_, remote_forest_, previously_syncing_gaia_id_info_);
}
base::UmaHistogramEnumeration(
"Sync.BookmarkModelMerger.PreviouslySyncingGaiaId",
previously_syncing_gaia_id_info_);
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS) &&
// !BUILDFLAG(IS_CHROMEOS)
// Algorithm description:
// Match up the roots and recursively do the following:
// * For each remote node for the current remote (sync) parent node, either
// find a local node with equal UUID anywhere throughout the tree or find
// the best matching bookmark node under the corresponding local bookmark
// parent node using semantics. If the found node has the same UUID as a
// different remote bookmark, we do not consider it a semantics match, as
// UUID matching takes precedence. If no matching node is found, create a
// new bookmark node in the same position as the corresponding remote node.
// If a matching node is found, update the properties of it from the
// corresponding remote node.
// * When all children remote nodes are done, add the extra children bookmark
// nodes to the remote (sync) parent node, unless they will be later matched
// by UUID.
//
// The semantics best match algorithm uses folder title or bookmark title/url
// to perform the primary match. If there are multiple match candidates it
// selects the first one.
// Associate permanent folders.
for (const auto& [server_defined_unique_tag, root] : remote_forest_) {
DCHECK(!server_defined_unique_tag.empty());
const bookmarks::BookmarkNode* permanent_folder =
GetPermanentFolderForServerDefinedUniqueTag(bookmark_model_,
server_defined_unique_tag);
// Ignore unsupported permanent folders.
if (!permanent_folder) {
DCHECK(!GetPermanentFolderUuidForServerDefinedUniqueTag(
server_defined_unique_tag)
.is_valid());
LogProblematicBookmark(
RemoteBookmarkUpdateError::kUnsupportedPermanentFolder);
continue;
}
DCHECK_EQ(permanent_folder->uuid(),
GetPermanentFolderUuidForServerDefinedUniqueTag(
server_defined_unique_tag));
MergeSubtree(/*local_node=*/permanent_folder,
/*remote_node=*/root);
}
if (base::FeatureList::IsEnabled(switches::kSyncReuploadBookmarks)) {
// When the reupload feature is enabled, all new empty trackers are
// automatically reuploaded (since there are no entities to reupload). This
// is used to disable reupload after initial merge.
bookmark_tracker_->SetBookmarksReuploaded();
}
if (base::FeatureList::IsEnabled(
switches::kSyncMigrateBookmarksWithoutClientTagHash)) {
for (const auto& [server_defined_unique_tag, root] : remote_forest_) {
MigrateBookmarksInSubtreeWithoutClientTagHash(root);
}
}
base::UmaHistogramCounts100000(
"Sync.BookmarkModelMerger.UnsyncedEntitiesUponCompletion",
GetNumUnsyncedEntities(bookmark_tracker_));
ReportTimeMetrics();
}
// static
BookmarkModelMerger::RemoteForest BookmarkModelMerger::BuildRemoteForest(
syncer::UpdateResponseDataList updates,
SyncedBookmarkTracker* tracker_for_recording_ignored_updates) {
TRACE_EVENT0("sync", "BookmarkModelMerger::BuildRemoteForest");
DCHECK(tracker_for_recording_ignored_updates);
// Filter out invalid remote updates and group the valid ones by the server ID
// of their parent.
GroupedUpdates grouped_updates = GroupValidUpdates(std::move(updates));
DeduplicateValidUpdatesByUuid(&grouped_updates.updates_per_parent_uuid);
// Construct one tree per permanent entity.
RemoteForest update_forest;
for (UpdateResponseData& permanent_node_update :
grouped_updates.permanent_node_updates) {
// Make a copy of the string to avoid relying on argument evaluation order.
const std::string server_defined_unique_tag =
permanent_node_update.entity.server_defined_unique_tag;
DCHECK(!server_defined_unique_tag.empty());
update_forest.emplace(
server_defined_unique_tag,
RemoteTreeNode::BuildTree(std::move(permanent_node_update),
kMaxBookmarkTreeDepth,
&grouped_updates.updates_per_parent_uuid));
}
// All remaining entries in |updates_per_parent_uuid| must be unreachable from
// permanent entities, since otherwise they would have been moved away.
for (const auto& [parent_uuid, updates_for_uuid] :
grouped_updates.updates_per_parent_uuid) {
for (const UpdateResponseData& update : updates_for_uuid) {
if (update.entity.specifics.has_bookmark()) {
LogProblematicBookmark(RemoteBookmarkUpdateError::kMissingParentEntity);
tracker_for_recording_ignored_updates
->RecordIgnoredServerUpdateDueToMissingParent(
update.response_version);
}
}
}
return update_forest;
}
// static
int BookmarkModelMerger::CountRemoteTreeNodeDescendantsForUma(
const RemoteTreeNode& node) {
int descendants = 0;
for (const RemoteTreeNode& child : node.children()) {
descendants += 1 + CountRemoteTreeNodeDescendantsForUma(child);
}
return descendants;
}
// static
std::unordered_map<base::Uuid, BookmarkModelMerger::GuidMatch, base::UuidHash>
BookmarkModelMerger::FindGuidMatchesOrReassignLocal(
const RemoteForest& remote_forest,
BookmarkModelView* bookmark_model) {
DCHECK(bookmark_model);
TRACE_EVENT0("sync", "BookmarkModelMerger::FindGuidMatchesOrReassignLocal");
// Build a temporary lookup table for remote UUIDs.
std::unordered_map<base::Uuid, const RemoteTreeNode*, base::UuidHash>
uuid_to_remote_node_map;
for (const auto& [server_defined_unique_tag, root] : remote_forest) {
root.EmplaceSelfAndDescendantsByUuid(&uuid_to_remote_node_map);
}
// Iterate through all local bookmarks to find matches by UUID.
std::unordered_map<base::Uuid, BookmarkModelMerger::GuidMatch, base::UuidHash>
uuid_to_match_map;
// Because ReplaceBookmarkNodeUuid() cannot be used while iterating the local
// bookmark model, a temporary list is constructed first to reassign later.
std::vector<const bookmarks::BookmarkNode*> nodes_to_replace_uuid;
ui::TreeNodeIterator<const bookmarks::BookmarkNode> iterator(
bookmark_model->root_node());
while (iterator.has_next()) {
const bookmarks::BookmarkNode* const node = iterator.Next();
DCHECK(node->uuid().is_valid());
// Ignore changes to non-syncable nodes. Managed nodes, which are
// unsyncable, use a random UUID so they should never match, but this
// codepath is useful when BookmarkModelMerger is used together with
// `BookmarkModelViewUsingAccountNodes`, which would otherwise match against
// local nodes.
if (!bookmark_model->IsNodeSyncable(node)) {
continue;
}
const auto remote_it = uuid_to_remote_node_map.find(node->uuid());
if (remote_it == uuid_to_remote_node_map.end()) {
continue;
}
const RemoteTreeNode* const remote_node = remote_it->second;
const syncer::EntityData& remote_entity = remote_node->entity();
// Permanent nodes don't match by UUID but by |server_defined_unique_tag|.
// As extra precaution, specially with remote UUIDs in mind, let's ignore
// them explicitly here.
DCHECK(remote_entity.server_defined_unique_tag.empty());
if (node->is_permanent_node()) {
continue;
}
if (GetProtoTypeFromBookmarkNode(node) !=
remote_entity.specifics.bookmark().type() ||
(node->is_url() &&
node->url() != remote_entity.specifics.bookmark().url())) {
// If local node and its remote node match are conflicting in node type or
// URL, replace local UUID with a random UUID.
nodes_to_replace_uuid.push_back(node);
continue;
}
const bool success =
uuid_to_match_map.emplace(node->uuid(), GuidMatch{node, remote_node})
.second;
// Insertion must have succeeded unless there were duplicate UUIDs in the
// local BookmarkModel (invariant violation that gets resolved upon
// restart).
DCHECK(success);
}
for (const bookmarks::BookmarkNode* node : nodes_to_replace_uuid) {
ReplaceBookmarkNodeUuid(node, base::Uuid::GenerateRandomV4(),
bookmark_model);
}
return uuid_to_match_map;
}
void BookmarkModelMerger::MigrateBookmarksInSubtreeWithoutClientTagHash(
const RemoteTreeNode& remote_node) {
// Recursively iterate children first for simplicity, as the order doesn't
// matter.
for (const RemoteTreeNode& child : remote_node.children()) {
MigrateBookmarksInSubtreeWithoutClientTagHash(child);
}
// Nothing to do for permanent folders.
if (!remote_node.entity().server_defined_unique_tag.empty()) {
return;
}
// Nothing to do if this entity already uses a client tag hash.
if (!remote_node.entity().client_tag_hash.value().empty()) {
return;
}
// Guaranteed by HasExpectedBookmarkGuid().
CHECK(!remote_node.entity().originator_cache_guid.empty() ||
!remote_node.entity().originator_client_item_id.empty());
const SyncedBookmarkTrackerEntity* old_entity =
bookmark_tracker_->GetEntityForSyncId(remote_node.entity().id);
CHECK(old_entity);
CHECK(old_entity->bookmark_node());
const base::Time creation_time =
syncer::ProtoTimeToTime(old_entity->metadata().creation_time());
const syncer::UniquePosition pos = syncer::UniquePosition::FromProto(
old_entity->metadata().unique_position());
const bookmarks::BookmarkNode* node = old_entity->bookmark_node();
bookmark_tracker_->MarkDeleted(old_entity, FROM_HERE);
bookmark_tracker_->IncrementSequenceNumber(old_entity);
// TODO(crbug.com/376641665): Consider generating new UUIDs deterministically
// rather than randomly to guard against concurrent clients or interrupted
// migrations.
const base::Uuid new_guid = base::Uuid::GenerateRandomV4();
node = ReplaceBookmarkNodeUuid(node, new_guid, bookmark_model_);
const sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(
node, bookmark_model_, pos.ToProto(), /*force_favicon_load=*/true);
const SyncedBookmarkTrackerEntity* new_entity = bookmark_tracker_->Add(
node, /*sync_id=*/new_guid.AsLowercaseString(),
syncer::kUncommittedVersion, creation_time, specifics);
// Mark the entity that it needs to be committed.
bookmark_tracker_->IncrementSequenceNumber(new_entity);
// Make sure all direct children are marked for commit, because their parent
// changed.
for (const RemoteTreeNode& child : remote_node.children()) {
const SyncedBookmarkTrackerEntity* child_entity =
bookmark_tracker_->GetEntityForSyncId(child.entity().id);
CHECK(child_entity);
bookmark_tracker_->IncrementSequenceNumber(child_entity);
}
}
void BookmarkModelMerger::MergeSubtree(
const bookmarks::BookmarkNode* local_subtree_root,
const RemoteTreeNode& remote_node) {
const EntityData& remote_update_entity = remote_node.entity();
const SyncedBookmarkTrackerEntity* entity = bookmark_tracker_->Add(
local_subtree_root, remote_update_entity.id,
remote_node.response_version(), remote_update_entity.creation_time,
remote_update_entity.specifics);
const bool is_reupload_needed =
!local_subtree_root->is_permanent_node() &&
IsBookmarkEntityReuploadNeeded(remote_update_entity);
if (is_reupload_needed) {
bookmark_tracker_->IncrementSequenceNumber(entity);
}
LogBookmarkReuploadNeeded(is_reupload_needed);
// If there are remote child updates, try to match them.
for (size_t remote_index = 0; remote_index < remote_node.children().size();
++remote_index) {
// TODO(crbug.com/40118203): change to DCHECK after investigating.
// Here is expected that all nodes to the left of current |remote_index| are
// filled with remote updates. All local nodes which are not merged will be
// added later.
CHECK_LE(remote_index, local_subtree_root->children().size());
const RemoteTreeNode& remote_child =
remote_node.children().at(remote_index);
const bookmarks::BookmarkNode* matching_local_node =
FindMatchingLocalNode(remote_child, local_subtree_root, remote_index);
// If no match found, create a corresponding local node.
if (!matching_local_node) {
ProcessRemoteCreation(remote_child, local_subtree_root, remote_index);
continue;
}
DCHECK(!local_subtree_root->HasAncestor(matching_local_node));
// Move if required, no-op otherwise.
bookmark_model_->Move(matching_local_node, local_subtree_root,
remote_index);
// Since nodes are matching, their subtrees should be merged as well.
matching_local_node = UpdateBookmarkNodeFromSpecificsIncludingUuid(
matching_local_node, remote_child);
MergeSubtree(matching_local_node, remote_child);
}
// At this point all the children of |remote_node| have corresponding local
// nodes under |local_subtree_root| and they are all in the right positions:
// from 0 to remote_node.children().size() - 1.
//
// This means, the children starting from remote_node.children().size() in
// the parent bookmark node are the ones that are not present in the parent
// sync node and not tracked yet. So create all of the remaining local nodes.
DCHECK_LE(remote_node.children().size(),
local_subtree_root->children().size());
for (size_t i = remote_node.children().size();
i < local_subtree_root->children().size(); ++i) {
// If local node has been or will be matched by UUID, skip it.
if (FindMatchingRemoteNodeByUuid(local_subtree_root->children()[i].get())) {
continue;
}
ProcessLocalCreation(local_subtree_root, i);
}
}
const bookmarks::BookmarkNode* BookmarkModelMerger::FindMatchingLocalNode(
const RemoteTreeNode& remote_child,
const bookmarks::BookmarkNode* local_parent,
size_t local_child_start_index) const {
DCHECK(local_parent);
// Try to match child by UUID. If we can't, try to match child by semantics.
const bookmarks::BookmarkNode* matching_local_node_by_uuid =
FindMatchingLocalNodeByUuid(remote_child);
if (matching_local_node_by_uuid) {
return matching_local_node_by_uuid;
}
// All local nodes up to |remote_index-1| have processed already. Look for a
// matching local node starting with the local node at position
// |local_child_start_index|. FindMatchingChildBySemanticsStartingAt()
// returns kInvalidIndex in the case where no semantics match was found or
// the semantics match found is UUID-matchable to a different node.
const size_t local_index = FindMatchingChildBySemanticsStartingAt(
/*remote_node=*/remote_child,
/*local_parent=*/local_parent,
/*starting_child_index=*/local_child_start_index);
if (local_index == kInvalidIndex) {
// If no match found, return.
return nullptr;
}
// The child at |local_index| has matched by semantics, which also means it
// does not match by UUID to any other remote node.
const bookmarks::BookmarkNode* matching_local_node_by_semantics =
local_parent->children()[local_index].get();
DCHECK(!FindMatchingRemoteNodeByUuid(matching_local_node_by_semantics));
return matching_local_node_by_semantics;
}
const bookmarks::BookmarkNode*
BookmarkModelMerger::UpdateBookmarkNodeFromSpecificsIncludingUuid(
const bookmarks::BookmarkNode* local_node,
const RemoteTreeNode& remote_node) {
DCHECK(local_node);
DCHECK(!local_node->is_permanent_node());
// Ensure bookmarks have the same URL, otherwise they would not have been
// matched.
DCHECK(local_node->is_folder() ||
local_node->url() ==
GURL(remote_node.entity().specifics.bookmark().url()));
const EntityData& remote_update_entity = remote_node.entity();
const sync_pb::BookmarkSpecifics& specifics =
remote_update_entity.specifics.bookmark();
// Update the local UUID if necessary for semantic matches (it's obviously not
// needed for UUID-based matches).
const bookmarks::BookmarkNode* possibly_replaced_local_node = local_node;
if (!specifics.guid().empty() &&
specifics.guid() != local_node->uuid().AsLowercaseString()) {
// If it's a semantic match, neither of the nodes should be involved in any
// UUID-based match.
DCHECK(!FindMatchingLocalNodeByUuid(remote_node));
DCHECK(!FindMatchingRemoteNodeByUuid(local_node));
possibly_replaced_local_node = ReplaceBookmarkNodeUuid(
local_node, base::Uuid::ParseLowercase(specifics.guid()),
bookmark_model_);
// TODO(rushans): remove the code below since DCHECKs above guarantee that
// |uuid_to_match_map_| has no such UUID.
//
// Update |uuid_to_match_map_| to avoid pointing to a deleted node. This
// should not be required in practice, because the algorithm processes each
// UUID once, but let's update nevertheless to avoid future issues.
const auto it =
uuid_to_match_map_.find(possibly_replaced_local_node->uuid());
if (it != uuid_to_match_map_.end() && it->second.local_node == local_node) {
it->second.local_node = possibly_replaced_local_node;
}
}
// Update all fields, where no-op changes are handled well.
UpdateBookmarkNodeFromSpecifics(specifics, possibly_replaced_local_node,
bookmark_model_, favicon_service_);
return possibly_replaced_local_node;
}
void BookmarkModelMerger::ProcessRemoteCreation(
const RemoteTreeNode& remote_node,
const bookmarks::BookmarkNode* local_parent,
size_t index) {
TRACE_EVENT0("sync", "BookmarkModelMerger::ProcessRemoteCreation");
DCHECK(!FindMatchingLocalNodeByUuid(remote_node));
const EntityData& remote_update_entity = remote_node.entity();
DCHECK(IsValidBookmarkSpecifics(remote_update_entity.specifics.bookmark()));
const sync_pb::EntitySpecifics& specifics = remote_node.entity().specifics;
const bookmarks::BookmarkNode* bookmark_node =
CreateBookmarkNodeFromSpecifics(specifics.bookmark(), local_parent, index,
bookmark_model_, favicon_service_);
DCHECK(bookmark_node);
const SyncedBookmarkTrackerEntity* entity = bookmark_tracker_->Add(
bookmark_node, remote_update_entity.id, remote_node.response_version(),
remote_update_entity.creation_time, specifics);
const bool is_reupload_needed =
IsBookmarkEntityReuploadNeeded(remote_node.entity());
if (is_reupload_needed) {
bookmark_tracker_->IncrementSequenceNumber(entity);
}
LogBookmarkReuploadNeeded(is_reupload_needed);
// Recursively, match by UUID or, if not possible, create local node for all
// child remote nodes.
size_t i = 0;
for (const RemoteTreeNode& remote_child : remote_node.children()) {
// TODO(crbug.com/40118203): change to DCHECK after investigating of some
// crashes.
CHECK_LE(i, bookmark_node->children().size());
const bookmarks::BookmarkNode* local_child =
FindMatchingLocalNodeByUuid(remote_child);
if (!local_child) {
ProcessRemoteCreation(remote_child, bookmark_node, i++);
continue;
}
bookmark_model_->Move(local_child, bookmark_node, i++);
local_child =
UpdateBookmarkNodeFromSpecificsIncludingUuid(local_child, remote_child);
MergeSubtree(local_child, remote_child);
}
}
void BookmarkModelMerger::ProcessLocalCreation(
const bookmarks::BookmarkNode* parent,
size_t index) {
DCHECK_LE(index, parent->children().size());
const SyncedBookmarkTrackerEntity* parent_entity =
bookmark_tracker_->GetEntityForBookmarkNode(parent);
// Since we are merging top down, parent entity must be tracked.
DCHECK(parent_entity);
// Assign a temp server id for the entity. Will be overridden by the actual
// server id upon receiving commit response.
const bookmarks::BookmarkNode* node = parent->children()[index].get();
DCHECK(!FindMatchingRemoteNodeByUuid(node));
// The node's UUID cannot run into collisions because
// FindGuidMatchesOrReassignLocal() takes care of reassigning local UUIDs if
// they won't actually be merged with the remote bookmark with the same UUID
// (e.g. incompatible types).
const base::Time creation_time = base::Time::Now();
const syncer::UniquePosition::Suffix suffix =
syncer::UniquePosition::GenerateSuffix(
SyncedBookmarkTracker::GetClientTagHashFromUuid(node->uuid()));
// Locally created nodes aren't tracked and hence don't have a unique position
// yet so we need to produce new ones.
const syncer::UniquePosition pos =
GenerateUniquePositionForLocalCreation(parent, index, suffix);
const sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(
node, bookmark_model_, pos.ToProto(), /*force_favicon_load=*/true);
const SyncedBookmarkTrackerEntity* entity = bookmark_tracker_->Add(
node, /*sync_id=*/node->uuid().AsLowercaseString(),
syncer::kUncommittedVersion, creation_time, specifics);
// Mark the entity that it needs to be committed.
bookmark_tracker_->IncrementSequenceNumber(entity);
for (size_t i = 0; i < node->children().size(); ++i) {
// If a local node hasn't matched with any remote entity, its descendants
// will neither, unless they have been or will be matched by UUID, in which
// case we skip them for now.
if (FindMatchingRemoteNodeByUuid(node->children()[i].get())) {
continue;
}
ProcessLocalCreation(/*parent=*/node, i);
}
}
size_t BookmarkModelMerger::FindMatchingChildBySemanticsStartingAt(
const RemoteTreeNode& remote_node,
const bookmarks::BookmarkNode* local_parent,
size_t starting_child_index) const {
DCHECK(local_parent);
TRACE_EVENT0("sync",
"BookmarkModelMerger::FindMatchingChildBySemanticsStartingAt");
const auto& children = local_parent->children();
DCHECK_LE(starting_child_index, children.size());
const EntityData& remote_entity = remote_node.entity();
// Precompute the remote title and URL before searching for a matching local
// node.
const std::string remote_canonicalized_title =
LegacyCanonicalizedTitleFromSpecifics(remote_entity.specifics.bookmark());
const sync_pb::BookmarkSpecifics::Type remote_type =
remote_entity.specifics.bookmark().type();
GURL remote_url;
if (remote_type == sync_pb::BookmarkSpecifics::URL) {
remote_url = GURL(remote_entity.specifics.bookmark().url());
}
const auto it = std::find_if(
children.cbegin() + starting_child_index, children.cend(),
[this, &remote_canonicalized_title, &remote_url,
remote_type](const auto& child) {
return !FindMatchingRemoteNodeByUuid(child.get()) &&
NodeSemanticsMatch(child.get(), remote_canonicalized_title,
remote_url, remote_type);
});
return (it == children.cend()) ? kInvalidIndex : (it - children.cbegin());
}
const BookmarkModelMerger::RemoteTreeNode*
BookmarkModelMerger::FindMatchingRemoteNodeByUuid(
const bookmarks::BookmarkNode* local_node) const {
DCHECK(local_node);
const auto it = uuid_to_match_map_.find(local_node->uuid());
if (it == uuid_to_match_map_.end()) {
return nullptr;
}
DCHECK_EQ(it->second.local_node, local_node);
return it->second.remote_node;
}
const bookmarks::BookmarkNode* BookmarkModelMerger::FindMatchingLocalNodeByUuid(
const RemoteTreeNode& remote_node) const {
const syncer::EntityData& remote_entity = remote_node.entity();
const auto it = uuid_to_match_map_.find(
base::Uuid::ParseLowercase(remote_entity.specifics.bookmark().guid()));
if (it == uuid_to_match_map_.end()) {
return nullptr;
}
DCHECK_EQ(it->second.remote_node, &remote_node);
return it->second.local_node;
}
syncer::UniquePosition
BookmarkModelMerger::GenerateUniquePositionForLocalCreation(
const bookmarks::BookmarkNode* parent,
size_t index,
const syncer::UniquePosition::Suffix& suffix) const {
// Try to find last tracked preceding entity. It is not always the previous
// one as it might be skipped if it has unprocessed remote matching by UUID
// update.
for (size_t i = index; i > 0; --i) {
const SyncedBookmarkTrackerEntity* predecessor_entity =
bookmark_tracker_->GetEntityForBookmarkNode(
parent->children()[i - 1].get());
if (predecessor_entity != nullptr) {
return syncer::UniquePosition::After(
syncer::UniquePosition::FromProto(
predecessor_entity->metadata().unique_position()),
suffix);
}
DCHECK(FindMatchingRemoteNodeByUuid(parent->children()[i - 1].get()));
}
return syncer::UniquePosition::InitialPosition(suffix);
}
void BookmarkModelMerger::ReportTimeMetrics() {
base::TimeDelta all_time_elapsed = base::TimeTicks::Now() - started_;
base::UmaHistogramMediumTimes("Sync.BookmarkModelMergerTime",
all_time_elapsed);
if (remote_updates_size_ >= 10000) {
base::UmaHistogramMediumTimes("Sync.BookmarkModelMergerTime.10kUpdates",
all_time_elapsed);
}
if (remote_updates_size_ >= 50000) {
base::UmaHistogramMediumTimes("Sync.BookmarkModelMergerTime.50kUpdates",
all_time_elapsed);
}
if (remote_updates_size_ >= 100000) {
base::UmaHistogramMediumTimes("Sync.BookmarkModelMergerTime.100kUpdates",
all_time_elapsed);
}
}
} // namespace sync_bookmarks
|