| // 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 "gpu/command_buffer/service/gpu_persistent_cache.h" | |
| #include <optional> | |
| #include <string_view> | |
| #include "base/compiler_specific.h" | |
| #include "base/containers/span.h" | |
| #include "base/dcheck_is_on.h" | |
| #include "base/functional/bind.h" | |
| #include "base/functional/function_ref.h" | |
| #include "base/immediate_crash.h" | |
| #include "base/logging.h" | |
| #include "base/memory/ref_counted.h" | |
| #include "base/metrics/histogram_functions.h" | |
| #include "base/synchronization/condition_variable.h" | |
| #include "base/synchronization/lock.h" | |
| #include "base/task/single_thread_task_runner.h" | |
| #include "base/task/task_runner.h" | |
| #include "base/thread_annotations.h" | |
| #include "base/threading/thread_restrictions.h" | |
| #include "base/timer/elapsed_timer.h" | |
| #include "base/trace_event/memory_dump_manager.h" | |
| #include "base/trace_event/memory_dump_request_args.h" | |
| #include "base/trace_event/trace_event.h" | |
| #include "base/types/expected_macros.h" | |
| #include "components/persistent_cache/client.h" | |
| #include "components/persistent_cache/persistent_cache.h" | |
| #include "components/persistent_cache/transaction_error.h" | |
| #include "components/version_info/version_info.h" | |
| #include "gpu/command_buffer/service/memory_cache.h" | |
| #include "ipc/common/gpu_client_ids.h" | |
| #include "ui/gl/gl_bindings.h" | |
| namespace gpu { | |
| namespace { | |
| constexpr size_t kMaxLoadStoreForTrackingCacheAvailable = 100; | |
| constexpr double kRecordCacheEntrySizeFrequency = 0.01; | |
| constexpr base::TimeDelta kDiskWriteDelaySeconds = base::Seconds(1); | |
| constexpr base::TimeDelta kDiskOpWaitTimeoutMs = base::Milliseconds(20); | |
| class ScopedHistogramTimer { | |
| public: | |
| explicit ScopedHistogramTimer(const std::string& name) : name_(name) {} | |
| ~ScopedHistogramTimer() { | |
| if (enabled_) { | |
| base::UmaHistogramCustomMicrosecondsTimes(name_, timer_.Elapsed(), | |
| base::Microseconds(1), | |
| base::Seconds(30), 100); | |
| } | |
| } | |
| void SetEnabled(bool enabled) { enabled_ = enabled; } | |
| private: | |
| const std::string name_; | |
| base::ElapsedTimer timer_; | |
| bool enabled_ = true; | |
| }; | |
| class DiskCacheTraceScope { | |
| public: | |
| explicit DiskCacheTraceScope(perfetto::StaticString name) { | |
| TRACE_EVENT_BEGIN("gpu", name); | |
| } | |
| ~DiskCacheTraceScope() { | |
| if (pending_bytes_) { | |
| TRACE_EVENT_END("gpu", "idle_id", idle_id_, "pending_bytes", | |
| pending_bytes_); | |
| } else { | |
| TRACE_EVENT_END("gpu", "idle_id", idle_id_); | |
| } | |
| } | |
| void SetIdleId(uint64_t idle_id) { idle_id_ = idle_id; } | |
| void SetPendingBytes(size_t pending_bytes) { pending_bytes_ = pending_bytes; } | |
| private: | |
| uint64_t idle_id_ = 0; | |
| std::optional<size_t> pending_bytes_; | |
| }; | |
| GLsizeiptr GL_APIENTRY GLBlobCacheGetCallback(const void* key, | |
| GLsizeiptr key_size, | |
| void* value_out, | |
| GLsizeiptr value_size, | |
| const void* user_param) { | |
| DCHECK(user_param != nullptr); | |
| GpuPersistentCache* cache = | |
| static_cast<GpuPersistentCache*>(const_cast<void*>(user_param)); | |
| return cache->GLBlobCacheGet(key, key_size, value_out, value_size); | |
| } | |
| void GL_APIENTRY GLBlobCacheSetCallback(const void* key, | |
| GLsizeiptr key_size, | |
| const void* value, | |
| GLsizeiptr value_size, | |
| const void* user_param) { | |
| DCHECK(user_param != nullptr); | |
| GpuPersistentCache* cache = | |
| static_cast<GpuPersistentCache*>(const_cast<void*>(user_param)); | |
| cache->GLBlobCacheSet(key, key_size, value, value_size); | |
| } | |
| // Cache prefix name used in all histograms, eg: | |
| // GPU.PersistentCache.{CachePrefix}.MetricName | |
| // Do not modify without changing | |
| // tools/metrics/histograms/metadata/gpu/histograms.xml | |
| const char* GetCacheHistogramPrefix(GpuDiskCacheHandle handle) { | |
| switch (GetHandleType(handle)) { | |
| case GpuDiskCacheType::kGlShaders: | |
| return IsReservedGpuDiskCacheHandle(handle) ? "Ganesh" : "WebGL"; | |
| case GpuDiskCacheType::kDawnWebGPU: | |
| return "WebGPU"; | |
| case GpuDiskCacheType::kDawnGraphite: | |
| return "GraphiteDawn"; | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| std::string GetHistogramName(std::string_view prefix, std::string_view metric) { | |
| return "GPU.PersistentCache." + std::string(prefix) + "." + | |
| std::string(metric); | |
| } | |
| NOINLINE NOOPT void HandlePersistentCacheError( | |
| GpuProcessShmCount* use_shader_cache_shm_count, | |
| persistent_cache::TransactionError error) { | |
| LOG(ERROR) << "Persistent cache error: " << static_cast<int>(error); | |
| switch (error) { | |
| case persistent_cache::TransactionError::kPermanent: | |
| if (use_shader_cache_shm_count) { | |
| GpuProcessShmCount::ScopedIncrement scoped_increment( | |
| use_shader_cache_shm_count); | |
| base::ImmediateCrash(); | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| bool TimedWait(base::ConditionVariable& cond_var, | |
| base::TimeDelta timeout, | |
| base::FunctionRef<bool()> wait_condition) { | |
| base::TimeTicks deadline = base::TimeTicks::Now() + timeout; | |
| while (wait_condition()) { | |
| base::TimeDelta remaining = deadline - base::TimeTicks::Now(); | |
| if (!remaining.is_positive()) { | |
| return false; // Timeout | |
| } | |
| cond_var.TimedWait(remaining); | |
| } | |
| return true; | |
| } | |
| #if BUILDFLAG(USE_DAWN) || BUILDFLAG(SKIA_USE_DAWN) | |
| bool IsVkPipelineCache(std::string_view key_str) { | |
| // Dawn/Vulkan appends a suffix to the cache key intentionally. | |
| return key_str.ends_with("MonolithicVkPipelineCache"); | |
| } | |
| #endif | |
| std::string GetCacheMetadataKey() { | |
| return version_info::GetProductNameAndVersionForUserAgent(); | |
| } | |
| } // namespace | |
| // AsyncDiskWriteOpts | |
| GpuPersistentCache::AsyncDiskWriteOpts::AsyncDiskWriteOpts() = default; | |
| GpuPersistentCache::AsyncDiskWriteOpts::AsyncDiskWriteOpts( | |
| const AsyncDiskWriteOpts&) = default; | |
| GpuPersistentCache::AsyncDiskWriteOpts::AsyncDiskWriteOpts( | |
| AsyncDiskWriteOpts&&) = default; | |
| GpuPersistentCache::AsyncDiskWriteOpts::~AsyncDiskWriteOpts() = default; | |
| GpuPersistentCache::AsyncDiskWriteOpts& | |
| GpuPersistentCache::AsyncDiskWriteOpts::operator=(const AsyncDiskWriteOpts&) = | |
| default; | |
| GpuPersistentCache::AsyncDiskWriteOpts& | |
| GpuPersistentCache::AsyncDiskWriteOpts::operator=(AsyncDiskWriteOpts&&) = | |
| default; | |
| // MetadataOpts | |
| GpuPersistentCache::MetadataOpts::MetadataOpts() = default; | |
| // Ref-counted wrapper for the persistent cache data, so it can be used safely | |
| // with asynchronous operations. | |
| struct GpuPersistentCache::DiskCache | |
| : public base::RefCountedThreadSafe<DiskCache> { | |
| explicit DiskCache( | |
| std::string_view cache_prefix, | |
| std::unique_ptr<persistent_cache::PersistentCache> cache, | |
| const GpuPersistentCache::AsyncDiskWriteOpts& async_write_options, | |
| scoped_refptr<RefCountedGpuProcessShmCount> use_shader_cache_shm_count); | |
| GpuPersistentCache::CacheLoadResult Load( | |
| std::string_view key, | |
| persistent_cache::BufferProvider buffer_provider); | |
| void Store(scoped_refptr<MemoryCacheEntry> entry); | |
| const persistent_cache::PersistentCache& persistent_cache() const { | |
| return *cache_; | |
| } | |
| private: | |
| friend class base::RefCountedThreadSafe<DiskCache>; | |
| ~DiskCache(); | |
| void SignalUsingCacheComplete(); | |
| void DoStoreToDisk(scoped_refptr<MemoryCacheEntry> entry); | |
| void DoDelayedStoreToDisk(scoped_refptr<MemoryCacheEntry> entry, | |
| uint64_t idle_id); | |
| const std::string cache_prefix_; | |
| const std::unique_ptr<persistent_cache::PersistentCache> cache_; | |
| // Used to track cache activity to delay writes until idle. Relaxed memory | |
| // order is sufficient because we only need to detect if any change has | |
| // occurred, and the variable doesn't need to synchronize with other | |
| // variables. | |
| std::atomic<uint64_t> current_idle_id_{0}; | |
| // Used to track the total bytes of pending writes. Relaxed memory order is | |
| // sufficient as this is used as a heuristic and strict synchronization is not | |
| // required. | |
| std::atomic<size_t> pending_bytes_to_write_{0}; | |
| const scoped_refptr<base::SequencedTaskRunner> disk_write_task_runner_; | |
| const size_t max_pending_bytes_to_write_; | |
| const scoped_refptr<RefCountedGpuProcessShmCount> use_shader_cache_shm_count_; | |
| // Synchronization primitives to enforce timed waits between reads & writes. | |
| base::Lock cache_in_use_mutex_; | |
| base::ConditionVariable cache_in_use_cond_var_{&cache_in_use_mutex_}; | |
| bool cache_in_use_ GUARDED_BY(cache_in_use_mutex_) = false; | |
| }; | |
| GpuPersistentCache::DiskCache::DiskCache( | |
| std::string_view cache_prefix, | |
| std::unique_ptr<persistent_cache::PersistentCache> cache, | |
| const GpuPersistentCache::AsyncDiskWriteOpts& async_write_options, | |
| scoped_refptr<RefCountedGpuProcessShmCount> use_shader_cache_shm_count) | |
| : cache_prefix_(cache_prefix), | |
| cache_(std::move(cache)), | |
| disk_write_task_runner_(async_write_options.task_runner), | |
| max_pending_bytes_to_write_( | |
| async_write_options.max_pending_bytes_to_write), | |
| use_shader_cache_shm_count_(std::move(use_shader_cache_shm_count)) {} | |
| GpuPersistentCache::DiskCache::~DiskCache() = default; | |
| void GpuPersistentCache::DiskCache::SignalUsingCacheComplete() { | |
| { | |
| base::AutoLock lock(cache_in_use_mutex_); | |
| DCHECK(cache_in_use_); | |
| cache_in_use_ = false; | |
| } | |
| cache_in_use_cond_var_.Signal(); | |
| } | |
| GpuPersistentCache::CacheLoadResult GpuPersistentCache::DiskCache::Load( | |
| std::string_view key, | |
| persistent_cache::BufferProvider buffer_provider) { | |
| ScopedHistogramTimer timer(GetHistogramName(cache_prefix_, "Load")); | |
| DiskCacheTraceScope trace_scope("GpuPersistentCache::DiskCache::Load"); | |
| const uint64_t idle_id = | |
| current_idle_id_.fetch_add(1, std::memory_order_relaxed) + 1; | |
| trace_scope.SetIdleId(idle_id); | |
| // The persistent cache backend can't read and write in parallel. Wait for | |
| // any pending writes/reads to complete before loading from the cache, to | |
| // avoid long waits in the backend. We wait for a maximum of 10ms. | |
| { | |
| base::ScopedAllowBaseSyncPrimitives allow_base_sync_primitives; | |
| base::AutoLock lock(cache_in_use_mutex_); | |
| if (!TimedWait(cache_in_use_cond_var_, kDiskOpWaitTimeoutMs, | |
| [this]() { return cache_in_use_; })) { | |
| // Treat as cache miss | |
| return CacheLoadResult::kMissTimeout; | |
| } | |
| cache_in_use_ = true; | |
| } | |
| // The work | |
| base::expected<std::optional<persistent_cache::EntryMetadata>, | |
| persistent_cache::TransactionError> | |
| result; | |
| { | |
| TRACE_EVENT0("gpu", "GpuPersistentCache::DiskCache::Cache::Find"); | |
| result = cache_->Find(base::as_byte_span(key), buffer_provider); | |
| } | |
| // Notify other threads | |
| SignalUsingCacheComplete(); | |
| ASSIGN_OR_RETURN( | |
| auto metadata, result, [&](persistent_cache::TransactionError error) { | |
| HandlePersistentCacheError(&use_shader_cache_shm_count_->data, error); | |
| return CacheLoadResult::kMissTransactionError; | |
| }); | |
| // Hit if present; miss otherwise. | |
| return metadata.has_value() ? CacheLoadResult::kHitDisk | |
| : CacheLoadResult::kMiss; | |
| } | |
| void GpuPersistentCache::DiskCache::Store( | |
| scoped_refptr<MemoryCacheEntry> entry) { | |
| DiskCacheTraceScope trace_scope("GpuPersistentCache::DiskCache::Store"); | |
| const uint64_t idle_id = | |
| current_idle_id_.fetch_add(1, std::memory_order_relaxed) + 1; | |
| trace_scope.SetIdleId(idle_id); | |
| if (!disk_write_task_runner_) { | |
| // No async task runner, write to disk immediately. | |
| DoStoreToDisk(entry); | |
| return; | |
| } | |
| // Increment the pending bytes in write queue. | |
| const size_t bytes_to_write = entry->TotalSize(); | |
| const auto pending_bytes = pending_bytes_to_write_.fetch_add( | |
| bytes_to_write, std::memory_order_relaxed); | |
| trace_scope.SetPendingBytes(pending_bytes + bytes_to_write); | |
| disk_write_task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&GpuPersistentCache::DiskCache::DoDelayedStoreToDisk, | |
| base::WrapRefCounted(this), std::move(entry), idle_id), | |
| kDiskWriteDelaySeconds); | |
| } | |
| void GpuPersistentCache::DiskCache::DoStoreToDisk( | |
| scoped_refptr<MemoryCacheEntry> entry) { | |
| ScopedHistogramTimer timer(GetHistogramName(cache_prefix_, "Store")); | |
| TRACE_EVENT0("gpu", "GpuPersistentCache::DiskCache::DoStoreToDisk"); | |
| { | |
| base::ScopedAllowBaseSyncPrimitives allow_base_sync_primitives; | |
| base::AutoLock lock(cache_in_use_mutex_); | |
| // Wait until the cache is not in use. | |
| while (cache_in_use_) { | |
| cache_in_use_cond_var_.Wait(); | |
| } | |
| cache_in_use_ = true; | |
| } | |
| // The work. | |
| base::expected<void, persistent_cache::TransactionError> result; | |
| { | |
| TRACE_EVENT0("gpu", "GpuPersistentCache::DiskCache::Cache::Insert"); | |
| result = cache_->Insert(base::as_byte_span(entry->Key()), entry->Data()); | |
| } | |
| // Unblock other threads. | |
| SignalUsingCacheComplete(); | |
| RETURN_IF_ERROR(result, [&](persistent_cache::TransactionError error) { | |
| HandlePersistentCacheError(&use_shader_cache_shm_count_->data, error); | |
| }); | |
| } | |
| void GpuPersistentCache::DiskCache::DoDelayedStoreToDisk( | |
| scoped_refptr<MemoryCacheEntry> entry, | |
| uint64_t idle_id) { | |
| DiskCacheTraceScope trace_scope( | |
| "GpuPersistentCache::DiskCache::DoDelayedStoreToDisk"); | |
| trace_scope.SetIdleId(idle_id); | |
| // The idle ID is used to check if there has been any cache activity since | |
| // the delayed store task was posted. If the IDs don't match, it means | |
| // another cache operation has occurred, so we reschedule the task to wait | |
| // for the next idle period. This ensures that we only perform the write | |
| // when the cache is truly idle. | |
| const uint64_t current_idle_id = | |
| current_idle_id_.load(std::memory_order_relaxed); | |
| const bool idle_id_match = current_idle_id == idle_id; | |
| // We also force writing if the pending bytes exceed limit. | |
| const size_t pending_bytes = | |
| pending_bytes_to_write_.load(std::memory_order_relaxed); | |
| const bool exceed_max_pending_bytes = | |
| pending_bytes > max_pending_bytes_to_write_; | |
| trace_scope.SetPendingBytes(pending_bytes); | |
| if (idle_id_match || exceed_max_pending_bytes) { | |
| DoStoreToDisk(entry); | |
| pending_bytes_to_write_.fetch_sub(entry->TotalSize(), | |
| std::memory_order_relaxed); | |
| return; | |
| } | |
| // Re-schedule the write since the cache is not idle. | |
| disk_write_task_runner_->PostDelayedTask( | |
| FROM_HERE, | |
| base::BindOnce(&GpuPersistentCache::DiskCache::DoDelayedStoreToDisk, | |
| base::WrapRefCounted(this), std::move(entry), | |
| current_idle_id), | |
| kDiskWriteDelaySeconds); | |
| } | |
| // GpuPersistentCache | |
| GpuPersistentCache::GpuPersistentCache(std::string_view cache_prefix, | |
| scoped_refptr<MemoryCache> memory_cache, | |
| MetadataOpts metadata_options, | |
| AsyncDiskWriteOpts async_write_options) | |
| : cache_prefix_(cache_prefix), | |
| memory_cache_(std::move(memory_cache)), | |
| async_write_options_(std::move(async_write_options)), | |
| metadata_options_(std::move(metadata_options)) { | |
| if (metadata_options_.enabled) { | |
| cache_metadata_ = CacheMetadata(); | |
| } | |
| } | |
| GpuPersistentCache::~GpuPersistentCache() = default; | |
| void GpuPersistentCache::InitializeCache( | |
| persistent_cache::PendingBackend pending_backend, | |
| scoped_refptr<RefCountedGpuProcessShmCount> use_shader_cache_shm_count) { | |
| CHECK(!disk_cache_initialized_.IsSet()); | |
| ASSIGN_OR_RETURN( | |
| auto cache, | |
| persistent_cache::PersistentCache::Bind( | |
| persistent_cache::Client::kShaderCache, std::move(pending_backend)), | |
| [&](persistent_cache::TransactionError error) { | |
| HandlePersistentCacheError(&use_shader_cache_shm_count->data, error); | |
| }); | |
| disk_cache_ = base::MakeRefCounted<DiskCache>( | |
| cache_prefix_, std::move(cache), async_write_options_, | |
| std::move(use_shader_cache_shm_count)); | |
| disk_cache_initialized_.Set(); | |
| base::UmaHistogramEnumeration( | |
| GetHistogramName(cache_prefix_, "MetadataEvent"), | |
| MetadataEvent::kCacheInitialized); | |
| if (memory_cache_) { | |
| // If opening the persistent cache succeeded, copy all entries from the | |
| // memory cache into it. | |
| memory_cache_->ForEach([this](MemoryCacheEntry* memory_entry) { | |
| // Query the existence of the disk cache entry by providing an empty | |
| // buffer so no data is copied. | |
| CacheLoadResult result = disk_cache_->Load( | |
| memory_entry->Key(), [](size_t) { return base::span<uint8_t>(); }); | |
| if (!IsCacheHitResult(result)) { | |
| disk_cache_->Store(memory_entry); | |
| } | |
| }); | |
| } | |
| if (metadata_options_.enabled) { | |
| // Load and process any metadata found in the disk cache. Run it | |
| // asynchronously if possible. | |
| if (async_write_options_.task_runner) { | |
| async_write_options_.task_runner->PostTask( | |
| FROM_HERE, | |
| base::BindOnce( | |
| &GpuPersistentCache::LoadAndProcessMetadataFromDiskCache, | |
| base::WrapRefCounted(this))); | |
| } else { | |
| LoadAndProcessMetadataFromDiskCache(); | |
| } | |
| } | |
| } | |
| #if BUILDFLAG(USE_DAWN) || BUILDFLAG(SKIA_USE_DAWN) | |
| size_t GpuPersistentCache::FindKey(std::string_view key) { | |
| size_t discovered_size = 0; | |
| auto buffer_provider = [&discovered_size](size_t content_size) { | |
| discovered_size = content_size; | |
| return base::span<uint8_t>(); | |
| }; | |
| CacheLoadResult result = LoadImpl(key, std::move(buffer_provider)); | |
| RecordCacheLoadResultHistogram(result); | |
| if (IsCacheHitResult(result) && IsVkPipelineCache(key)) { | |
| base::UmaHistogramMemoryKB( | |
| GetHistogramName(cache_prefix_, "VkPipelineCache.LoadedSize"), | |
| base::ByteSize(discovered_size)); | |
| } | |
| return discovered_size; | |
| } | |
| size_t GpuPersistentCache::LoadData(std::string_view key, | |
| base::span<uint8_t> dest) { | |
| if (dest.empty()) { | |
| return FindKey(key); | |
| } | |
| size_t discovered_size = 0; | |
| auto buffer_provider = [dest, &discovered_size](size_t content_size) { | |
| discovered_size = content_size; | |
| if (dest.size() >= content_size) { | |
| return dest.first(content_size); | |
| } | |
| return base::span<uint8_t>(); | |
| }; | |
| CacheLoadResult result = LoadImpl(key, std::move(buffer_provider)); | |
| if (!IsCacheHitResult(result)) { | |
| RecordCacheLoadResultHistogram(result); | |
| } else if (dest.size() < discovered_size) { | |
| // If the buffer size provided is smaller than the data, return 0. | |
| return 0; | |
| } | |
| return discovered_size; | |
| } | |
| size_t GpuPersistentCache::FindKey(std::span<const std::byte> key) { | |
| std::string_view key_str(reinterpret_cast<const char*>(key.data()), | |
| key.size()); | |
| return FindKey(key_str); | |
| } | |
| size_t GpuPersistentCache::LoadData(std::span<const std::byte> key, | |
| std::span<std::byte> dest) { | |
| std::string_view key_str(reinterpret_cast<const char*>(key.data()), | |
| key.size()); | |
| // SAFETY: `dest` is provided by the caller who is responsible. | |
| base::span<uint8_t> dest_span = UNSAFE_BUFFERS( | |
| base::span(reinterpret_cast<uint8_t*>(dest.data()), dest.size())); | |
| return LoadData(key_str, dest_span); | |
| } | |
| #endif | |
| sk_sp<SkData> GpuPersistentCache::load(const SkData& key) { | |
| std::string_view key_str(static_cast<const char*>(key.data()), key.size()); | |
| sk_sp<SkData> output_data; | |
| // A BufferProvider for PersistentCache that allocates a new SkData to hold an | |
| // entry's content and returns a view into it. | |
| auto buffer_provider = [&output_data](size_t content_size) { | |
| output_data = SkData::MakeUninitialized(content_size); | |
| // SAFETY: SkData doesn't provide an API to get its buffer as a | |
| // writeable span. | |
| return UNSAFE_BUFFERS( | |
| base::span(static_cast<uint8_t*>(output_data->writable_data()), | |
| output_data->size())); | |
| }; | |
| CacheLoadResult result = LoadImpl(key_str, std::move(buffer_provider)); | |
| RecordCacheLoadResultHistogram(result); | |
| return output_data; | |
| } | |
| int64_t GpuPersistentCache::GLBlobCacheGet(const void* key, | |
| int64_t key_size, | |
| void* value_out, | |
| int64_t value_size) { | |
| CHECK_GE(key_size, 0); | |
| std::string_view key_str(static_cast<const char*>(key), | |
| static_cast<size_t>(key_size)); | |
| size_t discovered_size = 0; | |
| // A BufferProvider for PersistentCache that puts the size of the content, in | |
| // bytes, into `discovered_size` and returns a view into the buffer at | |
| // `value_out` if it is big enough or an empty span otherwise. | |
| // SAFETY: Caller provides either null `value_out` or `value_out` plus | |
| // `value_size`. | |
| auto buffer_provider = | |
| [value = UNSAFE_BUFFERS(base::span(static_cast<uint8_t*>(value_out), | |
| static_cast<size_t>(value_size))), | |
| &discovered_size](size_t content_size) { | |
| // Cache hit: retain the size to return to the caller. | |
| discovered_size = content_size; | |
| if (value.size() >= content_size) { | |
| return value.first(content_size); | |
| } | |
| return base::span<uint8_t>(); | |
| }; | |
| CacheLoadResult result = LoadImpl(key_str, std::move(buffer_provider)); | |
| if (!IsCacheHitResult(result) || value_size == 0) { | |
| // This function is called twice in the cache hit case, once to query the | |
| // size of the buffer and again with a buffer to write into. To avoid | |
| // skewing the metrics by generating two cache hit data points, only record | |
| // a cache hit when there is no buffer provided. | |
| RecordCacheLoadResultHistogram(result); | |
| } | |
| // If the buffer size provided is smaller than the data, return 0. | |
| // We exclude value_size == 0 because that is used to query the size of the | |
| // cached data without reading it. | |
| if (value_size > 0 && static_cast<size_t>(value_size) < discovered_size) { | |
| return 0; | |
| } | |
| return discovered_size; | |
| } | |
| void GpuPersistentCache::OnUpdateMemoryLimit(int memory_limit) { | |
| if (memory_cache_) { | |
| memory_cache_->OnUpdateMemoryLimit(memory_limit); | |
| } | |
| } | |
| void GpuPersistentCache::OnReleaseMemory(int memory_limit) { | |
| if (memory_cache_) { | |
| memory_cache_->OnReleaseMemory(memory_limit); | |
| } | |
| } | |
| void GpuPersistentCache::OnMemoryDump( | |
| const std::string& dump_name, | |
| base::trace_event::ProcessMemoryDump* pmd) { | |
| if (memory_cache_) { | |
| memory_cache_->OnMemoryDump(dump_name, pmd); | |
| } | |
| } | |
| const persistent_cache::PersistentCache& | |
| GpuPersistentCache::GetPersistentCacheForTesting() const { | |
| return disk_cache_->persistent_cache(); | |
| } | |
| void GpuPersistentCache::FlushMetadataForTesting() { | |
| if (!metadata_options_.enabled) { | |
| return; | |
| } | |
| base::AutoLock lock(metadata_mutex_); | |
| if (!cache_metadata_) { | |
| return; | |
| } | |
| FlushCacheMetadata(std::move(cache_metadata_.value())); | |
| cache_metadata_.reset(); | |
| } | |
| bool GpuPersistentCache::IsCacheHitResult(CacheLoadResult result) { | |
| return result > CacheLoadResult::kMaxMissValue; | |
| } | |
| GpuPersistentCache::CacheLoadResult GpuPersistentCache::LoadImpl( | |
| std::string_view key, | |
| persistent_cache::BufferProvider buffer_provider) { | |
| const bool disk_cache_initialized = disk_cache_initialized_.IsSet(); | |
| TRACE_EVENT1("gpu", "GpuPersistentCache::LoadImpl", "persistent_cache", | |
| disk_cache_initialized); | |
| // Track cache available for the 1st kMaxLoadStoreForTrackingCacheAvailable | |
| // loads. | |
| if (load_count_.fetch_add(1, std::memory_order_relaxed) < | |
| kMaxLoadStoreForTrackingCacheAvailable) { | |
| base::UmaHistogramBoolean( | |
| GetHistogramName(cache_prefix_, "Load.CacheAvailable"), | |
| disk_cache_initialized); | |
| } | |
| UpdateCacheMetadataForLoad(key); | |
| if (memory_cache_) { | |
| if (auto memory_entry = memory_cache_->Find(key)) { | |
| base::span<uint8_t> output_buffer = | |
| buffer_provider(memory_entry->DataSize()); | |
| memory_entry->ReadData(output_buffer.data(), output_buffer.size()); | |
| return CacheLoadResult::kHitMemory; | |
| } | |
| } | |
| if (!disk_cache_initialized) { | |
| return CacheLoadResult::kMissNoDiskCache; | |
| } | |
| base::span<uint8_t> provided_buffer; | |
| base::HeapArray<uint8_t> local_allocated_buffer; | |
| // A BufferProvider for PersistentCache that returns one of: | |
| // 1. a view into the buffer at `provided_buffer` if it is big enough | |
| // 2. an empty span if no memory_cache_ exists, or | |
| // 3. a view into a new base::HeapArray (`local_allocated_buffer`) | |
| auto wrapped_buffer_provider = | |
| [buffer_provider, memory_cache_exists = memory_cache_ != nullptr, | |
| &provided_buffer, &local_allocated_buffer](size_t content_size) { | |
| // First attempt to use the buffer_provider to allocate a buffer for the | |
| // result. | |
| provided_buffer = buffer_provider(content_size); | |
| // If the `provided_buffer` is large enough, simply return it and let | |
| // the disk cache write into it | |
| if (provided_buffer.size() >= content_size) { | |
| return provided_buffer.first(content_size); // Case 1. | |
| } | |
| if (!memory_cache_exists) { | |
| return base::span<uint8_t>(); // Case 2. | |
| } | |
| // Allocate our own buffer into `local_allocated_buffer` so the result | |
| // can be put in the memory cache | |
| DCHECK(content_size != 0); | |
| local_allocated_buffer = base::HeapArray<uint8_t>::Uninit(content_size); | |
| return base::span<uint8_t>(local_allocated_buffer); // Case 3. | |
| }; | |
| CacheLoadResult disk_load_result = | |
| disk_cache_->Load(key, wrapped_buffer_provider); | |
| if (!IsCacheHitResult(disk_load_result)) { | |
| return disk_load_result; | |
| } | |
| if (memory_cache_) { | |
| // Verify the assumptions above. There should always be data in one of the | |
| // two buffers if the load was successful and a memory cache exists. | |
| DCHECK(!local_allocated_buffer.empty() || !provided_buffer.empty()); | |
| // After loading from the disk cache, copy the entry into the memory cache | |
| // for faster access on future loads. | |
| if (!local_allocated_buffer.empty()) { | |
| // Prefer the `local_allocated_buffer` because it can be moved directly | |
| // into the memory cache. | |
| memory_cache_->Store(key, std::move(local_allocated_buffer)); | |
| } else { | |
| // Otherwise we need to copy the result from the user provided buffer | |
| memory_cache_->Store(key, provided_buffer); | |
| } | |
| } | |
| return disk_load_result; | |
| } | |
| #if BUILDFLAG(USE_DAWN) || BUILDFLAG(SKIA_USE_DAWN) | |
| void GpuPersistentCache::StoreData(std::string_view key, | |
| base::span<const uint8_t> src) { | |
| // Serialized VkPipelineCache entries won't be loaded again until the GPU | |
| // process restarts. Storing this entry in the memory cache isn't useful but | |
| // does evict otherwise useful data. | |
| const bool skip_memory_cache = IsVkPipelineCache(key); | |
| StoreImpl(key, src, skip_memory_cache); | |
| } | |
| void GpuPersistentCache::StoreData(std::span<const std::byte> key, | |
| std::span<const std::byte> src) { | |
| std::string_view key_str(reinterpret_cast<const char*>(key.data()), | |
| key.size()); | |
| // SAFETY: `src` is provided by the caller who is responsible. | |
| base::span<const uint8_t> src_span = UNSAFE_BUFFERS( | |
| base::span(reinterpret_cast<const uint8_t*>(src.data()), src.size())); | |
| return StoreData(key_str, src_span); | |
| } | |
| #endif | |
| void GpuPersistentCache::store(const SkData& key, const SkData& data) { | |
| std::string_view key_str(static_cast<const char*>(key.data()), key.size()); | |
| base::span<const uint8_t> value_span = UNSAFE_BUFFERS( | |
| base::span(static_cast<const uint8_t*>(data.bytes()), data.size())); | |
| StoreImpl(key_str, value_span, /*skip_memory_cache=*/false); | |
| } | |
| void GpuPersistentCache::GLBlobCacheSet(const void* key, | |
| int64_t key_size, | |
| const void* value, | |
| int64_t value_size) { | |
| CHECK_GE(key_size, 0); | |
| CHECK_GE(value_size, 0); | |
| std::string_view key_str(static_cast<const char*>(key), | |
| static_cast<size_t>(key_size)); | |
| base::span<const uint8_t> value_span = UNSAFE_BUFFERS(base::span( | |
| static_cast<const uint8_t*>(value), static_cast<size_t>(value_size))); | |
| StoreImpl(key_str, value_span, /*skip_memory_cache=*/false); | |
| } | |
| void GpuPersistentCache::StoreImpl(std::string_view key, | |
| base::span<const uint8_t> value, | |
| bool skip_memory_cache) { | |
| const bool disk_cache_initialized = disk_cache_initialized_.IsSet(); | |
| TRACE_EVENT1("gpu", "GpuPersistentCache::StoreImpl", "persistent_cache", | |
| disk_cache_initialized); | |
| // Track cache available for the 1st kMaxLoadStoreForTrackingCacheAvailable | |
| // stores. | |
| if (store_count_.fetch_add(1, std::memory_order_relaxed) < | |
| kMaxLoadStoreForTrackingCacheAvailable) { | |
| base::UmaHistogramBoolean( | |
| GetHistogramName(cache_prefix_, "Store.CacheAvailable"), | |
| disk_cache_initialized); | |
| } | |
| // Record the cache key and value sizes infrequently. | |
| if (base::ShouldRecordSubsampledMetric(kRecordCacheEntrySizeFrequency)) { | |
| base::UmaHistogramCounts1M(GetHistogramName(cache_prefix_, "KeySize2"), | |
| key.size()); | |
| base::UmaHistogramCounts1M(GetHistogramName(cache_prefix_, "ValueSize2"), | |
| value.size()); | |
| } | |
| scoped_refptr<MemoryCacheEntry> memory_cache_entry; | |
| if (memory_cache_ && !skip_memory_cache) { | |
| memory_cache_entry = memory_cache_->Store(key, value); | |
| } | |
| if (!disk_cache_initialized) { | |
| return; | |
| } | |
| // If there was no memory cache, wrap the data in a new MemoryCacheEntry for | |
| // insertion. | |
| if (!memory_cache_entry) { | |
| memory_cache_entry = base::MakeRefCounted<MemoryCacheEntry>(key, value); | |
| } | |
| disk_cache_->Store(memory_cache_entry); | |
| } | |
| void GpuPersistentCache::RecordCacheLoadResultHistogram( | |
| CacheLoadResult result) { | |
| base::UmaHistogramEnumeration(GetHistogramName(cache_prefix_, "LoadResult"), | |
| result); | |
| } | |
| // CacheMetadata | |
| GpuPersistentCache::CacheMetadata::CacheMetadata::CacheMetadata() = default; | |
| GpuPersistentCache::CacheMetadata::CacheMetadata(const CacheMetadata&) = | |
| default; | |
| GpuPersistentCache::CacheMetadata::CacheMetadata(CacheMetadata&&) = default; | |
| GpuPersistentCache::CacheMetadata::~CacheMetadata() = default; | |
| GpuPersistentCache::CacheMetadata& GpuPersistentCache::CacheMetadata::operator=( | |
| const CacheMetadata&) = default; | |
| GpuPersistentCache::CacheMetadata& GpuPersistentCache::CacheMetadata::operator=( | |
| CacheMetadata&&) = default; | |
| base::HeapArray<uint8_t> GpuPersistentCache::CacheMetadata::Serialize() const { | |
| base::CheckedNumeric<size_t> buffer_size_needed = 0; | |
| buffer_size_needed += sizeof(uint64_t); // Count of first_loaded_keys | |
| for (const std::string& key : first_loaded_keys) { | |
| buffer_size_needed += sizeof(uint64_t); | |
| buffer_size_needed += key.size(); | |
| } | |
| auto serialized_data = | |
| base::HeapArray<uint8_t>::WithSize(buffer_size_needed.ValueOrDie()); | |
| size_t current_buffer_offset = 0; | |
| auto write_serialized_data = [&serialized_data, ¤t_buffer_offset]( | |
| base::span<const uint8_t> data) { | |
| base::span<uint8_t> dst_span = | |
| serialized_data.subspan(current_buffer_offset, data.size()); | |
| dst_span.copy_from_nonoverlapping(data); | |
| current_buffer_offset += data.size(); | |
| }; | |
| uint64_t first_loaded_keys_count = first_loaded_keys.size(); | |
| write_serialized_data( | |
| base::as_byte_span(base::span_from_ref(first_loaded_keys_count))); | |
| for (const std::string& key : first_loaded_keys) { | |
| uint64_t key_size = key.size(); | |
| write_serialized_data(base::as_byte_span(base::span_from_ref(key_size))); | |
| write_serialized_data(base::as_byte_span(key)); | |
| } | |
| return serialized_data; | |
| } | |
| GpuPersistentCache::CacheMetadata | |
| GpuPersistentCache::CacheMetadata::Deserialize( | |
| const base::HeapArray<uint8_t>& data) { | |
| size_t current_buffer_offset = 0; | |
| auto read_serialized_data = | |
| [&data, ¤t_buffer_offset](base::span<uint8_t> dst_data) { | |
| base::span<const uint8_t> src_data = | |
| data.subspan(current_buffer_offset, dst_data.size()); | |
| dst_data.copy_from_nonoverlapping(src_data); | |
| current_buffer_offset += dst_data.size(); | |
| }; | |
| auto read_serialized_string = [read_serialized_data]() { | |
| uint64_t string_length = 0; | |
| read_serialized_data( | |
| base::as_writable_bytes(base::span_from_ref(string_length))); | |
| std::string result; | |
| result.resize(string_length); | |
| read_serialized_data(base::as_writable_bytes(base::span(result))); | |
| return result; | |
| }; | |
| CacheMetadata metadata; | |
| uint64_t first_loaded_keys_count = 0; | |
| read_serialized_data( | |
| base::as_writable_bytes(base::span_from_ref(first_loaded_keys_count))); | |
| for (uint64_t key_index = 0; key_index < first_loaded_keys_count; | |
| key_index++) { | |
| metadata.first_loaded_keys.insert(read_serialized_string()); | |
| } | |
| return metadata; | |
| } | |
| void GpuPersistentCache::UpdateCacheMetadataForLoad(std::string_view key) { | |
| if (!metadata_options_.enabled) { | |
| return; | |
| } | |
| base::AutoLock lock(metadata_mutex_); | |
| if (!cache_metadata_) { | |
| return; | |
| } | |
| if (cache_metadata_->first_loaded_keys.size() < | |
| metadata_options_.preload_count) { | |
| cache_metadata_->first_loaded_keys.insert( | |
| std::string(key.data(), key.size())); | |
| } | |
| // Right now the metadata only contains the first_loaded_keys so flush once we | |
| // can't fit any more in the set. If the metadata contains more data in the | |
| // future, we may want to flush multiple times. | |
| bool flush_metadata = cache_metadata_->first_loaded_keys.size() >= | |
| metadata_options_.preload_count; | |
| if (flush_metadata) { | |
| base::UmaHistogramEnumeration( | |
| GetHistogramName(cache_prefix_, "MetadataEvent"), | |
| MetadataEvent::kMetadataFlushed); | |
| FlushCacheMetadata(std::move(cache_metadata_.value())); | |
| cache_metadata_.reset(); | |
| } | |
| } | |
| void GpuPersistentCache::FlushCacheMetadata(CacheMetadata metadata) { | |
| // If there is no disk cache to flush to, drop the metadata. | |
| const bool disk_cache_initialized = disk_cache_initialized_.IsSet(); | |
| if (!disk_cache_initialized) { | |
| return; | |
| } | |
| auto entry = base::MakeRefCounted<MemoryCacheEntry>(GetCacheMetadataKey(), | |
| metadata.Serialize()); | |
| disk_cache_->Store(std::move(entry)); | |
| } | |
| void GpuPersistentCache::LoadAndProcessMetadataFromDiskCache() { | |
| CHECK(disk_cache_); | |
| base::HeapArray<uint8_t> metadata_blob; | |
| bool metadata_exists = IsCacheHitResult(disk_cache_->Load( | |
| GetCacheMetadataKey(), [&metadata_blob](size_t content_size) { | |
| metadata_blob = base::HeapArray<uint8_t>::WithSize(content_size); | |
| return metadata_blob.first(content_size); | |
| })); | |
| if (metadata_exists) { | |
| base::UmaHistogramEnumeration( | |
| GetHistogramName(cache_prefix_, "MetadataEvent"), | |
| MetadataEvent::kMetadataLoaded); | |
| CHECK(!metadata_blob.empty()); | |
| CacheMetadata metadata = CacheMetadata::Deserialize(metadata_blob); | |
| ProcessMetadata(std::move(metadata)); | |
| } | |
| } | |
| void GpuPersistentCache::ProcessMetadata(const CacheMetadata& metadata) { | |
| for (const std::string& key : metadata.first_loaded_keys) { | |
| base::HeapArray<uint8_t> value; | |
| if (IsCacheHitResult(disk_cache_->Load(key, [&value](size_t content_size) { | |
| value = base::HeapArray<uint8_t>::WithSize(content_size); | |
| return value.first(content_size); | |
| }))) { | |
| memory_cache_->Store(key, std::move(value)); | |
| } | |
| } | |
| } | |
| void BindCacheToCurrentOpenGLContext(GpuPersistentCache* cache) { | |
| if (!cache || !gl::g_current_gl_driver->ext.b_GL_ANGLE_blob_cache) { | |
| return; | |
| } | |
| glBlobCacheCallbacksANGLE(GLBlobCacheSetCallback, GLBlobCacheGetCallback, | |
| cache); | |
| } | |
| void UnbindCacheFromCurrentOpenGLContext() { | |
| if (!gl::g_current_gl_driver->ext.b_GL_ANGLE_blob_cache) { | |
| return; | |
| } | |
| glBlobCacheCallbacksANGLE(nullptr, nullptr, nullptr); | |
| } | |
| GpuPersistentCacheCollection::GpuPersistentCacheCollection( | |
| size_t max_in_memory_cache_size, | |
| GpuPersistentCache::MetadataOpts metadata_options, | |
| GpuPersistentCache::AsyncDiskWriteOpts async_write_options) | |
| : max_in_memory_cache_size_(max_in_memory_cache_size), | |
| metadata_options_(metadata_options), | |
| async_write_options_(async_write_options) { | |
| if (base::SingleThreadTaskRunner::HasCurrentDefault()) { | |
| base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider( | |
| this, "GpuPersistentCache", | |
| base::SingleThreadTaskRunner::GetCurrentDefault()); | |
| } | |
| } | |
| GpuPersistentCacheCollection::~GpuPersistentCacheCollection() { | |
| base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider( | |
| this); | |
| } | |
| scoped_refptr<GpuPersistentCache> GpuPersistentCacheCollection::GetCache( | |
| const GpuDiskCacheHandle& handle) { | |
| base::AutoLock lock(mutex_); | |
| if (auto iter = caches_.find(handle); iter != caches_.end()) { | |
| return iter->second.get(); | |
| } | |
| auto memory_cache = | |
| base::MakeRefCounted<MemoryCache>(max_in_memory_cache_size_); | |
| auto [iter, inserted] = caches_.emplace( | |
| handle, base::MakeRefCounted<GpuPersistentCache>( | |
| GetCacheHistogramPrefix(handle), std::move(memory_cache), | |
| metadata_options_, async_write_options_)); | |
| DCHECK(inserted); | |
| iter->second->OnUpdateMemoryLimit(current_memory_limit_); | |
| return iter->second; | |
| } | |
| void GpuPersistentCacheCollection::OnUpdateMemoryLimit(int memory_limit) { | |
| base::AutoLock lock(mutex_); | |
| current_memory_limit_ = memory_limit; | |
| for (auto& [_, cache] : caches_) { | |
| cache->OnUpdateMemoryLimit(memory_limit); | |
| } | |
| } | |
| void GpuPersistentCacheCollection::OnReleaseMemory(int memory_limit) { | |
| base::AutoLock lock(mutex_); | |
| for (auto& [_, cache] : caches_) { | |
| cache->OnReleaseMemory(memory_limit); | |
| } | |
| } | |
| bool GpuPersistentCacheCollection::OnMemoryDump( | |
| const base::trace_event::MemoryDumpArgs& args, | |
| base::trace_event::ProcessMemoryDump* pmd) { | |
| base::AutoLock lock(mutex_); | |
| for (auto& [handle, cache] : caches_) { | |
| std::ostringstream dump_name; | |
| dump_name << "gpu/shader_cache/" << GetCacheHistogramPrefix(handle); | |
| if (!IsReservedGpuDiskCacheHandle(handle)) { | |
| int32_t value = GetHandleValue(handle); | |
| DCHECK_GE(value, 0); | |
| dump_name << "_" << value; | |
| } | |
| cache->OnMemoryDump(dump_name.str(), pmd); | |
| } | |
| return true; | |
| } | |
| } // namespace gpu |