Commit 89df24ad authored by Dan Harrington's avatar Dan Harrington Committed by Commit Bot

Add StreamModel

StreamModel ingests DataOperations and provides the set of content
to be rendered by the UI. Also supports ephemeral changes
which can be undone later.

We'll add some additional features to this class soon, as it is
only a subset of what we need.

Bug: 1044139
Change-Id: I1b8d6c6196c9102c0cd6696033b3083ccf350c47
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2083433
Commit-Queue: Dan H <harringtond@chromium.org>
Reviewed-by: default avatarIan Wells <iwells@chromium.org>
Cr-Commit-Position: refs/heads/master@{#746631}
parent 5c495213
......@@ -6,15 +6,11 @@ syntax = "proto3";
package feedstore;
option optimize_for = LITE_RUNTIME;
import "components/feed/core/proto/ui/piet/piet.proto";
import "components/feed/core/proto/ui/action/ui_feed_action.proto";
import "components/feed/core/proto/ui/stream/stream_structure.proto";
import "components/feed/core/proto/wire/action_payload.proto";
import "components/feed/core/proto/wire/content_id.proto";
import "components/feed/core/proto/wire/piet_shared_state_item.proto";
import "components/feed/core/proto/wire/semantic_properties.proto";
option optimize_for = LITE_RUNTIME;
// Actual data stored by the client.
// This data is sourced from the wire protocol, which is converted upon receipt.
......@@ -22,18 +18,19 @@ import "components/feed/core/proto/wire/semantic_properties.proto";
//
// This is the 'value' in the key/value store.
// Keys are defined as:
// 'root' -> stream_data
// 'c:<id>' -> cluster
// 'la:<id>' -> local_action
// 'ua:<id>' -> uploadable_action
// 'ss:<id>' -> shared_state
// 'S/<stream-id>' -> stream_data
// 'c/<content-id>' -> content
// 'a/<id>' -> action
// 's/<content-id>' -> shared_state
// 'N' -> next_stream_state
message Record {
oneof data {
Cluster cluster = 1;
StreamData stream_data = 2;
StreamLocalAction local_action = 3;
StreamUploadableAction uploadable_action = 4;
StreamSharedState shared_state = 5;
StreamData stream_data = 1;
Content content = 2;
StoredAction local_action = 3;
StreamSharedState shared_state = 4;
// The result of a background refresh, to be processed later.
StreamAndContentState next_stream_state = 5;
}
}
......@@ -47,26 +44,58 @@ message StreamData {
bytes consistency_token = 3;
// The unix timestamp in milliseconds that the most recent content was added.
int64 last_added_time_millis = 4;
// List of all clusters, in order they would be shown in the stream.
repeated feedwire.ContentId cluster_ids = 5;
// List of all stale clusters. Stale clusters can be removed when no session
// references them.
repeated feedwire.ContentId stale_cluster_ids = 6;
// List of operations to perform to create the stream.
repeated StreamStructure structures = 5;
// Next sequential ID to be used for StoredAction.id.
int32 next_action_id = 6;
}
message Cluster {
feedwire.ContentId content_id = 1;
feedwire.SemanticProperties semantic_properties = 2;
repeated Card cards = 3;
// This is the structure of the stream. It is defined through the parent/child
// relationship and an operation. This message will be journaled. Reading
// the journal from start to end will fully define the structure of the stream.
message StreamStructure {
// The defined set of DataOperations
// These operations align with the Operation enum defined in
// data_operation.proto.
enum Operation {
UNKNOWN = 0;
// Clear all the content in the session, creating a new session
CLEAR_ALL = 1;
// Append if not present or update
UPDATE_OR_APPEND = 2;
// Remove the node from its parent
REMOVE = 3;
}
Operation operation = 1;
// The ContentId of the content.
feedwire.ContentId content_id = 2;
// The parent ContentId, or unset if this is the root.
feedwire.ContentId parent_id = 3;
// Type of node as denoted by the server. This type has no meaning for the
// client.
enum Type {
// Default type for operations that don't affect the stream (e.g. operations
// on shared states).
UNKNOWN_TYPE = 0;
// The root of the stream.
STREAM = 1;
// An internal tree node, which may have children.
CARD = 2;
// A leaf node, which provides content.
CONTENT = 3;
// An internal tree node, which may have children.
CLUSTER = 4;
}
Type type = 4;
// Set iff type=CONTENT
ContentInfo content_info = 5;
}
message Card {
feedwire.ContentId card_id = 1;
feedwire.SemanticProperties semantic_properties = 2;
// This may not be present if Card is part of a DataOperation.
Content content = 3;
// All actions bound to this card or content.
repeated components.feed.core.proto.ui.action.FeedActionMetadata actions = 4;
message DataOperation {
StreamStructure structure = 1;
// Provided if structure adds content.
Content content = 2;
}
message RepresentationData {
......@@ -76,63 +105,44 @@ message RepresentationData {
int64 published_time_seconds = 2;
}
message Content {
// The parent Card's ContentId.
feedwire.ContentId content_id = 1;
feedwire.SemanticProperties semantic_properties = 2;
enum ContentType {
UNKNOWN_CONTENT = 0;
PIET = 1;
}
ContentType content_type = 3;
PietContent piet_content = 4;
message ContentInfo {
// Score given by server.
float score = 5;
float score = 1;
// Unix timestamp (seconds) that content was received by Chrome.
int64 availability_time_seconds = 6;
RepresentationData representation_data = 7;
components.feed.core.proto.ui.stream.OfflineMetadata offline_metadata = 8;
components.feed.core.proto.ui.action.FeedActionMetadata swipe_action = 9;
int64 availability_time_seconds = 2;
RepresentationData representation_data = 3;
components.feed.core.proto.ui.stream.OfflineMetadata offline_metadata = 4;
}
// Content which is able to show a Piet frame. This includes any data which may
// be needed to show a Piet frame.
message PietContent {
// Content Ids of Piet Shared States which should be provided to Piet in order
// to show its content.
repeated feedwire.ContentId piet_shared_states = 1;
// The Piet frame to render. This is the same frame as sent over the wire,
// except the action extensions have been replaced by ui.Action.
components.feed.core.proto.ui.piet.Frame frame = 2;
message Content {
feedwire.ContentId content_id = 1;
// Opaque content. The UI layer knows how to parse and render this as a slice.
bytes frame = 2;
}
// This represents a shared state item.
message StreamSharedState {
feedwire.ContentId content_id = 1;
feedwire.PietSharedStateItem piet_shared_state_item = 2;
// Opaque data required to render content.
bytes shared_state_data = 2;
}
// An action that should be eventually uploaded, unless it is undone.
message StreamLocalAction {
enum Type {
UNKNOWN = 0;
DISMISS = 1;
}
Type action = 1;
feedwire.ContentId feature_content_id = 2;
// When the action was recorded
int64 timestamp_seconds = 3;
// A stored action awaiting upload.
message StoredAction {
// Unique ID for this stored action, provided by the client.
// This is a sequential number, so that the action with the lowest id value
// was recorded chronologically first.
int32 id = 1;
// How many times we have tried to upload the action.
int32 upload_attempt_count = 2;
// The action to upload.
components.feed.core.proto.ui.action.FeedAction action = 3;
}
// An action ready to be uploaded.
message StreamUploadableAction {
feedwire.ContentId feature_content_id = 1;
// The number of time this action was attempted to be recorded
int32 upload_attempts = 2;
// Unix timestamp (seconds) when the action was recorded
int64 timestamp_seconds = 3;
feedwire.ActionPayload payload = 4;
// The internal version of the server response. Includes feature tree and
// content.
message StreamAndContentState {
StreamData stream_data = 1;
repeated Content content = 2;
repeated StreamSharedState shared_state = 3;
}
......@@ -6,118 +6,48 @@ syntax = "proto3";
package feedui;
option optimize_for = LITE_RUNTIME;
import "components/feed/core/proto/ui/piet/piet.proto";
option optimize_for = LITE_RUNTIME;
// This is a simplified and complete set of protos that define UI.
// It includes everything from search.now.ui needed in the UI, and excludes
// other data to reduce complexity. These proto messages should be constructible
// from the store protos.
// A stream is a list of cards in order.
// Each StreamUpdate contains the full list of cards,
// A stream is a list of chunks in order.
// Each StreamUpdate contains the full list of chunks,
// but subsequent StreamUpdates after the first may refer to
// cards previously received by card_id.
// chunks previously received by chunk_id.
message StreamUpdate {
// Either a reference to an existing card, or a new card.
message CardUpdate {
// Either a reference to an existing slice, or a new slice.
message SliceUpdate {
oneof update {
Card card = 1;
string card_id = 2;
Slice slice = 1;
string slice_id = 2;
}
}
// One entry for each card in the stream, in the order they should be
// presented. Existing cards not present in updated_cards should be dropped.
repeated CardUpdate updated_cards = 1;
// One entry for each slice in the stream, in the order they should be
// presented. Existing slices not present in updated_slices should be dropped.
repeated SliceUpdate updated_slices = 1;
// Additional shared states to be used. Usually just one, and sent only on the
// first update.
repeated SharedState new_shared_states = 2;
// True if all cards are already shown. The 'more' button will be
// hidden if this is true.
bool no_more_cards = 3;
}
// A card rendered with Piet.
message Card {
// An opaque unique ID.
string card_id = 1;
repeated string piet_shared_state_ids = 2;
// The piet frame. The frame contains Action messages through an
// extension of search.now.ui.piet.Action.
components.feed.core.proto.ui.piet.Frame frame = 3;
bool can_swipe = 4;
bool can_tap = 5;
// And other action availability...
// A horizontal slice of UI to be presented in the vertical-scrolling feed.
message Slice {
oneof SliceData { XSurfaceSlice xsurface_slice = 1; }
}
// True if the card is stale. Stale cards shouldn't be shown unless the user
// has already seen it on the same surface.
bool is_stale = 6;
message XSurfaceSlice {
bytes xsurface_frame = 1;
}
// Wraps a piet shared state with a unique ID.
// Wraps an XSurface shared state with a unique ID.
message SharedState {
string id = 1;
components.feed.core.proto.ui.piet.PietSharedState piet_shared_state = 2;
}
// Piet attaches actions to many things: entities, frames, text, etc...,
// and uses one Action for each user-action: tap, swipe, long-press, etc...
// We can swap out PietFeedActionPayload for the 'Action' message below
// before sending the protos over to java.
// See ui_extension.proto. This message extends ui.piet.Action.
message Action {
// Always sent to C++ when action is triggered.
string action_id = 1;
// The following fields are only necessary for a subset of action types that
// have an effect on the feed UI.
oneof extra_data {
// Present if this is a context menu action.
ContextMenu context_menu = 2;
// Present if this is a tooltip action.
TooltipData tooltip = 3;
}
// Present if the action can be undone.
UndoAction undo = 4;
// If not empty, this is the card dismissed by this action.
// Used when undo is present, so that the UI can hide the dismissed card
// before the dismiss is committed.
string dismiss_card_id = 5;
}
message ContextMenuEntry {
string label = 1;
int32 id = 2;
}
message ContextMenu {
repeated ContextMenuEntry entries = 1;
}
// These messages didn't change from now.ui.action.
message UndoAction {
// The string shown to the user that confirms the action was just taken.
string confirmation_label = 1;
// The string that labels that option to reverse the action. Defaults to
// "Undo" if not set.
string undo_label = 2;
}
message TooltipData {
message Insets {
int32 top = 1;
int32 bottom = 2;
}
enum FeatureName {
// No tooltip will render if the FeatureName is UNKNOWN.
UNKNOWN = 0;
CARD_MENU = 1;
}
string label = 1;
string accessibility_label = 2;
FeatureName feature_name = 3;
// The information for where to offset the arrow from the referenced view.
Insets insets = 4;
bytes xsurface_shared_state = 2;
}
// An event on the UI.
......@@ -149,7 +79,7 @@ message UiEvent {
INFINITE_FEED = 5;
}
// For CARD_* type events.
string card_id = 1;
string chunk_id = 1;
// For MORE_BUTTON_* type events.
int32 more_button_position = 2;
// For SPINNER_* type events.
......
......@@ -19,6 +19,14 @@ source_set("feed_core_v2") {
"feed_stream_api.h",
"feed_stream_background.cc",
"feed_stream_background.h",
"proto_util.cc",
"proto_util.h",
"stream_model.cc",
"stream_model.h",
"stream_model/ephemeral_change.cc",
"stream_model/ephemeral_change.h",
"stream_model/feature_tree.cc",
"stream_model/feature_tree.h",
]
deps = [
"//components/feed/core:feed_core",
......@@ -46,6 +54,7 @@ source_set("core_unit_tests") {
sources = [
"feed_network_impl_unittest.cc",
"feed_stream_unittest.cc",
"stream_model_unittest.cc",
]
deps = [
......
// Copyright 2020 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/v2/proto_util.h"
#include <tuple>
namespace feed {
bool Equal(const feedwire::ContentId& a, const feedwire::ContentId& b) {
return a.content_domain() == b.content_domain() && a.id() == b.id() &&
a.table() == b.table();
}
bool CompareContentId(const feedwire::ContentId& a,
const feedwire::ContentId& b) {
const int a_id = a.id(); // tie() needs l-values
const int b_id = b.id();
return std::tie(a.content_domain(), a_id, a.table()) <
std::tie(b.content_domain(), b_id, b.table());
}
} // namespace feed
// Copyright 2020 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.
#ifndef COMPONENTS_FEED_CORE_V2_PROTO_UTIL_H_
#define COMPONENTS_FEED_CORE_V2_PROTO_UTIL_H_
#include "components/feed/core/proto/wire/content_id.pb.h"
// Helper functions/classes for dealing with feed proto messages.
namespace feed {
bool Equal(const feedwire::ContentId& a, const feedwire::ContentId& b);
bool CompareContentId(const feedwire::ContentId& a,
const feedwire::ContentId& b);
class ContentIdCompareFunctor {
public:
bool operator()(const feedwire::ContentId& a,
const feedwire::ContentId& b) const {
return CompareContentId(a, b);
}
};
} // namespace feed
#endif // COMPONENTS_FEED_CORE_V2_PROTO_UTIL_H_
// Copyright 2020 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/v2/stream_model.h"
#include <algorithm>
#include <utility>
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/proto/wire/content_id.pb.h"
namespace feed {
StreamModel::UiUpdate::UiUpdate() = default;
StreamModel::UiUpdate::~UiUpdate() = default;
StreamModel::UiUpdate::UiUpdate(const UiUpdate&) = default;
StreamModel::UiUpdate& StreamModel::UiUpdate::operator=(const UiUpdate&) =
default;
StreamModel::StreamModel(Observer* observer) : observer_(observer) {}
StreamModel::~StreamModel() = default;
const feedstore::Content* StreamModel::FindContent(
ContentRevision revision) const {
return GetFinalFeatureTree()->FindContent(revision);
}
StreamModel::EphemeralChangeId StreamModel::CreateEphemeralChange(
std::vector<feedstore::DataOperation> operations) {
const EphemeralChangeId id =
ephemeral_changes_.AddEphemeralChange(std::move(operations))->id();
UpdateFlattenedTree();
return id;
}
void StreamModel::ExecuteOperations(
std::vector<feedstore::DataOperation> operations) {
for (feedstore::DataOperation& operation : operations) {
if (operation.has_structure()) {
base_feature_tree_.ApplyStreamStructure(operation.structure());
}
if (operation.has_content()) {
base_feature_tree_.AddContent(std::move(*operation.mutable_content()));
}
}
UpdateFlattenedTree();
}
bool StreamModel::CommitEphemeralChange(EphemeralChangeId id) {
std::unique_ptr<stream_model::EphemeralChange> change =
ephemeral_changes_.Remove(id);
if (!change)
return false;
// Note: it's possible that the does change even upon commit because it
// may change the order that operations are applied. ExecuteOperations
// will ensure observers are updated.
ExecuteOperations(change->GetOperations());
return true;
}
bool StreamModel::RejectEphemeralChange(EphemeralChangeId id) {
if (ephemeral_changes_.Remove(id)) {
UpdateFlattenedTree();
return true;
}
return false;
}
void StreamModel::UpdateFlattenedTree() {
if (ephemeral_changes_.GetChangeList().empty()) {
feature_tree_after_changes_.reset();
} else {
feature_tree_after_changes_ =
ApplyEphemeralChanges(base_feature_tree_, ephemeral_changes_);
}
std::vector<ContentRevision> new_state =
GetFinalFeatureTree()->GetVisibleContent();
UiUpdate update;
update.content_list_changed = content_list_ != new_state;
content_list_ = std::move(new_state);
observer_->OnUiUpdate(update);
}
stream_model::FeatureTree* StreamModel::GetFinalFeatureTree() {
return feature_tree_after_changes_ ? feature_tree_after_changes_.get()
: &base_feature_tree_;
}
const stream_model::FeatureTree* StreamModel::GetFinalFeatureTree() const {
return const_cast<StreamModel*>(this)->GetFinalFeatureTree();
}
} // namespace feed
// Copyright 2020 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.
#ifndef COMPONENTS_FEED_CORE_V2_STREAM_MODEL_H_
#define COMPONENTS_FEED_CORE_V2_STREAM_MODEL_H_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/proto/wire/content_id.pb.h"
#include "components/feed/core/v2/proto_util.h"
#include "components/feed/core/v2/stream_model/ephemeral_change.h"
#include "components/feed/core/v2/stream_model/feature_tree.h"
namespace feedwire {
class DataOperation;
} // namespace feedwire
namespace feed {
// An in-memory stream model.
class StreamModel {
public:
using ContentId = feedwire::ContentId;
using ContentRevision = stream_model::ContentRevision;
using EphemeralChangeId = stream_model::EphemeralChangeId;
// Information about an update to the model.
struct UiUpdate {
struct SharedStateInfo {
// The shared state's unique ID.
std::string shared_state_id;
// Whether the shared state was just modified or added.
bool updated = false;
};
UiUpdate();
~UiUpdate();
UiUpdate(const UiUpdate&);
UiUpdate& operator=(const UiUpdate&);
// Whether the list of content has changed. Use
// |StreamModel::GetContentList()| to get the updated list of content.
bool content_list_changed = false;
// The list of shared states in the model.
std::vector<SharedStateInfo> shared_states;
};
class Observer {
public:
// Called when the UI model changes.
virtual void OnUiUpdate(const UiUpdate&) = 0;
};
explicit StreamModel(Observer* observer);
~StreamModel();
StreamModel(const StreamModel& src) = delete;
StreamModel& operator=(const StreamModel&) = delete;
// Data access.
// Returns the full list of content in the order it should be presented.
const std::vector<ContentRevision>& GetContentList() const {
return content_list_;
}
// Returns the content identified by |ContentRevision|.
const feedstore::Content* FindContent(ContentRevision revision) const;
// Apply |operations| to the model.
void ExecuteOperations(std::vector<feedstore::DataOperation> operations);
// Create a temporary change that may be undone or committed later.
EphemeralChangeId CreateEphemeralChange(
std::vector<feedstore::DataOperation> operations);
// Commits a change. Returns false if the change does not exist.
bool CommitEphemeralChange(EphemeralChangeId id);
// Rejects a change. Returns false if the change does not exist.
bool RejectEphemeralChange(EphemeralChangeId id);
private:
// The final feature tree after applying any ephemeral changes.
// May link directly to |base_feature_tree_|.
stream_model::FeatureTree* GetFinalFeatureTree();
const stream_model::FeatureTree* GetFinalFeatureTree() const;
void UpdateFlattenedTree();
Observer* observer_; // Unowned.
stream_model::ContentIdMap id_map_;
stream_model::FeatureTree base_feature_tree_{&id_map_};
// |base_feature_tree_| with |ephemeral_changes_| applied.
// Null if there are no ephemeral changes.
std::unique_ptr<stream_model::FeatureTree> feature_tree_after_changes_;
stream_model::EphemeralChangeList ephemeral_changes_;
// Current state of the flattened tree.
// Updated after each tree change.
std::vector<ContentRevision> content_list_;
};
} // namespace feed
#endif // COMPONENTS_FEED_CORE_V2_STREAM_MODEL_H_
This directory contains implementation details for StreamModel.
// Copyright 2020 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/v2/stream_model/ephemeral_change.h"
namespace feed {
namespace stream_model {
EphemeralChange::EphemeralChange(
EphemeralChangeId id,
std::vector<feedstore::DataOperation> operations)
: id_(id), operations_(std::move(operations)) {}
EphemeralChange::~EphemeralChange() = default;
EphemeralChangeList::EphemeralChangeList() = default;
EphemeralChangeList::~EphemeralChangeList() = default;
EphemeralChange* EphemeralChangeList::AddEphemeralChange(
std::vector<feedstore::DataOperation> operations) {
change_list_.push_back(std::make_unique<EphemeralChange>(
id_generator_.GenerateNextId(), operations));
return change_list_.back().get();
}
EphemeralChange* EphemeralChangeList::Find(EphemeralChangeId id) {
for (std::unique_ptr<EphemeralChange>& change : change_list_) {
if (change->id() == id)
return change.get();
}
return nullptr;
}
std::unique_ptr<FeatureTree> ApplyEphemeralChanges(
const FeatureTree& tree,
const EphemeralChangeList& changes) {
auto tree_with_changes = std::make_unique<FeatureTree>(&tree);
for (const std::unique_ptr<EphemeralChange>& change :
changes.GetChangeList()) {
for (const feedstore::DataOperation& operation : change->GetOperations()) {
if (operation.has_structure()) {
tree_with_changes->ApplyStreamStructure(operation.structure());
}
if (operation.has_content()) {
tree_with_changes->AddContent(operation.content());
}
}
}
return tree_with_changes;
}
std::unique_ptr<EphemeralChange> EphemeralChangeList::Remove(
EphemeralChangeId id) {
for (size_t i = 0; i < change_list_.size(); ++i) {
if (change_list_[i]->id() == id) {
std::unique_ptr<EphemeralChange> result = std::move(change_list_[i]);
change_list_.erase(change_list_.begin() + i);
return result;
}
}
return nullptr;
}
} // namespace stream_model
} // namespace feed
// Copyright 2020 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.
#ifndef COMPONENTS_FEED_CORE_V2_STREAM_MODEL_EPHEMERAL_CHANGE_H_
#define COMPONENTS_FEED_CORE_V2_STREAM_MODEL_EPHEMERAL_CHANGE_H_
#include <memory>
#include <vector>
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/v2/stream_model/feature_tree.h"
namespace feed {
namespace stream_model {
using EphemeralChangeId = util::IdTypeU32<class EphemeralChangeIdClass>;
// A sequence of data operations that may be reverted.
class EphemeralChange {
public:
EphemeralChange(EphemeralChangeId id,
std::vector<feedstore::DataOperation> operations);
~EphemeralChange();
EphemeralChange(const EphemeralChange&) = delete;
EphemeralChange& operator=(const EphemeralChange&) = delete;
EphemeralChangeId id() const { return id_; }
const std::vector<feedstore::DataOperation>& GetOperations() const {
return operations_;
}
std::vector<feedstore::DataOperation>& GetOperations() { return operations_; }
private:
EphemeralChangeId id_;
std::vector<feedstore::DataOperation> operations_;
};
// A list of |EphemeralChange| objects.
class EphemeralChangeList {
public:
EphemeralChangeList();
~EphemeralChangeList();
EphemeralChangeList(const EphemeralChangeList&) = delete;
EphemeralChangeList& operator=(const EphemeralChangeList&) = delete;
const std::vector<std::unique_ptr<EphemeralChange>>& GetChangeList() const {
return change_list_;
}
EphemeralChange* Find(EphemeralChangeId id);
EphemeralChange* AddEphemeralChange(
std::vector<feedstore::DataOperation> operations);
std::unique_ptr<EphemeralChange> Remove(EphemeralChangeId id);
private:
EphemeralChangeId::Generator id_generator_;
std::vector<std::unique_ptr<EphemeralChange>> change_list_;
};
// Return a new |FeatureTree| by applying |changes| to |tree|.
std::unique_ptr<FeatureTree> ApplyEphemeralChanges(
const FeatureTree& tree,
const EphemeralChangeList& changes);
} // namespace stream_model
} // namespace feed
#endif // COMPONENTS_FEED_CORE_V2_STREAM_MODEL_EPHEMERAL_CHANGE_H_
// Copyright 2020 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/v2/stream_model/feature_tree.h"
#include "base/logging.h"
namespace feed {
namespace stream_model {
ContentIdMap::ContentIdMap() = default;
ContentIdMap::~ContentIdMap() = default;
ContentTag ContentIdMap::GetContentTag(const feedwire::ContentId& id) {
auto iter = mapping_.find(id);
if (iter != mapping_.end())
return iter->second;
ContentTag tag = tag_generator_.GenerateNextId();
mapping_[id] = tag;
return tag;
}
ContentRevision ContentIdMap::NextContentRevision() {
return revision_generator_.GenerateNextId();
}
StreamNode::StreamNode() = default;
StreamNode::~StreamNode() = default;
StreamNode::StreamNode(const StreamNode&) = default;
StreamNode& StreamNode::operator=(const StreamNode&) = default;
FeatureTree::FeatureTree(ContentIdMap* id_map) : id_map_(id_map) {}
FeatureTree::FeatureTree(const FeatureTree* base)
: base_(base),
id_map_(base->id_map_),
computed_root_(base->computed_root_),
root_tag_(base->root_tag_),
nodes_(base->nodes_) {}
FeatureTree::~FeatureTree() = default;
StreamNode* FeatureTree::GetOrMakeNode(ContentTag id) {
ResizeNodesIfNeeded(id);
return &nodes_[id.value()];
}
const StreamNode* FeatureTree::FindNode(ContentTag id) const {
return const_cast<FeatureTree*>(this)->FindNode(id);
}
StreamNode* FeatureTree::FindNode(ContentTag id) {
if (!id.is_null() && nodes_.size() > id.value())
return &nodes_[id.value()];
return nullptr;
}
const feedstore::Content* FeatureTree::FindContent(ContentRevision id) const {
auto iter = content_.find(id);
if (iter != content_.end())
return &iter->second;
return base_ ? base_->FindContent(id) : nullptr;
}
void FeatureTree::ApplyStreamStructure(
const feedstore::StreamStructure& structure) {
switch (structure.operation()) {
case feedstore::StreamStructure::CLEAR_ALL:
nodes_.clear();
content_.clear();
computed_root_ = false;
break;
case feedstore::StreamStructure::UPDATE_OR_APPEND: {
const ContentTag child_id = GetContentTag(structure.content_id());
const bool is_stream =
structure.type() == feedstore::StreamStructure::STREAM;
ContentTag parent_id;
if (structure.has_parent_id()) {
parent_id = GetContentTag(structure.parent_id());
}
ResizeNodesIfNeeded(std::max(child_id, parent_id));
StreamNode& child = nodes_[child_id.value()];
StreamNode* parent = FindNode(parent_id);
// If a node already has a parent, treat this as an update, not an append
// operation.
child.is_stream = is_stream;
child.tombstoned = false;
if (root_tag_ == child_id) {
computed_root_ = false;
}
if (parent && !child.has_parent) {
// The child doesn't yet have a parent, but it should. Link to the
// parent now. If the child already has a parent, we will never change
// the parent even if requested by UPDATE_OR_APPEND.
child.has_parent = true;
child.previous_sibling = parent->last_child;
parent->last_child = child_id;
} else if (!parent && is_stream) {
// The node meets the criteria for root.
computed_root_ = true;
root_tag_ = child_id;
}
} break;
case feedstore::StreamStructure::REMOVE: {
// Removal is just unlinking the node from the tree.
// If it's added back again later, it retains its old children.
ContentTag tag = GetContentTag(structure.content_id());
if (root_tag_ == tag) {
computed_root_ = false;
}
GetOrMakeNode(tag)->tombstoned = true;
} break;
default:
break;
}
} // namespace stream_model
void FeatureTree::ResizeNodesIfNeeded(ContentTag id) {
if (nodes_.size() <= id.value())
nodes_.resize(id.value() + 1);
}
void FeatureTree::AddContent(feedstore::Content content) {
AddContent(id_map_->NextContentRevision(), std::move(content));
}
void FeatureTree::AddContent(ContentRevision revision_id,
feedstore::Content content) {
// TODO(harringtond): Consider de-duping content.
// Currently, we copy content for ephemeral changes. Both when the ephemeral
// change is created, and when it is committed. We should consider eliminating
// these copies.
const ContentTag tag = GetContentTag(content.content_id());
DCHECK(!content_.count(revision_id));
GetOrMakeNode(tag)->content_revision = revision_id;
content_[revision_id] = std::move(content);
}
void FeatureTree::ResolveRoot() {
if (computed_root_) {
DCHECK(!FindNode(root_tag_) || FindNode(root_tag_)->is_stream) << root_tag_;
DCHECK(!FindNode(root_tag_) || !FindNode(root_tag_)->tombstoned);
DCHECK(!FindNode(root_tag_) || !FindNode(root_tag_)->has_parent);
return;
}
root_tag_ = ContentTag();
for (size_t i = 0; i < nodes_.size(); ++i) {
const StreamNode& node = nodes_[i];
if (node.is_stream && !node.tombstoned && !node.has_parent) {
root_tag_ = ContentTag(i);
}
}
computed_root_ = true;
}
std::vector<ContentRevision> FeatureTree::GetVisibleContent() {
ResolveRoot();
std::vector<ContentRevision> result;
std::vector<ContentTag> stack;
// Node: Cycles are impossible here. The root node is guaranteed to
// not be a child. All other nodes have exactly one parent.
// It is possible for nodes to cycle, like A->B->A, but in this case there can
// be no valid root because all nodes have a parent.
stack.push_back(root_tag_);
while (!stack.empty()) {
const ContentTag tag = stack.back();
stack.pop_back();
const StreamNode* node = FindNode(tag);
if (!node || node->tombstoned)
continue;
if (!node->last_child.is_null()) {
for (ContentTag child_id = node->last_child; !child_id.is_null();
child_id = nodes_[child_id.value()].previous_sibling) {
stack.push_back(child_id);
}
}
if (!node->content_revision.is_null()) {
result.push_back(node->content_revision);
}
}
return result;
}
} // namespace stream_model
} // namespace feed
// Copyright 2020 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.
#ifndef COMPONENTS_FEED_CORE_V2_STREAM_MODEL_FEATURE_TREE_H_
#define COMPONENTS_FEED_CORE_V2_STREAM_MODEL_FEATURE_TREE_H_
#include <map>
#include <vector>
#include "base/util/type_safety/id_type.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/v2/proto_util.h"
namespace feed {
namespace stream_model {
// Uniquely identifies a feedwire::ContentId. Provided by |ContentIdMap|.
using ContentTag = util::IdTypeU32<class ContentTagClass>;
// Uniquely identifies a revision of a |feedstore::Content|. If Content changes,
// it is assigned a new revision number.
using ContentRevision = util::IdTypeU32<class ContentRevisionClass>;
// Maps ContentId into ContentTag, and generates ContentRevision IDs.
class ContentIdMap {
public:
ContentIdMap();
~ContentIdMap();
ContentIdMap(const ContentIdMap&) = delete;
ContentIdMap& operator=(const ContentIdMap&) = delete;
ContentTag GetContentTag(const feedwire::ContentId& id);
ContentRevision NextContentRevision();
private:
ContentTag::Generator tag_generator_;
ContentRevision::Generator revision_generator_;
std::map<feedwire::ContentId, ContentTag, ContentIdCompareFunctor> mapping_;
};
// A node in FeatureTree.
struct StreamNode {
StreamNode();
~StreamNode();
StreamNode(const StreamNode&);
StreamNode& operator=(const StreamNode&);
// If true, this nodes has been removed and should be ignored.
bool tombstoned = false;
// Whether this is a STREAM node.
bool is_stream = false;
// Whether this node has a parent.
bool has_parent = false;
// If this node has content, this identifies it.
ContentRevision content_revision;
// Child relations are stored in linked-list fashion.
// The ID of the last child, or null.
ContentTag last_child;
// The ID of the sibling before this one.
ContentTag previous_sibling;
};
// The feature tree which underlies StreamModel.
// This tree is different that most, the rules are as follows:
// * A node may or may not have a parent, so this is more of a forest than a
// tree.
// * When nodes are removed, their set of children are remembered. If the node
// is added again, it retains its old children.
// * A node can be added multiple times, but subsequent adds will not change
// the node's parent.
// * There is only one 'stream root' acknowledged, even though there can be many
// roots. The stream root is the last root node added of type STREAM. The
// stream root identifies the tree whose nodes are used to compute
// |GetVisibleContent()|.
// * A tree can be constructed with a base tree. This copies features from base,
// but refers to content stored in base by reference.
class FeatureTree {
public:
// Constructor. |id_map| is retained by FeatureTree, and must have a greater
// scope than FeatureTree.
explicit FeatureTree(ContentIdMap* id_map);
// Create a |FeatureTree| which starts as a copy of |base|.
// Copies structure from |base|, and keeps a reference for content access.
explicit FeatureTree(const FeatureTree* base);
~FeatureTree();
FeatureTree(const FeatureTree& src) = delete;
FeatureTree& operator=(const FeatureTree& src) = delete;
// Mutations.
void ApplyStreamStructure(const feedstore::StreamStructure& structure);
void AddContent(feedstore::Content content);
void AddContent(ContentRevision revision_id, feedstore::Content content);
// Data access.
const StreamNode* FindNode(ContentTag id) const;
StreamNode* FindNode(ContentTag id);
const feedstore::Content* FindContent(ContentRevision id) const;
ContentTag GetContentTag(const feedwire::ContentId& id) {
return id_map_->GetContentTag(id);
}
// Returns the list of content that should be visible.
std::vector<ContentRevision> GetVisibleContent();
private:
StreamNode* GetOrMakeNode(ContentTag id);
void ResolveRoot();
void ResizeNodesIfNeeded(ContentTag id);
void RemoveFromParent(ContentTag node_id);
bool RemoveFromParent(StreamNode* parent, ContentTag node_id);
const FeatureTree* base_ = nullptr; // Unowned.
ContentIdMap* id_map_; // Unowned.
// Finding the root:
// We pick the root node as the last STREAM node which has no parent.
// In most cases, we can identify the root as the tree is built.
// But in some cases, we need to search all nodes to find the root.
// |computed_root_| is true if |root_tag_| is guaranteed to identify the root.
bool computed_root_ = true;
ContentTag root_tag_;
// All nodes in the forest, included removed nodes.
// This datastructure was selected to make copies efficient.
std::vector<StreamNode> nodes_;
// TODO(harringtond): It may be possible to remove old revisions of content
// to save memory.
std::map<ContentRevision, feedstore::Content> content_;
};
} // namespace stream_model
} // namespace feed
#endif // COMPONENTS_FEED_CORE_V2_STREAM_MODEL_FEATURE_TREE_H_
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment