blob: 78cb58d6e3ecb4b41dca09dba97ef35f283089d9 [file]
// Copyright 2025 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/persistent_cache/persistent_cache.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/auto_reset.h"
#include "base/containers/heap_array.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/function_ref.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "components/persistent_cache/backend_storage.h"
#include "components/persistent_cache/backend_type.h"
#include "components/persistent_cache/client.h"
#include "components/persistent_cache/pending_backend.h"
#include "components/persistent_cache/sqlite/sqlite_backend_impl.h"
#include "components/persistent_cache/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/perf/perf_result_reporter.h"
#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif
namespace persistent_cache {
// A test harness parameterized on the options for creating a PersistentCache.
class PersistentCachePerftest
: public testing::TestWithParam<std::tuple<bool, bool>> {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
backend_storage_.emplace(Client::kTest, BackendType::kSqlite,
temp_dir_.GetPath());
}
public:
// Returns a string representing the test parameter for story names.
static std::string GetParamName(std::tuple<bool, bool> param) {
auto [single_connection, journal_mode_wal] = param;
if (single_connection && journal_mode_wal) {
return "JournalModeWal";
} else if (single_connection && !journal_mode_wal) {
return "SingleConnection";
} else if (!single_connection && journal_mode_wal) {
return "MultipleConnectionsWal";
} else {
return "MultipleConnections";
}
}
// Returns a new cache configured according to the test's parameter.
std::unique_ptr<PersistentCache> MakeCache() {
auto [single_connection, journal_mode_wal] = GetParam();
return CreateCache(single_connection, journal_mode_wal);
}
// Returns a new cache with the given options.
std::unique_ptr<PersistentCache> CreateCache(bool single_connection,
bool journal_mode_wal) {
if (auto pending_backend = backend_storage_->MakePendingBackend(
base::FilePath(kBaseName), single_connection, journal_mode_wal);
pending_backend.has_value()) {
if (auto cache_result =
PersistentCache::Bind(Client::kTest, *std::move(pending_backend));
cache_result.has_value()) {
return *std::move(cache_result);
}
}
ADD_FAILURE() << "Failed to make PendingBackend or Bind it";
return nullptr;
}
// Returns true if caches created in this configuration can be shared across
// multiple connections.
static bool CanShareConnections() { return !std::get<0>(GetParam()); }
// Returns true if caches created in this configuration use the write-ahead
// log.
static bool IsWalMode() { return std::get<1>(GetParam()); }
std::optional<PendingBackend> ShareReadWriteConnection(
PersistentCache& cache) {
return backend_storage_->ShareReadWriteConnection(base::FilePath(kBaseName),
cache);
}
void RunAndTimeTest(std::string_view operation_name,
int iteration_count,
base::FunctionRef<void()> test_body) {
base::AutoReset<bool> resetter(&under_measurment_, true);
base::ElapsedTimer elapsed_timer;
base::ElapsedThreadTimer elapsed_thread_timer;
test_body();
ReportMeasurement(base::StrCat({operation_name, GetParamName(GetParam())}),
iteration_count, elapsed_timer.Elapsed(),
elapsed_thread_timer.Elapsed());
}
// Pregenerates keys. Use to avoid timing allocation overhead.
std::vector<std::string> GenerateKeys(int iteration_count) {
CHECK(!under_measurment_);
std::vector<std::string> keys(iteration_count);
std::generate(keys.begin(), keys.end(),
[i = 0]() mutable { return base::NumberToString(i++); });
return keys;
}
// Generates a value buffer to be inserted according to params. Should be done
// outside of timing to avoid measuring overhead.
base::HeapArray<uint8_t> MakeValue() {
CHECK(!under_measurment_);
// Median size of entries for a use case of PersistentCache as reported by
// UMA on November 7th 2025.
static constexpr size_t kValueSize = 6958;
auto value = base::HeapArray<uint8_t>::Uninit(kValueSize);
// Fill the data with random bytes to avoid unknown optimizations for
// identical pages.
base::RandBytes(value);
return value;
}
// Returns true if this platform has expensive database commits.
static bool HasExpensiveCommits() {
#if BUILDFLAG(IS_MAC)
// Commits are slow on macOS 12. Speculation: perhaps it does not benefit
// from F_BARRIERFSYNC.
return base::mac::MacOSMajorVersion() < 13;
#elif BUILDFLAG(IS_WIN)
return true;
#else
// Android and other POSIX systems appear to benefit from batch atomic
// writes.
return false;
#endif
}
void ReportMeasurement(std::string operation_name,
int iteration_count,
base::TimeDelta elapsed_time,
base::TimeDelta elapsed_thread_time) {
const std::string reporter_name("PersistentCache");
perf_test::PerfResultReporter reporter(reporter_name, operation_name);
reporter.RegisterImportantMetric(".wall_time", "us");
reporter.AddResult(
".wall_time",
static_cast<size_t>(elapsed_time.InMicroseconds() / iteration_count));
reporter.RegisterImportantMetric(".thread_time", "us");
reporter.AddResult(
".thread_time",
static_cast<size_t>(elapsed_thread_time.InMicroseconds() /
iteration_count));
}
static base::FilePath GetBaseName(int i) {
return base::FilePath(kBaseName).InsertBeforeExtensionASCII(
base::NumberToString(i));
}
BackendStorage& backend_storage() { return *backend_storage_; }
private:
static constexpr base::FilePath::StringViewType kBaseName =
FILE_PATH_LITERAL("perftest");
base::ScopedTempDir temp_dir_;
std::optional<BackendStorage> backend_storage_;
bool under_measurment_ = false;
};
TEST_P(PersistentCachePerftest, Create) {
int iteration_count = 1024;
if (HasExpensiveCommits()) {
iteration_count /= 4;
}
// iteration_count distinct backend names.
auto backend_names =
base::HeapArray<base::FilePath>::WithSize(iteration_count);
std::ranges::generate(backend_names,
[i = 0] mutable { return GetBaseName(i++); });
// Storage for iteration_count distinct caches.
auto caches = base::HeapArray<std::unique_ptr<PersistentCache>>::WithSize(
iteration_count);
// Creates or opens all iteration_count caches.
auto make_and_bind_caches = [&, single_connection = std::get<0>(GetParam()),
journal_mode_wal = std::get<1>(GetParam())] {
std::ranges::generate(
caches, [&, i = 0] mutable -> std::unique_ptr<PersistentCache> {
if (auto pending_backend = backend_storage().MakePendingBackend(
backend_names[i++], single_connection, journal_mode_wal);
pending_backend.has_value()) {
if (auto cache_result = PersistentCache::Bind(
Client::kTest, *std::move(pending_backend));
cache_result.has_value()) {
return std::move(cache_result).value();
}
}
return nullptr;
});
};
// Closes all iteration_count caches.
auto close_caches = [&caches] { std::ranges::fill(caches, nullptr); };
RunAndTimeTest("Create", iteration_count, [&] { make_and_bind_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), 0);
RunAndTimeTest("FirstClose", iteration_count, [&] { close_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), iteration_count);
RunAndTimeTest("FirstOpen", iteration_count, [&] { make_and_bind_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), 0);
RunAndTimeTest("SecondClose", iteration_count, [&] { close_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), iteration_count);
RunAndTimeTest("SecondOpen", iteration_count,
[&] { make_and_bind_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), 0);
RunAndTimeTest("ThirdClose", iteration_count, [&] { close_caches(); });
ASSERT_EQ(std::ranges::count(caches, nullptr), iteration_count);
}
TEST_P(PersistentCachePerftest, OpenClose) {
if (!CanShareConnections()) {
// TODO(crbug.com/377475540): Switch from sharing a connection below to
// Bind/Unbind so that the same file handles are used repeatedly to
// open/close the database.
GTEST_SKIP();
}
static constexpr int kIterationCount = 1024;
std::unique_ptr<PersistentCache> cache = MakeCache();
int success_count = 0;
RunAndTimeTest(
"OpenClose", kIterationCount, [this, &cache = *cache, &success_count] {
for (size_t i = 0; i < kIterationCount; ++i) {
if (PersistentCache::Bind(Client::kTest,
*ShareReadWriteConnection(cache))
.has_value()) {
++success_count;
}
}
});
ASSERT_EQ(success_count, kIterationCount);
}
TEST_P(PersistentCachePerftest, Insert) {
int kIterationCount = 1024;
if (!IsWalMode() && HasExpensiveCommits()) {
// Insertions take an egregiously long time when commits are expensive.
// Scale back the number of iterations in that case.
kIterationCount /= 4;
}
std::unique_ptr<PersistentCache> cache = MakeCache();
std::vector<std::string> keys = GenerateKeys(kIterationCount);
base::HeapArray<uint8_t> value = MakeValue();
int success_count = 0;
RunAndTimeTest("Insert", kIterationCount, [&] {
success_count = std::ranges::count_if(keys, [&cache = *cache,
&value](const auto& key) {
return cache.Insert(base::as_byte_span(key), value.as_span()).has_value();
});
});
ASSERT_EQ(success_count, kIterationCount);
}
TEST_P(PersistentCachePerftest, Find) {
static constexpr int kIterationCount = 1024;
// Open the cache in WAL mode and fill it.
std::unique_ptr<PersistentCache> cache =
CreateCache(/*single_connection=*/true, /*journal_mode_wal=*/true);
std::vector<std::string> keys = GenerateKeys(kIterationCount);
base::HeapArray<uint8_t> value = MakeValue();
// Fill the cache.
for (const auto& key : keys) {
ASSERT_OK(cache->Insert(base::as_byte_span(key), value, {}));
}
// Switch the cache back to using a rollback journal and close it. This will
// perform a checkpoint and allow the database to be opened without the
// write-ahead log file.
ASSERT_OK(static_cast<SqliteBackendImpl*>(cache->GetBackendForTesting())
->ExecuteStatementForTesting("PRAGMA journal_mode=TRUNCATE"));
cache.reset();
cache = MakeCache();
// Shuffle the keys around to avoid taking advantage of file-system caching
// behavior.
base::RandomShuffle(keys.begin(), keys.end());
int success_count = 0;
RunAndTimeTest("Find", kIterationCount, [&] {
success_count =
std::ranges::count_if(keys, [&cache = *cache](const auto& key) {
return cache
.Find(base::as_byte_span(key),
[](size_t content_size) { return base::span<uint8_t>(); })
.has_value();
});
});
ASSERT_EQ(success_count, kIterationCount);
}
TEST_P(PersistentCachePerftest, WALPerformance) {
if (!IsWalMode()) {
GTEST_SKIP();
}
static constexpr int kTotalCount = 2048;
static constexpr int kHalfCount = kTotalCount / 2;
std::unique_ptr<PersistentCache> cache = MakeCache();
auto* backend =
static_cast<SqliteBackendImpl*>(cache->GetBackendForTesting());
// Disable automatic checkpointing.
ASSERT_OK(backend->ExecuteStatementForTesting("PRAGMA wal_autocheckpoint=0"));
std::vector<std::string> keys = GenerateKeys(kTotalCount);
base::HeapArray<uint8_t> value = MakeValue();
// 1. Insert first half of the data (goes to WAL).
for (int i = 0; i < kHalfCount; ++i) {
ASSERT_OK(cache->Insert(base::as_byte_span(keys[i]), value));
}
// 2. Perform a truncating checkpoint to move data from WAL to database.
ASSERT_OK(
backend->ExecuteStatementForTesting("PRAGMA wal_checkpoint(TRUNCATE)"));
// 3. Insert second half of the data (goes to WAL).
for (int i = kHalfCount; i < kTotalCount; ++i) {
ASSERT_OK(cache->Insert(base::as_byte_span(keys[i]), value));
}
// Shuffle keys for random access.
base::RandomShuffle(keys.begin(), keys.end());
// 4. Measure performance with mixed WAL and DB data.
base::ElapsedTimer mixed_timer;
base::ElapsedThreadTimer mixed_thread_timer;
for (const auto& key : keys) {
auto result = cache->Find(base::as_byte_span(key), [&value](size_t size) {
return value.as_span();
});
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value().has_value());
}
base::TimeDelta mixed_elapsed = mixed_timer.Elapsed();
base::TimeDelta mixed_thread_elapsed = mixed_thread_timer.Elapsed();
// 5. Perform final truncating checkpoint.
ASSERT_OK(
backend->ExecuteStatementForTesting("PRAGMA wal_checkpoint(TRUNCATE)"));
// 6. Measure performance with all data in DB.
base::ElapsedTimer db_timer;
base::ElapsedThreadTimer db_thread_timer;
for (const auto& key : keys) {
auto result = cache->Find(base::as_byte_span(key), [&value](size_t size) {
return value.as_span();
});
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value().has_value());
}
base::TimeDelta db_elapsed = db_timer.Elapsed();
base::TimeDelta db_thread_elapsed = db_thread_timer.Elapsed();
// 7. Report the difference (Mixed - DB = Overhead).
ReportMeasurement(
"WALOverhead", kTotalCount,
std::max(base::TimeDelta(), mixed_elapsed - db_elapsed),
std::max(base::TimeDelta(), mixed_thread_elapsed - db_thread_elapsed));
}
INSTANTIATE_TEST_SUITE_P(
,
PersistentCachePerftest,
testing::Combine(testing::Bool(), testing::Bool()),
[](const testing::TestParamInfo<PersistentCachePerftest::ParamType>& info) {
return PersistentCachePerftest::GetParamName(info.param);
});
} // namespace persistent_cache