| // 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. | |
| #include "gpu/command_buffer/service/dawn_caching_interface.h" | |
| #include <array> | |
| #include <cstdint> | |
| #include <string> | |
| #include <string_view> | |
| #include "base/containers/span.h" | |
| #include "base/files/scoped_temp_dir.h" | |
| #include "base/memory_coordinator/memory_coordinator_features.h" | |
| #include "base/memory_coordinator/utils.h" | |
| #include "base/strings/string_view_util.h" | |
| #include "base/test/scoped_feature_list.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 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->FindKey(kKey)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, StoreThenLoadSameInterface) { | |
| auto dawn_caching_interface = factory_.CreateInstance(handle_); | |
| dawn_caching_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| std::array<uint8_t, kDataSize> buffer = {}; | |
| EXPECT_EQ(kDataSize, dawn_caching_interface->FindKey(kKey)); | |
| EXPECT_EQ(kDataSize, dawn_caching_interface->LoadData(kKey, buffer)); | |
| EXPECT_EQ(kData, base::as_string_view(buffer)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, LoadPartialData) { | |
| auto dawn_caching_interface = factory_.CreateInstance(handle_); | |
| dawn_caching_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| static constexpr size_t kPartialSize = kDataSize / 2; | |
| std::array<uint8_t, kPartialSize> buffer = {}; | |
| // Should return 0 because of size mismatch. | |
| EXPECT_EQ(0u, dawn_caching_interface->LoadData(kKey, buffer)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, LoadLargerBuffer) { | |
| auto dawn_caching_interface = factory_.CreateInstance(handle_); | |
| dawn_caching_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| static constexpr size_t kLargerSize = kDataSize * 2; | |
| std::array<uint8_t, kLargerSize> buffer = {}; | |
| // Should return kDataSize and only copy kDataSize bytes. | |
| EXPECT_EQ(kDataSize, dawn_caching_interface->LoadData(kKey, buffer)); | |
| EXPECT_EQ(kData, base::as_string_view(base::span(buffer).first(kDataSize))); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, StoreThenLoadSameHandle) { | |
| auto store_interface = factory_.CreateInstance(handle_); | |
| store_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| auto load_interface = factory_.CreateInstance(handle_); | |
| std::array<uint8_t, kDataSize> buffer = {}; | |
| EXPECT_EQ(kDataSize, load_interface->FindKey(kKey)); | |
| EXPECT_EQ(kDataSize, load_interface->LoadData(kKey, buffer)); | |
| EXPECT_EQ(kData, base::as_string_view(buffer)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, StoreDestroyThenLoadSameHandle) { | |
| auto store_interface = factory_.CreateInstance(handle_); | |
| store_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| store_interface.reset(); | |
| auto load_interface = factory_.CreateInstance(handle_); | |
| std::array<uint8_t, kDataSize> buffer = {}; | |
| EXPECT_EQ(kDataSize, load_interface->FindKey(kKey)); | |
| EXPECT_EQ(kDataSize, load_interface->LoadData(kKey, buffer)); | |
| EXPECT_EQ(kData, base::as_string_view(buffer)); | |
| } | |
| // 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, base::as_byte_span(kData)); | |
| store_interface.reset(); | |
| factory_.ReleaseHandle(handle_); | |
| auto load_interface = factory_.CreateInstance(handle_); | |
| EXPECT_EQ(0u, load_interface->FindKey(kKey)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, IncognitoCachesDoNotShare) { | |
| auto interface_1 = factory_.CreateInstance(); | |
| interface_1->StoreData(kKey, base::as_byte_span(kData)); | |
| auto interface_2 = factory_.CreateInstance(); | |
| EXPECT_EQ(0u, interface_2->FindKey(kKey)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, UnableToCreateBackend) { | |
| // This factory mimics what happens when we are unable to create a backend. | |
| DawnCachingInterfaceFactory factory(base::BindRepeating( | |
| []() -> scoped_refptr<MemoryCache> { return nullptr; })); | |
| // Without an actual backend, all loads and stores should do nothing. | |
| { | |
| auto incongnito_interface = factory.CreateInstance(); | |
| incongnito_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| EXPECT_EQ(0u, incongnito_interface->FindKey(kKey)); | |
| } | |
| { | |
| auto handle_interface = factory.CreateInstance(handle_); | |
| handle_interface->StoreData(kKey, base::as_byte_span(kData)); | |
| EXPECT_EQ(0u, handle_interface->FindKey(kKey)); | |
| } | |
| } | |
| 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, base::as_byte_span(kData)); | |
| } | |
| 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<MemoryCache>(kCacheSize); })); | |
| auto interface = factory.CreateInstance(); | |
| interface->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface->StoreData(kKey2, base::as_byte_span(kData2)); | |
| EXPECT_EQ(0u, interface->FindKey(kKey1)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey2)); | |
| } | |
| 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<MemoryCache>(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, base::as_byte_span(kData1)); | |
| interface->StoreData(kKey2, base::as_byte_span(kData2)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| interface->StoreData(kKey3, base::as_byte_span(kData3)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| EXPECT_EQ(0u, interface->FindKey(kKey2)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey3)); | |
| } | |
| // 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 kLargeSize = kLarge.size(); | |
| static constexpr size_t kCacheSize = kLargeSize - 1u; | |
| DawnCachingInterfaceFactory factory(base::BindRepeating( | |
| []() { return base::MakeRefCounted<MemoryCache>(kCacheSize); })); | |
| auto interface = factory.CreateInstance(); | |
| { | |
| // When the key is larger than the cache size but the value is not, caching | |
| // fails. | |
| interface->StoreData(kLarge, base::as_byte_span(kSmall)); | |
| EXPECT_EQ(0u, interface->FindKey(kLarge)); | |
| } | |
| { | |
| // When the key is smaller than the cache size, but the value is not, | |
| // caching fails. | |
| interface->StoreData(kSmall, base::as_byte_span(kLarge)); | |
| EXPECT_EQ(0u, interface->FindKey(kSmall)); | |
| } | |
| { | |
| // When the both the key and the value is larger than the cache size, | |
| // caching fails. | |
| interface->StoreData(kLarge, base::as_byte_span(kLarge)); | |
| EXPECT_EQ(0u, interface->FindKey(kLarge)); | |
| } | |
| } | |
| TEST_F(DawnCachingInterfaceTest, TestMemoryPressureCritical) { | |
| // Verifies that on PurgeMemory the cache becomes empty for critical pressure | |
| // levels without `kAggressiveShaderCacheLimits` feature flag. | |
| // This test relies on the memory limit not persisting after | |
| // OnReleaseMemory(). See OnReleaseMemoryLimitPersistsStateful for the | |
| // stateful behavior. | |
| base::test::ScopedFeatureList feature_list; | |
| feature_list.InitAndDisableFeature(base::kStatefulMemoryPressure); | |
| 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<MemoryCache>(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, base::as_byte_span(kData1)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| factory.OnReleaseMemory(base::kCriticalMemoryPressureThreshold); | |
| EXPECT_EQ(0u, interface->FindKey(kKey1)); | |
| } | |
| } | |
| TEST_F(DawnCachingInterfaceTest, TestAggressiveCacheAndMemoryPressure) { | |
| // Verifies PurgeMemory with `kAggressiveShaderCacheLimits` feature flag. | |
| // This test relies on the memory limit not persisting after | |
| // OnReleaseMemory(). See OnReleaseMemoryLimitPersistsStateful for the | |
| // stateful behavior. | |
| base::test::ScopedFeatureList feature_list; | |
| feature_list.InitWithFeatures( | |
| /*enabled_features=*/{::features::kAggressiveShaderCacheLimits}, | |
| /*disabled_features=*/{base::kStatefulMemoryPressure}); | |
| 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<MemoryCache>(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, base::as_byte_span(kData1)); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| // Moderate memory pressure is ignored | |
| factory.OnReleaseMemory(base::kModerateMemoryPressureThreshold); | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| // But not critical, except on Android | |
| factory.OnReleaseMemory(base::kCriticalMemoryPressureThreshold); | |
| #if BUILDFLAG(IS_ANDROID) | |
| EXPECT_EQ(kDataSize, interface->FindKey(kKey1)); | |
| #else | |
| EXPECT_EQ(0u, interface->FindKey(kKey1)); | |
| #endif | |
| } | |
| } | |
| TEST_F(DawnCachingInterfaceTest, OnUpdateMemoryLimitStateful) { | |
| base::test::ScopedFeatureList feature_list{base::kStatefulMemoryPressure}; | |
| static constexpr std::string_view kKey1 = "key1"; | |
| static constexpr std::string_view kData1 = "data1"; | |
| static constexpr std::string_view kKey2 = "key2"; | |
| static constexpr std::string_view kData2 = "data2"; | |
| static constexpr size_t kSingleEntrySize = kKey1.size() + kData1.size(); | |
| // Under moderate pressure (50%), capacity scales to 25% of kCacheSize. | |
| // Set kCacheSize = 4 * kSingleEntrySize so that exactly 1 entry fits under | |
| // moderate pressure. | |
| static constexpr size_t kCacheSize = 4u * kSingleEntrySize; | |
| DawnCachingInterfaceFactory factory(base::BindRepeating( | |
| []() { return base::MakeRefCounted<MemoryCache>(kCacheSize); })); | |
| std::unique_ptr<DawnInMemoryCache> interface_graphite = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kDawnGraphiteHandle)); | |
| std::unique_ptr<DawnInMemoryCache> interface_webgpu = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kDawnWebGPUHandle)); | |
| // 1. Setup data for both caches. | |
| interface_graphite->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_graphite->StoreData(kKey2, base::as_byte_span(kData2)); | |
| interface_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_webgpu->StoreData(kKey2, base::as_byte_span(kData2)); | |
| EXPECT_EQ(kData1.size(), interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_graphite->FindKey(kKey2)); | |
| EXPECT_EQ(kData1.size(), interface_webgpu->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_webgpu->FindKey(kKey2)); | |
| // 2. Call OnUpdateMemoryLimit() on factory (non-destructive update). | |
| factory.OnUpdateMemoryLimit(base::kModerateMemoryPressureThreshold); | |
| // 3. Verify keys for both caches remain intact. | |
| EXPECT_EQ(kData1.size(), interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_graphite->FindKey(kKey2)); | |
| EXPECT_EQ(kData1.size(), interface_webgpu->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_webgpu->FindKey(kKey2)); | |
| // 4. Call OnReleaseMemory() on factory (evicts down to moderate threshold). | |
| factory.OnReleaseMemory(base::kModerateMemoryPressureThreshold); | |
| // 5. Verify LRU entries (kKey1) are evicted across both caches. | |
| EXPECT_EQ(0u, interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_graphite->FindKey(kKey2)); | |
| EXPECT_EQ(0u, interface_webgpu->FindKey(kKey1)); | |
| EXPECT_EQ(kData2.size(), interface_webgpu->FindKey(kKey2)); | |
| // Under critical pressure, all entries are evicted across both caches. | |
| factory.OnReleaseMemory(base::kCriticalMemoryPressureThreshold); | |
| EXPECT_EQ(0u, interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(0u, interface_graphite->FindKey(kKey2)); | |
| EXPECT_EQ(0u, interface_webgpu->FindKey(kKey1)); | |
| EXPECT_EQ(0u, interface_webgpu->FindKey(kKey2)); | |
| // Restoring memory limit allows storing new entries again across both. | |
| factory.OnUpdateMemoryLimit(base::kNoMemoryPressureThreshold); | |
| interface_graphite->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(kData1.size(), interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(kData1.size(), interface_webgpu->FindKey(kKey1)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, NewlyCreatedBackendRespectsMemoryLimit) { | |
| base::test::ScopedFeatureList feature_list{base::kStatefulMemoryPressure}; | |
| static constexpr std::string_view kKey1 = "key1"; | |
| static constexpr std::string_view kData1 = "data1"; | |
| static constexpr size_t kSingleEntrySize = kKey1.size() + kData1.size(); | |
| static constexpr size_t kCacheSize = 4u * kSingleEntrySize; | |
| DawnCachingInterfaceFactory factory(base::BindRepeating( | |
| []() { return base::MakeRefCounted<MemoryCache>(kCacheSize); })); | |
| // Set memory limit to critical before creating the backend instance. | |
| factory.OnUpdateMemoryLimit(base::kCriticalMemoryPressureThreshold); | |
| std::unique_ptr<DawnInMemoryCache> interface = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kDawnGraphiteHandle)); | |
| // Newly created backend should have size limit of 0 under critical pressure. | |
| interface->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(0u, interface->FindKey(kKey1)); | |
| // Restoring memory limit allows storing entries. | |
| factory.OnUpdateMemoryLimit(base::kNoMemoryPressureThreshold); | |
| interface->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(kData1.size(), interface->FindKey(kKey1)); | |
| } | |
| TEST_F(DawnCachingInterfaceTest, OnReleaseMemoryLimitPersistsStateful) { | |
| // Use the default policy, where critical pressure scales the cache to 0. | |
| base::test::ScopedFeatureList feature_list; | |
| feature_list.InitWithFeatures( | |
| /*enabled_features=*/{base::kStatefulMemoryPressure}, | |
| /*disabled_features=*/{::features::kAggressiveShaderCacheLimits}); | |
| static constexpr std::string_view kKey1 = "key1"; | |
| static constexpr std::string_view kData1 = "data1"; | |
| static constexpr size_t kSingleEntrySize = kKey1.size() + kData1.size(); | |
| static constexpr size_t kCacheSize = 4u * kSingleEntrySize; | |
| static constexpr gpu::GpuDiskCacheDawnWebGPUHandle kOtherDawnWebGPUHandle = | |
| gpu::GpuDiskCacheDawnWebGPUHandle(3); | |
| DawnCachingInterfaceFactory factory(base::BindRepeating( | |
| []() { return base::MakeRefCounted<MemoryCache>(kCacheSize); })); | |
| std::unique_ptr<DawnInMemoryCache> interface_graphite = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kDawnGraphiteHandle)); | |
| std::unique_ptr<DawnInMemoryCache> interface_webgpu = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kDawnWebGPUHandle)); | |
| factory.OnReleaseMemory(base::kCriticalMemoryPressureThreshold); | |
| // The limit persists after OnReleaseMemory(), so stores are rejected. | |
| interface_graphite->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(0u, interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(0u, interface_webgpu->FindKey(kKey1)); | |
| // Backends created after OnReleaseMemory() also respect the limit. | |
| std::unique_ptr<DawnInMemoryCache> interface_other_webgpu = | |
| factory.CreateInstance(gpu::GpuDiskCacheHandle(kOtherDawnWebGPUHandle)); | |
| interface_other_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(0u, interface_other_webgpu->FindKey(kKey1)); | |
| // Restoring the memory limit allows storing entries again. | |
| factory.OnUpdateMemoryLimit(base::kNoMemoryPressureThreshold); | |
| interface_graphite->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| interface_other_webgpu->StoreData(kKey1, base::as_byte_span(kData1)); | |
| EXPECT_EQ(kData1.size(), interface_graphite->FindKey(kKey1)); | |
| EXPECT_EQ(kData1.size(), interface_webgpu->FindKey(kKey1)); | |
| EXPECT_EQ(kData1.size(), interface_other_webgpu->FindKey(kKey1)); | |
| } | |
| } // namespace | |
| } // namespace gpu::webgpu |