blob: c98aed3dc3c3796eb66160df91f02ac7a9242f23 [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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 <cstring>
#include <string>
#include "base/bind.h"
#include "base/location.h"
#include "base/memory/ref_counted.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "net/base/io_buffer.h"
namespace gpu::webgpu {
namespace {
// TODO(dawn:549) Tune this timeout once we have some more data on what it
// should be.
static constexpr base::TimeDelta kCacheOpTimeout = base::Milliseconds(50);
} // namespace
// Custom deleter used for entries to close them on the internal backend thread.
struct OnTaskRunnerEntryDeleter {
explicit OnTaskRunnerEntryDeleter(
scoped_refptr<base::SequencedTaskRunner> task_runner)
: task_runner_(task_runner) {}
~OnTaskRunnerEntryDeleter() = default;
OnTaskRunnerEntryDeleter(OnTaskRunnerEntryDeleter&&) = default;
OnTaskRunnerEntryDeleter& operator=(OnTaskRunnerEntryDeleter&&) = default;
void operator()(disk_cache::Entry* ptr) {
if (ptr) {
task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&disk_cache::Entry::Close, base::Unretained(ptr)));
}
}
scoped_refptr<base::SequencedTaskRunner> task_runner_;
};
using ScopedEntryPtr =
std::unique_ptr<disk_cache::Entry, OnTaskRunnerEntryDeleter>;
class DawnCacheOperation
: public base::RefCountedThreadSafe<DawnCacheOperation> {
public:
enum Type {
kRead,
kWrite,
};
DawnCacheOperation(scoped_refptr<base::SingleThreadTaskRunner> backend_thread,
Type type,
size_t buffer_size);
// Runs this operation with the given key on the given backend.
void Run(const std::string& key, disk_cache::Backend* backend);
// Waits for the completion of this operation up to the timeout. Returns true
// iff the operation completed before the timeout.
bool TimedWait(base::TimeDelta timeout) { return signal_.TimedWait(timeout); }
// Returns the result of the operation. If the operation is not complete, this
// always returns 0.
size_t Result() const { return result_size_; }
void* Data() { return buffer_->data(); }
private:
friend class base::RefCountedThreadSafe<DawnCacheOperation>;
~DawnCacheOperation() = default;
// Returns the entry size or clamps errors (negative values) to 0.
size_t GetEntrySize() const;
// Callback to handle when an entry has been opened.
void OnOpenedEntry(disk_cache::EntryResult entry);
// Callback to handle when an operation has completed with a status. Note that
// statuses in the backend API are ints. Non-negative values indicate the size
// of the result while negative results are used to indicate some sort of
// error. For negative values, this will be a call to OnOpCompleteSize with 0
// as the argument.
void OnOpCompleteStatus(int status);
// Callback to handle a valid entry size (or 0 when the operation did not
// complete successfully) and save it to the result of this operation.
void OnOpCompleteSize(size_t size);
scoped_refptr<base::SingleThreadTaskRunner> backend_thread_;
Type type_;
scoped_refptr<net::IOBuffer> buffer_;
size_t buffer_size_;
ScopedEntryPtr entry_;
size_t result_size_ = 0;
base::WaitableEvent signal_;
};
DawnCacheOperation::DawnCacheOperation(
scoped_refptr<base::SingleThreadTaskRunner> backend_thread,
Type type,
size_t buffer_size)
: backend_thread_(backend_thread),
type_(type),
buffer_(buffer_size > 0
? base::WrapRefCounted(new net::IOBuffer(buffer_size))
: nullptr),
buffer_size_(buffer_size),
entry_(nullptr, OnTaskRunnerEntryDeleter(backend_thread_)),
signal_(base::WaitableEvent::ResetPolicy::MANUAL,
base::WaitableEvent::InitialState::NOT_SIGNALED) {}
void DawnCacheOperation::Run(const std::string& key,
disk_cache::Backend* backend) {
disk_cache::EntryResult entry = backend->OpenOrCreateEntry(
key, net::RequestPriority::DEFAULT_PRIORITY,
base::BindOnce(&DawnCacheOperation::OnOpenedEntry, this));
if (entry.net_error() == net::Error::OK) {
backend_thread_->PostTask(
FROM_HERE, base::BindOnce(&DawnCacheOperation::OnOpenedEntry, this,
std::move(entry)));
}
}
size_t DawnCacheOperation::GetEntrySize() const {
if (entry_ != nullptr) {
int tmp = entry_->GetDataSize(0);
if (tmp > 0) {
return size_t(tmp);
}
}
return 0u;
}
void DawnCacheOperation::OnOpenedEntry(disk_cache::EntryResult entry) {
// Check if we are pending since this function may be scheduled twice if the
// return value was asynchronous.
if (entry.net_error() == net::ERR_IO_PENDING) {
return;
}
// Backend cache implementation uses integers as both a status and a return
// value. Non-negative values represent successful operation and doubles as
// the size read/written depending on the operation.
int status = 0;
entry_.reset(entry.ReleaseEntry());
if (entry_ == nullptr) {
OnOpCompleteStatus(status);
return;
}
switch (type_) {
case Type::kRead: {
if (buffer_size_ == 0) {
// Size-read case.
OnOpCompleteSize(GetEntrySize());
return;
} else {
if (buffer_size_ != GetEntrySize()) {
// If we are reading the actual data, the buffer size must equal the
// entry size, otherwise we would not be able to copy the data out
// anyways so we can just skip and return 0 to indicate that the read
// did no occur.
OnOpCompleteSize(0u);
return;
}
buffer_ = base::WrapRefCounted(new net::IOBuffer(buffer_size_));
status = entry_->ReadData(
0, 0, buffer_.get(), buffer_size_,
base::BindOnce(&DawnCacheOperation::OnOpCompleteStatus, this));
}
break;
}
case Type::kWrite: {
status = entry_->WriteData(
0, 0, buffer_.get(), buffer_size_,
base::BindOnce(&DawnCacheOperation::OnOpCompleteStatus, this), false);
break;
}
}
// Both ReadData and StoreData will call OnOpCompleteStatus as a callback if
// the returned 'status' is ERR_IO_PENDING. Otherwise, the calls will not call
// their callback so we need to call it here, passing 'status' as the
// argument.
if (status != net::ERR_IO_PENDING) {
OnOpCompleteStatus(status);
}
}
void DawnCacheOperation::OnOpCompleteSize(size_t size) {
result_size_ = size;
switch (type_) {
case Type::kRead: {
// No-op since we already set the result.
break;
}
case Type::kWrite: {
// If the write wasn't complete (didn't write the full buffer out), we
// mark the entry as doomed for deletion. In general, this shouldn't
// happen, but may occur in shutdown or error scenarios.
if (entry_ != nullptr && result_size_ != buffer_size_) {
entry_->Doom();
}
break;
}
}
signal_.Signal();
}
void DawnCacheOperation::OnOpCompleteStatus(int status) {
// Any errors are negative, and we mask the status here to 0 size to indicate
// read/write failed.
OnOpCompleteSize(status > 0 ? size_t(status) : 0u);
}
DawnCachingInterface::DawnCachingInterface(net::CacheType cache_type,
int64_t cache_size,
const base::FilePath& path)
: DawnCachingInterface(
base::BindOnce(&DawnCachingInterface::DefaultCacheBackendFactory,
cache_type,
cache_size,
std::move(path))) {}
DawnCachingInterface::DawnCachingInterface(CacheBackendFactory factory)
: factory_(std::move(factory)),
backend_thread_(base::ThreadPool::CreateSingleThreadTaskRunner(
{base::MayBlock()},
base::SingleThreadTaskRunnerThreadMode::DEDICATED)),
backend_(nullptr, base::OnTaskRunnerDeleter(backend_thread_)) {}
net::Error DawnCachingInterface::Init() {
// Initialization blocks until either a valid backend is returned or an error
// occurs.
std::unique_ptr<disk_cache::Backend> backend;
base::WaitableEvent signal;
net::Error error;
backend_thread_->PostTask(
FROM_HERE,
base::BindOnce(std::move(factory_), base::Unretained(&backend),
base::Unretained(&signal), base::Unretained(&error)));
signal.Wait();
backend_.reset(backend.release());
return error;
}
// static
std::unique_ptr<DawnCachingInterface> DawnCachingInterface::CreateForTesting(
CacheBackendFactory factory) {
return std::unique_ptr<DawnCachingInterface>(
new DawnCachingInterface(std::move(factory)));
}
DawnCachingInterface::~DawnCachingInterface() = default;
size_t DawnCachingInterface::LoadData(const void* key,
size_t key_size,
void* value_out,
size_t value_size) {
if (backend_ == nullptr) {
return 0u;
}
std::string key_str(static_cast<const char*>(key), key_size);
// Check if the call is a size/existence check.
bool size_only = value_size == 0 && value_out == nullptr;
auto op = base::WrapRefCounted(new DawnCacheOperation(
backend_thread_, DawnCacheOperation::Type::kRead, value_size));
backend_thread_->PostTask(
FROM_HERE,
base::BindOnce(&DawnCacheOperation::Run, op, std::move(key_str),
base::Unretained(backend_.get())));
// For loading data, to appear synchronous, we block, but for a max timeout.
if (!op->TimedWait(kCacheOpTimeout)) {
return 0u;
}
// Copy the read memory into the actual destination buffer if we did not time
// out reading the entry.
if (!size_only) {
std::memcpy(value_out, op->Data(), value_size);
}
return op->Result();
}
void DawnCachingInterface::StoreData(const void* key,
size_t key_size,
const void* value,
size_t value_size) {
if (backend_ == nullptr || value == nullptr || value_size <= 0) {
return;
}
std::string key_str(static_cast<const char*>(key), key_size);
auto op = base::WrapRefCounted(new DawnCacheOperation(
backend_thread_, DawnCacheOperation::Type::kWrite, value_size));
// Copy the contents of buffer into the internal buffer that will live for the
// duration of the async operation.
std::memcpy(op->Data(), value, value_size);
backend_thread_->PostTask(
FROM_HERE,
base::BindOnce(&DawnCacheOperation::Run, op, std::move(key_str),
base::Unretained(backend_.get())));
}
namespace {
void BackendInitCallback(base::WaitableEvent* signal,
net::Error* error,
std::unique_ptr<disk_cache::Backend>* backend,
disk_cache::BackendResult result) {
*error = result.net_error;
*backend = std::move(result.backend);
signal->Signal();
}
} // namespace
// static
void DawnCachingInterface::DefaultCacheBackendFactory(
net::CacheType cache_type,
int64_t cache_size,
const base::FilePath& path,
std::unique_ptr<disk_cache::Backend>* backend,
base::WaitableEvent* signal,
net::Error* error) {
disk_cache::BackendResult result = disk_cache::CreateCacheBackend(
cache_type, net::CACHE_BACKEND_BLOCKFILE,
/*file_operations=*/nullptr, path,
/*max_bytes=*/cache_size, disk_cache::ResetHandling::kNeverReset,
/*net_log=*/nullptr,
base::BindOnce(&BackendInitCallback, base::Unretained(signal),
base::Unretained(error), base::Unretained(backend)));
*error = result.net_error;
if (*error == net::ERR_IO_PENDING) {
return;
}
*backend = std::move(result.backend);
signal->Signal();
}
} // namespace gpu::webgpu