blob: d783b1642a92b24922a95e2968b76729d3c70974 [file]
// 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/memory_cache.h"
#include "base/feature_list.h"
#include "base/memory_coordinator/memory_coordinator_features.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_dump_request_args.h"
#include "gpu/command_buffer/service/service_utils.h"
namespace gpu {
MemoryCacheEntry::MemoryCacheEntry(std::string_view key,
base::span<const uint8_t> data)
: key_(key), data_(base::HeapArray<uint8_t>::CopiedFrom(data)) {}
MemoryCacheEntry::MemoryCacheEntry(std::string_view key,
base::HeapArray<uint8_t> data)
: key_(key), data_(std::move(data)) {}
MemoryCacheEntry::~MemoryCacheEntry() = default;
std::string_view MemoryCacheEntry::Key() const {
return key_;
}
size_t MemoryCacheEntry::TotalSize() const {
return key_.length() + data_.size();
}
size_t MemoryCacheEntry::DataSize() const {
return data_.size();
}
size_t MemoryCacheEntry::ReadData(void* value_out, size_t value_size) const {
// First handle "peek" case where use is trying to get the size of the entry.
if (value_out == nullptr && value_size == 0) {
return DataSize();
}
// If the buffer size provided is smaller than the data, return 0.
// This prevents heap buffer overflow writes.
if (value_size < DataSize()) {
return 0;
}
std::copy(data_.begin(), data_.end(), static_cast<uint8_t*>(value_out));
return DataSize();
}
base::span<const uint8_t> MemoryCacheEntry::Data() const {
return data_;
}
bool operator<(const scoped_refptr<MemoryCacheEntry>& lhs,
const scoped_refptr<MemoryCacheEntry>& rhs) {
return lhs->Key() < rhs->Key();
}
bool operator<(const scoped_refptr<MemoryCacheEntry>& lhs,
std::string_view rhs) {
return lhs->Key() < rhs;
}
bool operator<(std::string_view lhs,
const scoped_refptr<MemoryCacheEntry>& rhs) {
return lhs < rhs->Key();
}
MemoryCache::MemoryCache(size_t max_size,
std::string_view cache_hit_trace_event)
: max_size_(max_size),
size_limit_(max_size),
cache_hit_trace_event_(cache_hit_trace_event) {}
MemoryCache::~MemoryCache() = default;
scoped_refptr<MemoryCacheEntry> MemoryCache::Find(std::string_view key) {
// Because we are tracking LRU, even loads modify internal state so mutex is
// required.
base::AutoLock lock(mutex_);
auto it = entries_.find(key);
if (it == entries_.end()) {
return nullptr;
}
if (!cache_hit_trace_event_.empty()) {
TRACE_EVENT0("gpu", cache_hit_trace_event_.c_str());
}
// Even if this was just a "peek" operation to get size, the entry was
// accessed so move it to the back of the eviction queue.
scoped_refptr<MemoryCacheEntry>& entry = *it;
entry->RemoveFromList();
lru_.Append(entry.get());
return entry;
}
scoped_refptr<MemoryCacheEntry> MemoryCache::Store(
std::string_view key,
base::span<const uint8_t> data) {
base::AutoLock lock(mutex_);
EvictEntry(key);
if (!CanFitMemoryCacheEntry(key.size() + data.size())) {
return nullptr;
}
auto entry = base::MakeRefCounted<MemoryCacheEntry>(key, data);
InsertEntry(entry);
return entry;
}
scoped_refptr<MemoryCacheEntry> MemoryCache::Store(
std::string_view key,
base::HeapArray<uint8_t> data) {
base::AutoLock lock(mutex_);
EvictEntry(key);
if (!CanFitMemoryCacheEntry(key.size() + data.size())) {
return nullptr;
}
auto entry = base::MakeRefCounted<MemoryCacheEntry>(key, std::move(data));
InsertEntry(entry);
return entry;
}
void MemoryCache::OnUpdateMemoryLimit(int memory_limit) {
base::AutoLock lock(mutex_);
if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) {
size_t target_limit =
gpu::UpdateShaderCacheSizeOnMemoryLimit(max_size_, memory_limit);
// Ensure no memory is released during OnUpdateMemoryLimit.
size_limit_ = std::max(current_size_, target_limit);
}
}
void MemoryCache::OnReleaseMemory(int memory_limit) {
base::AutoLock lock(mutex_);
if (base::FeatureList::IsEnabled(base::kStatefulMemoryPressure)) {
// In OnUpdateMemoryLimit(), size_limit_ is clamped to current_size_
// to avoid unexpected eviction during subsequent Store() calls.
// When OnReleaseMemory() is called to explicitly free memory, we must
// update size_limit_ to the actual target limit before enforcing it.
size_limit_ =
gpu::UpdateShaderCacheSizeOnMemoryLimit(max_size_, memory_limit);
while (current_size_ > size_limit_) {
EvictEntry(lru_.head()->value());
}
return;
}
size_t new_limit =
gpu::UpdateShaderCacheSizeOnMemoryLimit(max_size_, memory_limit);
while (current_size_ > new_limit) {
EvictEntry(lru_.head()->value());
}
}
void MemoryCache::PurgeMemory(base::MemoryPressureLevel memory_pressure_level) {
base::AutoLock lock(mutex_);
size_t new_limit = gpu::UpdateShaderCacheSizeOnMemoryPressure(
max_size_, memory_pressure_level);
// Evict the least recently used entries until we reach the `new_limit`
while (current_size_ > new_limit) {
EvictEntry(lru_.head()->value());
}
}
void MemoryCache::OnMemoryDump(const std::string& dump_name,
base::trace_event::ProcessMemoryDump* pmd) {
base::AutoLock lock(mutex_);
using base::trace_event::MemoryAllocatorDump;
MemoryAllocatorDump* dump = pmd->CreateAllocatorDump(dump_name);
dump->AddScalar(MemoryAllocatorDump::kNameSize,
MemoryAllocatorDump::kUnitsBytes, current_size_);
dump->AddScalar(MemoryAllocatorDump::kNameObjectCount,
MemoryAllocatorDump::kUnitsObjects, entries_.size());
}
void MemoryCache::EvictEntry(std::string_view key) {
if (auto it = entries_.find(key); it != entries_.end()) {
const scoped_refptr<MemoryCacheEntry>& entry = *it;
EvictEntry(entry.get());
}
}
void MemoryCache::EvictEntry(MemoryCacheEntry* entry) {
// Always remove the entry from the LRU first because removing it from the
// entry map will cause the entry to be destroyed.
entry->RemoveFromList();
// Update the size information.
current_size_ -= entry->TotalSize();
// Finally remove the entry from the map thereby destroying the entry.
entries_.erase(entry->Key());
}
void MemoryCache::InsertEntry(scoped_refptr<MemoryCacheEntry> entry) {
// Evict least used entries until we have enough room to add the new entry.
while (current_size_ + entry->TotalSize() > size_limit_) {
EvictEntry(lru_.head()->value());
}
// Add the entry size to the overall size and update the eviction queue.
current_size_ += entry->TotalSize();
lru_.Append(entry.get());
auto [it, inserted] = entries_.insert(entry);
DCHECK(inserted);
}
bool MemoryCache::CanFitMemoryCacheEntry(size_t data_size) const {
// Don't need to do anything if we are not storing anything.
if (data_size == 0) {
return false;
}
// If this entry is larger than we can make space for, don't allow it to be
// stored.
if (data_size >= size_limit_) {
return false;
}
return true;
}
} // namespace gpu