// 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::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
