blob: 28b22ce80693f5eedd5f185c7fa8b4b6dca7cbff [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/390223051): Remove C-library calls to fix the errors.
#pragma allow_unsafe_libc_calls
#endif
#include "gpu/command_buffer/service/dawn_caching_interface.h"
#include <string>
#include <string_view>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/scoped_feature_list.h"
#include "components/persistent_cache/backend_params.h"
#include "components/persistent_cache/sqlite/vfs/sandboxed_file.h"
#include "gpu/command_buffer/service/gpu_persistent_cache.h"
#include "gpu/command_buffer/service/mocks.h"
#include "gpu/config/gpu_finch_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace gpu::webgpu {
namespace {
using ::testing::StrictMock;
class DawnCachingInterfaceTest : public testing::Test {
protected:
static constexpr std::string_view kKey = "cache key";
static constexpr std::string_view kData = "some data";
static constexpr size_t kKeySize = kKey.size();
static constexpr size_t kDataSize = kData.size();
static constexpr gpu::GpuDiskCacheDawnWebGPUHandle kDawnWebGPUHandle =
gpu::GpuDiskCacheDawnWebGPUHandle(1);
static constexpr gpu::GpuDiskCacheDawnGraphiteHandle kDawnGraphiteHandle =
gpu::GpuDiskCacheDawnGraphiteHandle(2);
DawnCachingInterfaceFactory factory_;
gpu::GpuDiskCacheHandle handle_ = kDawnWebGPUHandle;
StrictMock<MockDecoderClient> decoder_client_mock_;
};
TEST_F(DawnCachingInterfaceTest, LoadNonexistentSize) {
auto dawn_caching_interface = factory_.CreateInstance(handle_);
EXPECT_EQ(
0u, dawn_caching_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
}
TEST_F(DawnCachingInterfaceTest, StoreThenLoadSameInterface) {
auto dawn_caching_interface = factory_.CreateInstance(handle_);
dawn_caching_interface->StoreData(kKey.data(), kKeySize, kData.data(),
kDataSize);
char buffer[kDataSize];
EXPECT_EQ(kDataSize, dawn_caching_interface->LoadData(kKey.data(), kKeySize,
nullptr, 0));
EXPECT_EQ(kDataSize, dawn_caching_interface->LoadData(kKey.data(), kKeySize,
buffer, kDataSize));
EXPECT_EQ(0, memcmp(buffer, kData.data(), kDataSize));
}
TEST_F(DawnCachingInterfaceTest, StoreThenLoadSameHandle) {
auto store_interface = factory_.CreateInstance(handle_);
store_interface->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
auto load_interface = factory_.CreateInstance(handle_);
char buffer[kDataSize];
EXPECT_EQ(kDataSize,
load_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
EXPECT_EQ(kDataSize,
load_interface->LoadData(kKey.data(), kKeySize, buffer, kDataSize));
EXPECT_EQ(0, memcmp(buffer, kData.data(), kDataSize));
}
TEST_F(DawnCachingInterfaceTest, StoreDestroyThenLoadSameHandle) {
auto store_interface = factory_.CreateInstance(handle_);
store_interface->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
store_interface.reset();
auto load_interface = factory_.CreateInstance(handle_);
char buffer[kDataSize];
EXPECT_EQ(kDataSize,
load_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
EXPECT_EQ(kDataSize,
load_interface->LoadData(kKey.data(), kKeySize, buffer, kDataSize));
EXPECT_EQ(0, memcmp(buffer, kData.data(), kDataSize));
}
// If the handle is released before a new cache is created, the new cache should
// use a new in-memory cache.
TEST_F(DawnCachingInterfaceTest, StoreReleaseThenLoad) {
auto store_interface = factory_.CreateInstance(handle_);
store_interface->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
store_interface.reset();
factory_.ReleaseHandle(handle_);
auto load_interface = factory_.CreateInstance(handle_);
EXPECT_EQ(0u, load_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
}
TEST_F(DawnCachingInterfaceTest, IncognitoCachesDoNotShare) {
auto interface_1 = factory_.CreateInstance();
interface_1->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
auto interface_2 = factory_.CreateInstance();
EXPECT_EQ(0u, interface_2->LoadData(kKey.data(), kKeySize, nullptr, 0));
}
TEST_F(DawnCachingInterfaceTest, UnableToCreateBackend) {
// This factory mimics what happens when we are unable to create a backend.
DawnCachingInterfaceFactory factory(base::BindRepeating(
[]() -> scoped_refptr<detail::DawnMemoryCache> { return nullptr; }));
// Without an actual backend, all loads and stores should do nothing.
{
auto incongnito_interface = factory.CreateInstance();
incongnito_interface->StoreData(kKey.data(), kKeySize, kData.data(),
kDataSize);
EXPECT_EQ(
0u, incongnito_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
}
{
auto handle_interface = factory.CreateInstance(handle_);
handle_interface->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
EXPECT_EQ(0u,
handle_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
}
}
TEST_F(DawnCachingInterfaceTest, StoreTriggersHostSide) {
auto dawn_caching_interface = factory_.CreateInstance(
handle_, base::BindRepeating(&MockDecoderClient::CacheBlob,
base::Unretained(&decoder_client_mock_),
gpu::GpuDiskCacheType::kDawnWebGPU));
EXPECT_CALL(decoder_client_mock_,
CacheBlob(gpu::GpuDiskCacheType::kDawnWebGPU, std::string(kKey),
std::string(kData)));
dawn_caching_interface->StoreData(kKey.data(), kKeySize, kData.data(),
kDataSize);
}
TEST_F(DawnCachingInterfaceTest, TestMaxSizeEviction) {
// Verifies that a cache size that should only fit one entry will only keep
// one entry in memory.
static constexpr std::string_view kKey1 = "1";
static constexpr std::string_view kData1 = "1";
static constexpr std::string_view kKey2 = "2";
static constexpr std::string_view kData2 = "2";
static_assert(kKey1.size() == kKey2.size());
static_assert(kData1.size() == kData2.size());
static constexpr size_t kKeySize = kKey1.size();
static constexpr size_t kDataSize = kData1.size();
static constexpr size_t kCacheSize = 2u * kKeySize + 2u * kDataSize - 1u;
DawnCachingInterfaceFactory factory(base::BindRepeating([]() {
return base::MakeRefCounted<detail::DawnMemoryCache>(kCacheSize);
}));
auto interface = factory.CreateInstance();
interface->StoreData(kKey1.data(), kKeySize, kData1.data(), kDataSize);
interface->StoreData(kKey2.data(), kKeySize, kData2.data(), kDataSize);
EXPECT_EQ(0u, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
EXPECT_EQ(kDataSize, interface->LoadData(kKey2.data(), 1u, nullptr, 0));
}
TEST_F(DawnCachingInterfaceTest, TestLruEviction) {
// Verifies that a cache size that should only fit two entries evicts the
// proper entry when a third one is stored.
static constexpr std::string_view kKey1 = "1";
static constexpr std::string_view kData1 = "1";
static constexpr std::string_view kKey2 = "2";
static constexpr std::string_view kData2 = "2";
static constexpr std::string_view kKey3 = "3";
static constexpr std::string_view kData3 = "3";
static_assert(kKey1.size() == kKey2.size());
static_assert(kKey2.size() == kKey3.size());
static_assert(kData1.size() == kData2.size());
static_assert(kData2.size() == kData3.size());
static constexpr size_t kKeySize = kKey1.size();
static constexpr size_t kDataSize = kData1.size();
static constexpr size_t kCacheSize = 3u * kKeySize + 3u * kDataSize - 1u;
DawnCachingInterfaceFactory factory(base::BindRepeating([]() {
return base::MakeRefCounted<detail::DawnMemoryCache>(kCacheSize);
}));
// Even though Key1 was stored first, because we loaded it once, Key2 should
// be the one to be evicted when Key3 is added.
auto interface = factory.CreateInstance();
interface->StoreData(kKey1.data(), kKeySize, kData1.data(), kDataSize);
interface->StoreData(kKey2.data(), kKeySize, kData2.data(), kDataSize);
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
interface->StoreData(kKey3.data(), kKeySize, kData3.data(), kDataSize);
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
EXPECT_EQ(0u, interface->LoadData(kKey2.data(), 1u, nullptr, 0));
EXPECT_EQ(kDataSize, interface->LoadData(kKey3.data(), 1u, nullptr, 0));
}
// Entries that are too large for the size of the cache are not cached and do
// not cause any crashes. This is a regression test for dawn:2034.
TEST_F(DawnCachingInterfaceTest, TestVeryLargeEntrySize) {
static constexpr std::string_view kSmall = "1";
static constexpr std::string_view kLarge = "11111";
static constexpr size_t kSmallSize = kSmall.size();
static constexpr size_t kLargeSize = kLarge.size();
static constexpr size_t kCacheSize = kLargeSize - 1u;
DawnCachingInterfaceFactory factory(base::BindRepeating([]() {
return base::MakeRefCounted<detail::DawnMemoryCache>(kCacheSize);
}));
auto interface = factory.CreateInstance();
{
// When the key is larger than the cache size but the value is not, caching
// fails.
interface->StoreData(kLarge.data(), kLargeSize, kSmall.data(), kSmallSize);
EXPECT_EQ(0u, interface->LoadData(kLarge.data(), kLargeSize, nullptr, 0));
}
{
// When the key is smaller than the cache size, but the value is not,
// caching fails.
interface->StoreData(kSmall.data(), kSmallSize, kLarge.data(), kLargeSize);
EXPECT_EQ(0u, interface->LoadData(kSmall.data(), kSmallSize, nullptr, 0));
}
{
// When the both the key and the value is larger than the cache size,
// caching fails.
interface->StoreData(kLarge.data(), kLargeSize, kLarge.data(), kLargeSize);
EXPECT_EQ(0u, interface->LoadData(kLarge.data(), kLargeSize, nullptr, 0));
}
}
TEST_F(DawnCachingInterfaceTest, TestMemoryPressureCritical) {
// Verifies that on PurgeMemory the cache becomes empty for critical pressure
// levels without `kAggressiveShaderCacheLimits` feature flag.
static constexpr std::string_view kKey1 = "1";
static constexpr std::string_view kData1 = "1";
static constexpr size_t kKeySize = kKey1.size();
static constexpr size_t kDataSize = kData1.size();
static constexpr size_t kCacheSize = 2u * kKeySize + 2u * kDataSize - 1u;
DawnCachingInterfaceFactory factory(base::BindRepeating([]() {
return base::MakeRefCounted<detail::DawnMemoryCache>(kCacheSize);
}));
// Pass handles here so that the backends_ are populated.
auto interfaces = {factory.CreateInstance(kDawnGraphiteHandle),
factory.CreateInstance(kDawnWebGPUHandle)};
for (auto& interface : interfaces) {
interface->StoreData(kKey1.data(), kKeySize, kData1.data(), kDataSize);
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
factory.PurgeMemory(base::MEMORY_PRESSURE_LEVEL_CRITICAL);
EXPECT_EQ(0u, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
}
}
TEST_F(DawnCachingInterfaceTest, TestAggressiveCacheAndMemoryPressure) {
// Verifies PurgeMemory with `kAggressiveShaderCacheLimits` feature flag.
base::test::ScopedFeatureList feature_list{
::features::kAggressiveShaderCacheLimits};
static constexpr std::string_view kKey1 = "1";
static constexpr std::string_view kData1 = "1";
static constexpr size_t kKeySize = kKey1.size();
static constexpr size_t kDataSize = kData1.size();
static constexpr size_t kCacheSize = 2u * kKeySize + 2u * kDataSize - 1u;
DawnCachingInterfaceFactory factory(base::BindRepeating([]() {
return base::MakeRefCounted<detail::DawnMemoryCache>(kCacheSize);
}));
// Pass handles here so that the backends_ are populated.
auto interfaces = {factory.CreateInstance(kDawnGraphiteHandle),
factory.CreateInstance(kDawnWebGPUHandle)};
for (auto& interface : interfaces) {
interface->StoreData(kKey1.data(), kKeySize, kData1.data(), kDataSize);
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
// Moderate memory pressure is ignored
factory.PurgeMemory(base::MEMORY_PRESSURE_LEVEL_MODERATE);
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
// But not critical, except on Android
factory.PurgeMemory(base::MEMORY_PRESSURE_LEVEL_CRITICAL);
#if BUILDFLAG(IS_ANDROID)
EXPECT_EQ(kDataSize, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
#else
EXPECT_EQ(0u, interface->LoadData(kKey1.data(), 1u, nullptr, 0));
#endif
}
}
// Verifies that data stored in a persistent cache can be loaded back.
TEST_F(DawnCachingInterfaceTest, StoreAndLoadWithPersistentCache) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
auto shared_lock = base::UnsafeSharedMemoryRegion::Create(
sizeof(persistent_cache::LockState));
ASSERT_TRUE(shared_lock.IsValid());
auto OpenPersistentCache =
[&temp_dir, &shared_lock]() -> std::unique_ptr<GpuPersistentCache> {
auto db_path = temp_dir.GetPath().AppendASCII("test.db");
auto journal_path = temp_dir.GetPath().AppendASCII("test.journal");
persistent_cache::BackendParams params;
params.type = persistent_cache::BackendType::kSqlite;
params.db_file =
base::File(db_path, base::File::FLAG_OPEN_ALWAYS |
base::File::FLAG_READ | base::File::FLAG_WRITE);
params.journal_file = base::File(
journal_path, base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ |
base::File::FLAG_WRITE);
params.shared_lock = shared_lock.Duplicate();
CHECK(params.db_file.IsValid());
CHECK(params.journal_file.IsValid());
auto persistent_cache = std::make_unique<GpuPersistentCache>("Test");
persistent_cache->InitializeCache(std::move(params));
return persistent_cache;
};
// Store data to the persistent cache via store interface.
{
scoped_refptr<detail::DawnMemoryCache> memory_cache =
base::MakeRefCounted<detail::DawnMemoryCache>(1024);
DawnCachingInterfaceFactory store_factory(base::BindRepeating(
[](scoped_refptr<detail::DawnMemoryCache> cache) { return cache; },
memory_cache));
auto store_interface =
store_factory.CreateInstance(handle_, OpenPersistentCache());
store_interface->StoreData(kKey.data(), kKeySize, kData.data(), kDataSize);
// Check that the entry exists in the memory cache.
char buffer[kDataSize];
EXPECT_EQ(kDataSize, memory_cache->LoadData(std::string(kKey), nullptr, 0));
EXPECT_EQ(kDataSize,
memory_cache->LoadData(std::string(kKey), buffer, kDataSize));
EXPECT_EQ(0, memcmp(buffer, kData.data(), kDataSize));
}
// Use the same persistent cache but with different memory cache.
{
scoped_refptr<detail::DawnMemoryCache> memory_cache2 =
base::MakeRefCounted<detail::DawnMemoryCache>(1024);
DawnCachingInterfaceFactory load_factory(base::BindRepeating(
[](scoped_refptr<detail::DawnMemoryCache> cache) { return cache; },
memory_cache2));
auto load_interface =
load_factory.CreateInstance(handle_, OpenPersistentCache());
EXPECT_EQ(0u, memory_cache2->LoadData(std::string(kKey), nullptr, 0));
// Verify that we can query the existing entry.
char buffer[kDataSize];
EXPECT_EQ(kDataSize,
load_interface->LoadData(kKey.data(), kKeySize, nullptr, 0));
EXPECT_EQ(kDataSize, load_interface->LoadData(kKey.data(), kKeySize, buffer,
kDataSize));
EXPECT_EQ(0, memcmp(buffer, kData.data(), kDataSize));
// Check that the memory cache now contains the same entry after the
// LoadData() call above
EXPECT_EQ(kDataSize,
memory_cache2->LoadData(std::string(kKey), nullptr, 0));
}
}
} // namespace
} // namespace gpu::webgpu