Trim AutofillAiModelCache
This CL introduces a TrimEntries method to AutofillAiModelCacheImpl. TrimEntries does two things: - It removes entries (starting with the oldest) if the cache exceeds the target maximum size. - It removes entries once they exceed the maximum target age. The TrimEntries method is triggered whenever the DB is loaded (i.e., on start) and when a cache entry is updated/added. Bug: 389631477 Change-Id: If9e1fee273e6596121ba45514a9114fe97b2be04 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6317566 Reviewed-by: Christoph Schwering <schwering@google.com> Commit-Queue: Christoph Schwering <schwering@google.com> Cr-Commit-Position: refs/heads/main@{#1427183}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
5bac4ceea7
commit
c440026588
chrome/browser/autofill
components/autofill/core
@ -49,7 +49,9 @@ AutofillAiModelCacheFactory::BuildServiceInstanceForBrowserContext(
|
||||
Profile* profile = Profile::FromBrowserContext(context);
|
||||
return std::make_unique<AutofillAiModelCacheImpl>(
|
||||
profile->GetDefaultStoragePartition()->GetProtoDatabaseProvider(),
|
||||
profile->GetPath());
|
||||
profile->GetPath(),
|
||||
autofill::features::kAutofillAiServerModelCacheSize.Get(),
|
||||
autofill::features::kAutofillAiServerModelCacheAge.Get());
|
||||
}
|
||||
|
||||
bool AutofillAiModelCacheFactory::ServiceIsCreatedWithBrowserContext() const {
|
||||
|
@ -4,12 +4,13 @@
|
||||
|
||||
#include "components/autofill/core/browser/ml_model/autofill_ai/autofill_ai_model_cache_impl.h"
|
||||
|
||||
#include <iterator>
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/to_vector.h"
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/functional/bind.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
@ -20,9 +21,21 @@
|
||||
|
||||
namespace autofill {
|
||||
|
||||
namespace {
|
||||
|
||||
// A helper for serializing time used in this DB.
|
||||
constexpr int64_t SerializeTime(base::Time time) {
|
||||
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AutofillAiModelCacheImpl::AutofillAiModelCacheImpl(
|
||||
leveldb_proto::ProtoDatabaseProvider* db_provider,
|
||||
const base::FilePath& profile_path) {
|
||||
const base::FilePath& profile_path,
|
||||
size_t max_cache_size,
|
||||
base::TimeDelta max_cache_age)
|
||||
: max_cache_size_(max_cache_size), max_cache_age_(max_cache_age) {
|
||||
auto database_task_runner = base::ThreadPool::CreateSequencedTaskRunner(
|
||||
{base::MayBlock(), base::TaskPriority::BEST_EFFORT,
|
||||
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
|
||||
@ -41,14 +54,59 @@ void AutofillAiModelCacheImpl::Update(FormSignature form_signature,
|
||||
CacheEntry entry) {
|
||||
CacheEntryWithMetadata entry_with_metadata;
|
||||
*entry_with_metadata.mutable_server_response() = std::move(entry);
|
||||
entry_with_metadata.set_creation_time(
|
||||
base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
|
||||
entry_with_metadata.set_creation_time(SerializeTime(base::Time::Now()));
|
||||
UpdateInDatabase(form_signature, entry_with_metadata);
|
||||
in_memory_cache_[form_signature] = std::move(entry_with_metadata);
|
||||
TrimEntries();
|
||||
}
|
||||
|
||||
bool AutofillAiModelCacheImpl::Contains(FormSignature form_signature) const {
|
||||
return in_memory_cache_.contains(form_signature);
|
||||
auto it = in_memory_cache_.find(form_signature);
|
||||
return it != in_memory_cache_.end() &&
|
||||
it->second.creation_time() >=
|
||||
SerializeTime(base::Time::Now() - max_cache_age_);
|
||||
}
|
||||
|
||||
void AutofillAiModelCacheImpl::TrimEntries() {
|
||||
// Transform the cache into an ordered list of (creation_time, FormSignature)
|
||||
// pairs.
|
||||
std::vector<std::pair<int64_t, FormSignature>> entries_by_creation_time =
|
||||
base::ToVector(in_memory_cache_,
|
||||
[](const std::pair<FormSignature, CacheEntryWithMetadata>&
|
||||
map_entry) {
|
||||
const auto& [signature, cache_entry] = map_entry;
|
||||
return std::pair(cache_entry.creation_time(), signature);
|
||||
});
|
||||
std::ranges::sort(entries_by_creation_time);
|
||||
|
||||
// Entries with a time stamp smaller than `cut_off_time` should be removed.
|
||||
const int64_t cut_off_time =
|
||||
SerializeTime(base::Time::Now() - max_cache_age_);
|
||||
const size_t deletions_due_to_time_cutoff = std::distance(
|
||||
entries_by_creation_time.begin(),
|
||||
std::ranges::lower_bound(entries_by_creation_time, cut_off_time, {},
|
||||
&std::pair<int64_t, FormSignature>::first));
|
||||
// The remaining cache should at most have size `max_cache_size_`.
|
||||
size_t deletions_overall = deletions_due_to_time_cutoff;
|
||||
if (entries_by_creation_time.size() - deletions_overall > max_cache_size_) {
|
||||
deletions_overall = entries_by_creation_time.size() - max_cache_size_;
|
||||
}
|
||||
|
||||
if (deletions_overall == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto entries_to_save = std::make_unique<
|
||||
leveldb_proto::ProtoDatabase<CacheEntryWithMetadata>::KeyEntryVector>();
|
||||
auto keys_to_remove = std::make_unique<std::vector<std::string>>();
|
||||
keys_to_remove->reserve(deletions_overall);
|
||||
for (const auto [_, signature_to_delete] :
|
||||
base::span(entries_by_creation_time).first(deletions_overall)) {
|
||||
in_memory_cache_.erase(signature_to_delete);
|
||||
keys_to_remove->emplace_back(base::NumberToString(*signature_to_delete));
|
||||
}
|
||||
db_->UpdateEntries(std::move(entries_to_save), std::move(keys_to_remove),
|
||||
/*callback=*/base::DoNothing());
|
||||
}
|
||||
|
||||
void AutofillAiModelCacheImpl::UpdateInDatabase(
|
||||
@ -88,10 +146,12 @@ void AutofillAiModelCacheImpl::OnDatabaseLoadKeysAndEntries(
|
||||
for (auto& [signature_str, entry] : *entries) {
|
||||
uint64_t signature_uint64;
|
||||
if (!base::StringToUint64(signature_str, &signature_uint64)) {
|
||||
// TODO(crbug.com/389631477): Clean up malformed entries.
|
||||
continue;
|
||||
}
|
||||
in_memory_cache_[FormSignature(signature_uint64)] = std::move(entry);
|
||||
}
|
||||
TrimEntries();
|
||||
}
|
||||
|
||||
} // namespace autofill
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#include "base/files/file_path.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/time/time.h"
|
||||
#include "components/autofill/core/browser/ml_model/autofill_ai/autofill_ai_model_cache.h"
|
||||
#include "components/autofill/core/browser/proto/autofill_ai_model_cache.pb.h"
|
||||
#include "components/autofill/core/common/signatures.h"
|
||||
@ -32,12 +33,12 @@ inline constexpr base::FilePath::StringViewType
|
||||
// `AutofillAiModelCacheImpl` implements `AutofillAiModelCache` using a
|
||||
// LevelDB as a persistence layer and a map as an in-memory cache to allow
|
||||
// synchronous access.
|
||||
// TODO(crbug.com/389631477): Implement a maximum size of the cache.
|
||||
// TODO(crbug.com/389631477): Implement a maximum lifetime for cache entries.
|
||||
class AutofillAiModelCacheImpl : public AutofillAiModelCache {
|
||||
public:
|
||||
AutofillAiModelCacheImpl(leveldb_proto::ProtoDatabaseProvider* db_provider,
|
||||
const base::FilePath& profile_path);
|
||||
const base::FilePath& profile_path,
|
||||
size_t max_cache_size,
|
||||
base::TimeDelta max_cache_age);
|
||||
AutofillAiModelCacheImpl(const AutofillAiModelCacheImpl&) = delete;
|
||||
AutofillAiModelCacheImpl& operator=(const AutofillAiModelCacheImpl&) = delete;
|
||||
AutofillAiModelCacheImpl(AutofillAiModelCacheImpl&&) = delete;
|
||||
@ -51,6 +52,9 @@ class AutofillAiModelCacheImpl : public AutofillAiModelCache {
|
||||
private:
|
||||
using CacheEntryWithMetadata = AutofillAiModelCacheEntryWithMetadata;
|
||||
|
||||
// Removes expired cache entries and limits the cache size to `max_cache_size`
|
||||
// by removing the oldest entries.
|
||||
void TrimEntries();
|
||||
void UpdateInDatabase(FormSignature form_signature,
|
||||
const CacheEntryWithMetadata& entry);
|
||||
|
||||
@ -63,6 +67,9 @@ class AutofillAiModelCacheImpl : public AutofillAiModelCache {
|
||||
return weak_ptr_factory_.GetWeakPtr();
|
||||
}
|
||||
|
||||
const size_t max_cache_size_;
|
||||
const base::TimeDelta max_cache_age_;
|
||||
|
||||
// An in-memory cache that allows for synchronous access. Should contain the
|
||||
// same content as the database.
|
||||
std::map<FormSignature, CacheEntryWithMetadata> in_memory_cache_;
|
||||
|
@ -8,7 +8,9 @@
|
||||
|
||||
#include "base/files/scoped_temp_dir.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "base/time/time.h"
|
||||
#include "components/autofill/core/browser/ml_model/autofill_ai/autofill_ai_model_cache.h"
|
||||
#include "components/autofill/core/browser/proto/autofill_ai_model_cache.pb.h"
|
||||
#include "components/autofill/core/common/signatures.h"
|
||||
#include "components/leveldb_proto/public/proto_database_provider.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
@ -33,12 +35,17 @@ class AutofillAiModelCacheImplTest : public ::testing::Test {
|
||||
task_environment_.RunUntilIdle();
|
||||
}
|
||||
|
||||
void AdvanceClock(base::TimeDelta delta) {
|
||||
task_environment_.AdvanceClock(delta);
|
||||
}
|
||||
|
||||
// Simulates restart of the browser by recreating the cache.
|
||||
void RecreateCache() {
|
||||
void RecreateCache(size_t max_cache_size = 50,
|
||||
base::TimeDelta max_cache_age = base::Days(7)) {
|
||||
// Process remaining operations.
|
||||
task_environment_.RunUntilIdle();
|
||||
cache_ = std::make_unique<AutofillAiModelCacheImpl>(db_provider_.get(),
|
||||
temp_dir_.GetPath());
|
||||
cache_ = std::make_unique<AutofillAiModelCacheImpl>(
|
||||
db_provider_.get(), temp_dir_.GetPath(), max_cache_size, max_cache_age);
|
||||
// Wait until database has loaded.
|
||||
task_environment_.RunUntilIdle();
|
||||
}
|
||||
@ -46,7 +53,8 @@ class AutofillAiModelCacheImplTest : public ::testing::Test {
|
||||
AutofillAiModelCache& cache() { return *cache_; }
|
||||
|
||||
private:
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
base::test::TaskEnvironment task_environment_{
|
||||
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
|
||||
base::ScopedTempDir temp_dir_;
|
||||
std::unique_ptr<leveldb_proto::ProtoDatabaseProvider> db_provider_;
|
||||
std::unique_ptr<AutofillAiModelCache> cache_;
|
||||
@ -77,6 +85,77 @@ TEST_F(AutofillAiModelCacheImplTest, CacheSurvivesRestart) {
|
||||
EXPECT_TRUE(cache().Contains(signature));
|
||||
}
|
||||
|
||||
// Tests that the maximum cache size is enforced by removing the oldest entries
|
||||
// that exceed the cache size.
|
||||
TEST_F(AutofillAiModelCacheImplTest, MaxCacheSize) {
|
||||
constexpr auto signature1 = FormSignature(123);
|
||||
constexpr auto signature2 = FormSignature(1234);
|
||||
constexpr auto signature3 = FormSignature(12345);
|
||||
constexpr auto signature4 = FormSignature(123456);
|
||||
|
||||
RecreateCache(/*max_cache_size=*/3);
|
||||
cache().Update(signature1, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
cache().Update(signature2, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
cache().Update(signature3, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
EXPECT_TRUE(cache().Contains(signature1));
|
||||
EXPECT_TRUE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
EXPECT_FALSE(cache().Contains(signature4));
|
||||
|
||||
// Adding a fourth entry removes the first one.
|
||||
cache().Update(signature4, AutofillAiModelCache::CacheEntry());
|
||||
EXPECT_FALSE(cache().Contains(signature1));
|
||||
EXPECT_TRUE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
EXPECT_TRUE(cache().Contains(signature4));
|
||||
|
||||
// This remains true after a restart.
|
||||
RecreateCache();
|
||||
EXPECT_FALSE(cache().Contains(signature1));
|
||||
EXPECT_TRUE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
EXPECT_TRUE(cache().Contains(signature4));
|
||||
}
|
||||
|
||||
// Tests that the maximum cache age is enforced.
|
||||
TEST_F(AutofillAiModelCacheImplTest, MaxCacheAge) {
|
||||
constexpr auto signature1 = FormSignature(123);
|
||||
constexpr auto signature2 = FormSignature(1234);
|
||||
constexpr auto signature3 = FormSignature(12345);
|
||||
|
||||
RecreateCache(/*max_cache_size=*/10, /*max_cache_age=*/base::Days(3));
|
||||
cache().Update(signature1, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
cache().Update(signature2, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
cache().Update(signature3, AutofillAiModelCache::CacheEntry());
|
||||
AdvanceClock(base::Days(1));
|
||||
EXPECT_TRUE(cache().Contains(signature1));
|
||||
EXPECT_TRUE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
|
||||
// If we advance the clock further, the first entry expires.
|
||||
AdvanceClock(base::Hours(1));
|
||||
EXPECT_FALSE(cache().Contains(signature1));
|
||||
EXPECT_TRUE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
|
||||
// A day later, the second entry expires as well.
|
||||
AdvanceClock(base::Days(1));
|
||||
EXPECT_FALSE(cache().Contains(signature1));
|
||||
EXPECT_FALSE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
|
||||
// This is still true after a restart.
|
||||
RecreateCache(/*max_cache_size=*/10, /*max_cache_age=*/base::Days(3));
|
||||
EXPECT_FALSE(cache().Contains(signature1));
|
||||
EXPECT_FALSE(cache().Contains(signature2));
|
||||
EXPECT_TRUE(cache().Contains(signature3));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace autofill
|
||||
|
@ -57,6 +57,17 @@ BASE_FEATURE(kAutofillAiServerModel,
|
||||
"AutofillAiServerModel",
|
||||
base::FEATURE_DISABLED_BY_DEFAULT);
|
||||
|
||||
// The maximum duration for which an AutofillAI server model response is kept in
|
||||
// the local cache. NOTE: It is advisable to choose a value that is at least as
|
||||
// large as the cache duration for Autofill server responses to limit cases in
|
||||
// which the model is run multiple times for the same form.
|
||||
const base::FeatureParam<base::TimeDelta> kAutofillAiServerModelCacheAge{
|
||||
&kAutofillAiServerModel, "autofill_ai_model_cache_age", base::Days(7)};
|
||||
|
||||
// The maximum size of the AutofillAI server model cache.
|
||||
const base::FeatureParam<int> kAutofillAiServerModelCacheSize{
|
||||
&kAutofillAiServerModel, "autofill_ai_model_cache_size", 100};
|
||||
|
||||
// Enables the second iteration AutofillAI.
|
||||
// This feature is independent of `autofill_ai::kAutofillAi`.
|
||||
BASE_FEATURE(kAutofillAiWithDataSchema,
|
||||
|
@ -26,6 +26,10 @@ BASE_DECLARE_FEATURE(kAutofillAddressUserPerceptionSurvey);
|
||||
COMPONENT_EXPORT(AUTOFILL)
|
||||
BASE_DECLARE_FEATURE(kAutofillAiServerModel);
|
||||
COMPONENT_EXPORT(AUTOFILL)
|
||||
extern const base::FeatureParam<base::TimeDelta> kAutofillAiServerModelCacheAge;
|
||||
COMPONENT_EXPORT(AUTOFILL)
|
||||
extern const base::FeatureParam<int> kAutofillAiServerModelCacheSize;
|
||||
COMPONENT_EXPORT(AUTOFILL)
|
||||
BASE_DECLARE_FEATURE(kAutofillAiWithDataSchema);
|
||||
COMPONENT_EXPORT(AUTOFILL)
|
||||
extern const base::FeatureParam<int>
|
||||
|
Reference in New Issue
Block a user