blob: d02146e4ec089f0821e7fc6d7dcac6196da35686 [file] [log] [blame]
// 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 "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/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/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 {
// The variations of cache options available for creating/testing
// PersistentCache.
enum class CacheOption {
kMultipleConnections,
kSingleConnection,
kJournalModeWal,
};
// A printer for `CacheOption`; used by GoogleTest for more friendly output and
// to suffix the story name for performance measurements.
void PrintTo(CacheOption cache_option, std::ostream* os) {
switch (cache_option) {
case CacheOption::kMultipleConnections:
*os << "MultipleConnections";
break;
case CacheOption::kSingleConnection:
*os << "SingleConnection";
break;
case CacheOption::kJournalModeWal:
*os << "JournalModeWal";
break;
}
}
// A test harness parameterized on the options for creating a PersistentCache.
class PersistentCachePerftest : public testing::TestWithParam<CacheOption> {
protected:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
backend_storage_.emplace(BackendType::kSqlite, temp_dir_.GetPath());
}
// Returns a new cache configured according to the test's parameter.
std::unique_ptr<PersistentCache> MakeCache() {
switch (GetParam()) {
case CacheOption::kMultipleConnections:
return CreateCache(/*single_connection=*/false,
/*journal_mode_wal=*/false);
case CacheOption::kSingleConnection:
return CreateCache(/*single_connection=*/true,
/*journal_mode_wal=*/false);
case CacheOption::kJournalModeWal:
return CreateCache(/*single_connection=*/true,
/*journal_mode_wal=*/true);
}
}
// 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()) {
return PersistentCache::Bind(*std::move(pending_backend));
}
ADD_FAILURE() << "Failed to make PendingBackend";
return nullptr;
}
// Returns true if caches created in this configuration can be shared across
// multiple connections.
static bool CanShareConnections() {
return GetParam() == CacheOption::kMultipleConnections;
}
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();
ReportMeasurment(
base::StrCat({operation_name, testing::PrintToString(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
}
private:
static constexpr base::FilePath::StringViewType kBaseName =
FILE_PATH_LITERAL("perftest");
void ReportMeasurment(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));
}
base::ScopedTempDir temp_dir_;
std::optional<BackendStorage> backend_storage_;
bool under_measurment_ = false;
};
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) {
auto persistent_cache_under_test =
PersistentCache::Bind(*ShareReadWriteConnection(cache));
if (persistent_cache_under_test) {
++success_count;
}
}
});
ASSERT_EQ(success_count, kIterationCount);
}
TEST_P(PersistentCachePerftest, Insert) {
int kIterationCount = 1024;
if (GetParam() != CacheOption::kJournalModeWal && 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(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(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(key, [](size_t content_size) { return base::span<uint8_t>(); })
.has_value();
});
});
ASSERT_EQ(success_count, kIterationCount);
}
INSTANTIATE_TEST_SUITE_P(,
PersistentCachePerftest,
testing::Values(CacheOption::kMultipleConnections,
CacheOption::kSingleConnection,
CacheOption::kJournalModeWal),
testing::PrintToStringParamName());
} // namespace persistent_cache