blob: f8c54be17f73b0e9d9e9a133531f914e98ea8b38 [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 "net/http/no_vary_search_cache.h"
#include <stddef.h>
#include <stdint.h>
#include <array>
#include <optional>
#include <string>
#include "base/base64.h"
#include "base/containers/span.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "net/base/features.h"
#include "net/base/network_isolation_key.h"
#include "net/base/schemeful_site.h"
#include "net/http/http_no_vary_search_data.h"
#include "net/http/http_request_info.h"
#include "net/http/http_response_headers.h"
#include "net/http/no_vary_search_cache_test_utils.h"
#include "third_party/abseil-cpp/absl/base/call_once.h"
#include "third_party/google_benchmark/src/include/benchmark/benchmark.h"
#include "url/gurl.h"
namespace net {
namespace {
// Maximum number of entries in the NoVarySearchCache.
constexpr size_t kMaxSize = 1000u;
// When generating a request to lookup in the cache, which key it should fail to
// match on (or kIsAsMatch if the match should succeed).
enum class DiffersWhere {
kNIK,
kBaseURL,
kQuery,
kIsAMatch,
};
// Because GoogleBench only supports integer args, different cases are specified
// as indexes into an array.
struct TestCase {
// The three counts multiplied together will be the total number of entries in
// the cache, and so must be <= kMaxSize.
size_t distinct_nik_count;
size_t distinct_base_url_count;
size_t distinct_query_count;
DiffersWhere differs_where;
};
constexpr auto kTestCases = std::to_array<TestCase>({
{1000, 1, 1, DiffersWhere::kNIK},
{1000, 1, 1, DiffersWhere::kIsAMatch},
{1, 1000, 1, DiffersWhere::kBaseURL},
{1, 1000, 1, DiffersWhere::kIsAMatch},
{1, 1, 1000, DiffersWhere::kQuery},
{1, 1, 1000, DiffersWhere::kIsAMatch},
{10, 10, 10, DiffersWhere::kNIK},
{10, 10, 10, DiffersWhere::kBaseURL},
{10, 10, 10, DiffersWhere::kQuery},
{10, 10, 10, DiffersWhere::kIsAMatch},
});
// Creates a request for insertion or lookup in the cache. The NIK, base URL and
// query parts can take different values which are specified by interpolating
// the numbers specified by `nik_index`, `base_url_index`, and `query_index`.
HttpRequestInfo CreateRequest(size_t nik_index,
size_t base_url_index,
size_t query_index) {
const SchemefulSite top_frame_site(GURL("https://example.com/"));
const SchemefulSite frame_site(
GURL(base::StringPrintf("https://a%zi.example/", nik_index)));
const NetworkIsolationKey nik(top_frame_site, frame_site);
// The value of `junk_parameter` isn't important, but it is useful to have it
// vary between URLs, and since this is a benchmark it is important that it be
// deterministic.
const uint32_t junk_parameter = static_cast<uint8_t>(
nik_index * 999983 + base_url_index * 700001 + query_index * 100003);
const std::string base64_junk =
base::Base64Encode(base::byte_span_from_ref(junk_parameter));
const GURL url(
base::StringPrintf("https://www.a%zi.example/query?s=%zi&junk=%s",
base_url_index, query_index, base64_junk));
return no_vary_search_cache_test_utils::CreateTestRequest(url, nik);
}
// Creates a request for looking up in the cache. `test_case.differs_where`
// specifies which component if any should mismatch.
HttpRequestInfo CreateRequestForLookup(const TestCase& test_case) {
const auto& [distinct_nik_count, distinct_base_url_count,
distinct_query_count, differs_where] = test_case;
// These constants give the indexes of matching and non-matching values for
// each key. Index 0 is always present, and the index after the last value is
// never present.
const size_t matching_nik = 0u;
const size_t non_matching_nik = distinct_nik_count;
const size_t matching_base_url = 0u;
const size_t non_matching_base_url = distinct_base_url_count;
const size_t matching_query = 0u;
const size_t non_matching_query = distinct_query_count;
switch (differs_where) {
case DiffersWhere::kNIK:
return CreateRequest(non_matching_nik, matching_base_url, matching_query);
case DiffersWhere::kBaseURL:
return CreateRequest(matching_nik, non_matching_base_url, matching_query);
case DiffersWhere::kQuery:
return CreateRequest(matching_nik, matching_base_url, non_matching_query);
case DiffersWhere::kIsAMatch:
return CreateRequest(matching_nik, matching_base_url, matching_query);
}
}
// `cache` is global so FillCache() can fill it and BM_NoVarySearchCacheLookup()
// can performs lookups in it. It is wrapped in std::optional so that it can be
// destroyed and recreated for each case.
std::optional<NoVarySearchCache>& Cache() {
static base::NoDestructor<std::optional<NoVarySearchCache>> cache;
return *cache;
}
// To avoid state from leaking out into other performance tests in the same
// binary, this struct is constructed in SetFeatures() and destroyed again in
// ClearFeatures().
struct FeatureState {
base::test::ScopedFeatureList scoped_feature_list;
ScopedHttpNoVarySearchDataEquivalentImplementationOverrideForTesting
equivalent_override{true};
};
std::optional<FeatureState>& GetFeatureState() {
static base::NoDestructor<std::optional<FeatureState>> feature_state;
return *feature_state;
}
void SetFeatures() {
auto& feature_state = GetFeatureState();
feature_state.emplace();
feature_state->scoped_feature_list.InitWithFeatureState(
features::kSplitCacheByNetworkIsolationKey, true);
}
void ClearFeatures(const benchmark::State& state) {
GetFeatureState().reset();
}
// Sets up the contents of the cache for a single case. This can be very time
// consuming, so to avoid polluting the timing it is done in a separate setup
// step.
void FillCache(const benchmark::State& state) {
SetFeatures();
const int case_index = state.range(0);
const TestCase& test_case = kTestCases[case_index];
const auto& [distinct_nik_count, distinct_base_url_count,
distinct_query_count, differs_where] = test_case;
Cache().emplace(kMaxSize);
auto& cache = Cache().value();
const auto response_headers =
no_vary_search_cache_test_utils::CreateTestHeaders("params=(\"junk\")");
for (size_t nik_index = 0; nik_index < distinct_nik_count; ++nik_index) {
for (size_t base_url_index = 0; base_url_index < distinct_base_url_count;
++base_url_index) {
for (size_t query_index = 0; query_index < distinct_query_count;
++query_index) {
const HttpRequestInfo request =
CreateRequest(nik_index, base_url_index, query_index);
cache.MaybeInsert(request, *response_headers);
}
}
}
CHECK_EQ(cache.size(),
distinct_nik_count * distinct_base_url_count * distinct_query_count);
}
// Performs the actual benchmark, just calling the Lookup() method in a loop.
void BM_NoVarySearchCacheLookup(benchmark::State& state) {
const int case_index = state.range(0);
const TestCase& test_case = kTestCases[case_index];
const auto request = CreateRequestForLookup(test_case);
auto& cache = Cache().value();
for (auto _ : state) {
auto lookup_result = cache.Lookup(request);
benchmark::DoNotOptimize(lookup_result);
benchmark::ClobberMemory();
CHECK_EQ(lookup_result.has_value(),
test_case.differs_where == DiffersWhere::kIsAMatch);
}
}
BENCHMARK(BM_NoVarySearchCacheLookup)
->DenseRange(0, std::size(kTestCases) - 1)
->Setup(FillCache)
->Teardown(ClearFeatures);
} // namespace
} // namespace net