blob: 4130b46c056e6205fde873d05cec65326b16eeb4 [file] [log] [blame]
// Copyright 2023 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/feed/core/v2/view_demotion.h"
#include <algorithm>
#include <map>
#include <ostream>
#include <tuple>
#include <vector>
#include "base/check_op.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/v2/config.h"
#include "components/feed/core/v2/feed_store.h"
#include "components/feed/core/v2/feed_stream.h"
#include "components/feed/core/v2/feedstore_util.h"
#include "components/feed/feed_feature_list.h"
namespace feed {
namespace {
base::TimeDelta kMaxViewAge = base::Hours(72);
bool IsEnabled(FeedStream& feed_stream) {
return base::FeatureList::IsEnabled(kFeedSignedOutViewDemotion) &&
!feed_stream.IsSignedIn();
}
} // namespace
namespace internal {
DocViewDigest CreateDigest(std::vector<feedstore::DocView> all_views) {
const base::Time now = base::Time::Now();
const base::Time too_old = now - kMaxViewAge;
DocViewDigest digest;
// Remove all views that are too old.
std::erase_if(all_views, [&](const feedstore::DocView& view) {
base::Time view_time =
feedstore::FromTimestampMillis(view.view_time_millis());
if (view_time > now || view_time < too_old) {
digest.old_doc_views.push_back(view);
return true;
}
return false;
});
// Note that all_views will be sorted by the datastore key, which is prefixed
// with the docid, and therefore we can assume that all views for a given
// docid are adjacent in this list. We take advantage of this here.
for (const feedstore::DocView& view : all_views) {
if (digest.doc_view_counts.empty() ||
digest.doc_view_counts.back().docid != view.docid()) {
digest.doc_view_counts.push_back(DocViewCount{view.docid(), 1});
} else {
++digest.doc_view_counts.back().view_count;
}
}
// If there are too many recent entries, remove docids which have been viewed
// least recently.
if (digest.doc_view_counts.size() > GetFeedConfig().max_docviews_to_send) {
// Get the most recent view time for each doc, sort by time and select the
// docids to remove.
size_t remove_count =
digest.doc_view_counts.size() - GetFeedConfig().max_docviews_to_send;
base::UmaHistogramCounts100(
"ContentSuggestions.Feed.DroppedDocumentViewCount", remove_count);
std::vector<std::pair<int64_t, uint64_t>> latest_view_to_docid;
for (const feedstore::DocView& view : all_views) {
if (latest_view_to_docid.empty() ||
latest_view_to_docid.back().second != view.docid()) {
latest_view_to_docid.emplace_back(view.view_time_millis(),
view.docid());
} else {
auto& view_time = latest_view_to_docid.back().first;
view_time = std::max(view_time, view.view_time_millis());
}
}
std::ranges::sort(latest_view_to_docid);
std::vector<uint64_t> docids_to_remove;
docids_to_remove.reserve(remove_count);
for (auto iter = latest_view_to_docid.begin();
iter != latest_view_to_docid.begin() + remove_count; ++iter) {
docids_to_remove.push_back(iter->second);
}
// Finally, remove the old docids, and add to old_doc_views.
base::flat_set<uint64_t> docids_to_remove_set(std::move(docids_to_remove));
std::erase_if(digest.doc_view_counts, [&](const DocViewCount& view_count) {
return docids_to_remove_set.contains(view_count.docid);
});
for (const feedstore::DocView& view : all_views) {
if (docids_to_remove_set.contains(view.docid())) {
digest.old_doc_views.push_back(view);
}
}
DCHECK_EQ(digest.doc_view_counts.size(),
GetFeedConfig().max_docviews_to_send);
}
base::UmaHistogramCounts100(
"ContentSuggestions.Feed.DocumentViewSendCount100",
digest.doc_view_counts.size());
base::UmaHistogramCounts1000(
"ContentSuggestions.Feed.DocumentViewSendCount1000",
digest.doc_view_counts.size());
return digest;
}
} // namespace internal
bool DocViewCount::operator==(const DocViewCount& rhs) const {
return std::tie(docid, view_count) == std::tie(rhs.docid, rhs.view_count);
}
void ReadDocViewDigestIfEnabled(
FeedStream& feed_stream,
base::OnceCallback<void(DocViewDigest)> callback) {
if (!IsEnabled(feed_stream)) {
std::move(callback).Run({});
return;
}
auto callback_wrapper = [](base::OnceCallback<void(DocViewDigest)> callback,
std::vector<feedstore::DocView> all_views) {
std::move(callback).Run(internal::CreateDigest(std::move(all_views)));
};
feed_stream.GetStore().ReadDocViews(
base::BindOnce(callback_wrapper, std::move(callback)));
}
void RemoveOldDocViews(const DocViewDigest& doc_view_digest,
FeedStore& feed_store) {
feed_store.RemoveDocViews(doc_view_digest.old_doc_views);
}
void WriteDocViewIfEnabled(FeedStream& feed_stream, uint64_t docid) {
if (!IsEnabled(feed_stream)) {
return;
}
feed_stream.GetStore().WriteDocView(feedstore::CreateDocView(docid));
}
DocViewDigest::DocViewDigest() = default;
DocViewDigest::~DocViewDigest() = default;
DocViewDigest::DocViewDigest(const DocViewDigest&) = default;
DocViewDigest::DocViewDigest(DocViewDigest&&) = default;
DocViewDigest& DocViewDigest::operator=(const DocViewDigest&) = default;
DocViewDigest& DocViewDigest::operator=(DocViewDigest&&) = default;
std::ostream& operator<<(std::ostream& os, const DocViewCount& doc_view_count) {
return os << "DocViewCount{docid: " << doc_view_count.docid
<< " count: " << doc_view_count.view_count << " }";
}
} // namespace feed