blob: 733bf6746453456fb23d1e9ebd7a3c8e6521658f [file] [log] [blame]
// Copyright 2018 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 "content/browser/code_cache/generated_code_cache.h"
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "content/public/common/url_constants.h"
#include "net/base/completion_callback.h"
#include "net/base/completion_once_callback.h"
#include "net/base/url_util.h"
#include "url/gurl.h"
namespace content {
namespace {
constexpr char kPrefix[] = "_key";
constexpr char kSeparator[] = " \n";
// We always expect to receive valid URLs that can be used as keys to the code
// cache. The relevant checks (for ex: resource_url is valid, origin_lock is
// not opque etc.,) must be done prior to requesting the code cache.
//
// This function doesn't enforce anything in the production code. It is here
// to make the assumptions explicit and to catch any errors when DCHECKs are
// enabled.
void CheckValidKeys(const GURL& resource_url, const GURL& origin_lock) {
// If the resource url is invalid don't cache the code.
DCHECK(resource_url.is_valid() && resource_url.SchemeIsHTTPOrHTTPS());
// |origin_lock| should be either empty or should have Http/Https/chrome
// schemes and it should not be a URL with opaque origin. Empty origin_locks
// are allowed when the renderer is not locked to an origin.
DCHECK(origin_lock.is_empty() ||
((origin_lock.SchemeIsHTTPOrHTTPS() ||
origin_lock.SchemeIs(content::kChromeUIScheme)) &&
!url::Origin::Create(origin_lock).opaque()));
}
// Generates the cache key for the given |resource_url| and the |origin_lock|.
// |resource_url| is the url corresponding to the requested resource.
// |origin_lock| is the origin that the renderer which requested this
// resource is locked to.
// For example, if SitePerProcess is enabled and http://script.com/script1.js is
// requested by http://example.com, then http://script.com/script.js is the
// resource_url and http://example.com is the origin_lock.
//
// This returns the key by concatenating the serialized url and origin lock
// with a separator in between. |origin_lock| could be empty when renderer is
// not locked to an origin (ex: SitePerProcess is disabled) and it is safe to
// use only |resource_url| as the key in such cases.
std::string GetCacheKey(const GURL& resource_url, const GURL& origin_lock) {
CheckValidKeys(resource_url, origin_lock);
// Add a prefix _ so it can't be parsed as a valid URL.
std::string key(kPrefix);
// Remove reference, username and password sections of the URL.
key.append(net::SimplifyUrlForRequest(resource_url).spec());
// Add a separator between URL and origin to avoid any possibility of
// attacks by crafting the URL. URLs do not contain any control ASCII
// characters, and also space is encoded. So use ' \n' as a seperator.
key.append(kSeparator);
if (origin_lock.is_valid())
key.append(net::SimplifyUrlForRequest(origin_lock).spec());
return key;
}
} // namespace
std::string GeneratedCodeCache::GetResourceURLFromKey(const std::string& key) {
constexpr size_t kPrefixStringLen = base::size(kPrefix) - 1;
// Only expect valid keys. All valid keys have a prefix and a separator.
DCHECK_GE(key.length(), kPrefixStringLen);
DCHECK_NE(key.find(kSeparator), std::string::npos);
std::string resource_url =
key.substr(kPrefixStringLen, key.find(kSeparator) - kPrefixStringLen);
return resource_url;
}
void GeneratedCodeCache::CollectStatistics(
GeneratedCodeCache::CacheEntryStatus status) {
switch (cache_type_) {
case GeneratedCodeCache::CodeCacheType::kJavaScript:
UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.JS.Behaviour", status);
break;
case GeneratedCodeCache::CodeCacheType::kWebAssembly:
UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.WASM.Behaviour", status);
break;
}
}
// Stores the information about a pending request while disk backend is
// being initialized.
class GeneratedCodeCache::PendingOperation {
public:
static std::unique_ptr<PendingOperation> CreateWritePendingOp(
std::string key,
scoped_refptr<net::IOBufferWithSize>);
static std::unique_ptr<PendingOperation> CreateFetchPendingOp(
std::string key,
const ReadDataCallback&);
static std::unique_ptr<PendingOperation> CreateDeletePendingOp(
std::string key);
static std::unique_ptr<PendingOperation> CreateGetBackendPendingOp(
GetBackendCallback callback);
~PendingOperation();
Operation operation() const { return op_; }
const std::string& key() const { return key_; }
const scoped_refptr<net::IOBufferWithSize> data() const { return data_; }
ReadDataCallback ReleaseReadCallback() { return std::move(read_callback_); }
GetBackendCallback ReleaseCallback() { return std::move(callback_); }
private:
PendingOperation(Operation op,
std::string key,
scoped_refptr<net::IOBufferWithSize>,
const ReadDataCallback&,
GetBackendCallback);
const Operation op_;
const std::string key_;
const scoped_refptr<net::IOBufferWithSize> data_;
ReadDataCallback read_callback_;
GetBackendCallback callback_;
};
std::unique_ptr<GeneratedCodeCache::PendingOperation>
GeneratedCodeCache::PendingOperation::CreateWritePendingOp(
std::string key,
scoped_refptr<net::IOBufferWithSize> buffer) {
return base::WrapUnique(
new PendingOperation(Operation::kWrite, std::move(key), buffer,
ReadDataCallback(), GetBackendCallback()));
}
std::unique_ptr<GeneratedCodeCache::PendingOperation>
GeneratedCodeCache::PendingOperation::CreateFetchPendingOp(
std::string key,
const ReadDataCallback& read_callback) {
return base::WrapUnique(new PendingOperation(
Operation::kFetch, std::move(key), scoped_refptr<net::IOBufferWithSize>(),
read_callback, GetBackendCallback()));
}
std::unique_ptr<GeneratedCodeCache::PendingOperation>
GeneratedCodeCache::PendingOperation::CreateDeletePendingOp(std::string key) {
return base::WrapUnique(
new PendingOperation(Operation::kDelete, std::move(key),
scoped_refptr<net::IOBufferWithSize>(),
ReadDataCallback(), GetBackendCallback()));
}
std::unique_ptr<GeneratedCodeCache::PendingOperation>
GeneratedCodeCache::PendingOperation::CreateGetBackendPendingOp(
GetBackendCallback callback) {
return base::WrapUnique(
new PendingOperation(Operation::kGetBackend, std::string(),
scoped_refptr<net::IOBufferWithSize>(),
ReadDataCallback(), std::move(callback)));
}
GeneratedCodeCache::PendingOperation::PendingOperation(
Operation op,
std::string key,
scoped_refptr<net::IOBufferWithSize> buffer,
const ReadDataCallback& read_callback,
GetBackendCallback callback)
: op_(op),
key_(std::move(key)),
data_(buffer),
read_callback_(read_callback),
callback_(std::move(callback)) {}
GeneratedCodeCache::PendingOperation::~PendingOperation() = default;
GeneratedCodeCache::GeneratedCodeCache(const base::FilePath& path,
int max_size_bytes,
CodeCacheType cache_type)
: backend_state_(kInitializing),
path_(path),
max_size_bytes_(max_size_bytes),
cache_type_(cache_type),
weak_ptr_factory_(this) {
CreateBackend();
}
GeneratedCodeCache::~GeneratedCodeCache() = default;
void GeneratedCodeCache::GetBackend(GetBackendCallback callback) {
switch (backend_state_) {
case kFailed:
std::move(callback).Run(nullptr);
return;
case kInitialized:
std::move(callback).Run(backend_.get());
return;
case kInitializing:
pending_ops_.push_back(
GeneratedCodeCache::PendingOperation::CreateGetBackendPendingOp(
std::move(callback)));
return;
}
}
void GeneratedCodeCache::WriteData(const GURL& url,
const GURL& origin_lock,
const base::Time& response_time,
const std::vector<uint8_t>& data) {
// Silently ignore the requests.
if (backend_state_ == kFailed) {
CollectStatistics(CacheEntryStatus::kError);
return;
}
// Append the response time to the metadata. Code caches store
// response_time + generated code as a single entry.
scoped_refptr<net::IOBufferWithSize> buffer =
base::MakeRefCounted<net::IOBufferWithSize>(data.size() +
kResponseTimeSizeInBytes);
int64_t serialized_time =
response_time.ToDeltaSinceWindowsEpoch().InMicroseconds();
memcpy(buffer->data(), &serialized_time, kResponseTimeSizeInBytes);
if (!data.empty())
memcpy(buffer->data() + kResponseTimeSizeInBytes, &data.front(),
data.size());
std::string key = GetCacheKey(url, origin_lock);
// If there is an in progress operation corresponding to this key. Enqueue it
// so we can issue once the in-progress operation finishes.
if (EnqueueAsPendingOperation(
key, GeneratedCodeCache::PendingOperation::CreateWritePendingOp(
key, buffer))) {
return;
}
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.push_back(
GeneratedCodeCache::PendingOperation::CreateWritePendingOp(
std::move(key), buffer));
return;
}
WriteDataImpl(key, buffer);
}
void GeneratedCodeCache::FetchEntry(const GURL& url,
const GURL& origin_lock,
ReadDataCallback read_data_callback) {
if (backend_state_ == kFailed) {
CollectStatistics(CacheEntryStatus::kError);
// Silently ignore the requests.
std::move(read_data_callback).Run(base::Time(), std::vector<uint8_t>());
return;
}
std::string key = GetCacheKey(url, origin_lock);
// If there is an in progress operation corresponding to this key. Enqueue it
// so we can issue once the in-progress operation finishes.
if (EnqueueAsPendingOperation(
key, GeneratedCodeCache::PendingOperation::CreateFetchPendingOp(
key, read_data_callback))) {
return;
}
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.push_back(
GeneratedCodeCache::PendingOperation::CreateFetchPendingOp(
std::move(key), read_data_callback));
return;
}
FetchEntryImpl(key, read_data_callback);
}
void GeneratedCodeCache::DeleteEntry(const GURL& url, const GURL& origin_lock) {
// Silently ignore the requests.
if (backend_state_ == kFailed) {
CollectStatistics(CacheEntryStatus::kError);
return;
}
std::string key = GetCacheKey(url, origin_lock);
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.push_back(
GeneratedCodeCache::PendingOperation::CreateDeletePendingOp(
std::move(key)));
return;
}
DeleteEntryImpl(key);
}
void GeneratedCodeCache::CreateBackend() {
// Create a new Backend pointer that cleans itself if the GeneratedCodeCache
// instance is not live when the CreateCacheBackend finishes.
scoped_refptr<base::RefCountedData<ScopedBackendPtr>> shared_backend_ptr =
new base::RefCountedData<ScopedBackendPtr>();
net::CompletionOnceCallback create_backend_complete =
base::BindOnce(&GeneratedCodeCache::DidCreateBackend,
weak_ptr_factory_.GetWeakPtr(), shared_backend_ptr);
// If the initialization of the existing cache fails, this call would delete
// all the contents and recreates a new one.
int rv = disk_cache::CreateCacheBackend(
net::GENERATED_CODE_CACHE, net::CACHE_BACKEND_SIMPLE, path_,
max_size_bytes_, true, nullptr, &shared_backend_ptr->data,
std::move(create_backend_complete));
if (rv != net::ERR_IO_PENDING) {
DidCreateBackend(shared_backend_ptr, rv);
}
}
void GeneratedCodeCache::DidCreateBackend(
scoped_refptr<base::RefCountedData<ScopedBackendPtr>> backend_ptr,
int rv) {
if (rv != net::OK) {
backend_state_ = kFailed;
// Process pending operations to process any required callbacks.
IssuePendingOperations();
return;
}
backend_ = std::move(backend_ptr->data);
backend_state_ = kInitialized;
IssuePendingOperations();
}
void GeneratedCodeCache::IssuePendingOperations() {
DCHECK_EQ(backend_state_, kInitialized);
// Issue all the pending operations that were received when creating
// the backend.
for (auto const& op : pending_ops_) {
IssueOperation(op.get());
}
pending_ops_.clear();
}
void GeneratedCodeCache::IssueOperation(PendingOperation* op) {
switch (op->operation()) {
case kFetch:
FetchEntryImpl(op->key(), op->ReleaseReadCallback());
break;
case kWrite:
WriteDataImpl(op->key(), op->data());
break;
case kDelete:
DeleteEntryImpl(op->key());
break;
case kGetBackend:
DoPendingGetBackend(op->ReleaseCallback());
break;
}
}
void GeneratedCodeCache::WriteDataImpl(
const std::string& key,
scoped_refptr<net::IOBufferWithSize> buffer) {
if (backend_state_ != kInitialized) {
IssueQueuedOperationForEntry(key);
return;
}
scoped_refptr<base::RefCountedData<disk_cache::EntryWithOpened>>
entry_struct = new base::RefCountedData<disk_cache::EntryWithOpened>();
net::CompletionOnceCallback callback =
base::BindOnce(&GeneratedCodeCache::CompleteForWriteData,
weak_ptr_factory_.GetWeakPtr(), buffer, key, entry_struct);
int result = backend_->OpenOrCreateEntry(key, net::LOW, &entry_struct->data,
std::move(callback));
if (result != net::ERR_IO_PENDING) {
CompleteForWriteData(buffer, key, entry_struct, result);
}
}
void GeneratedCodeCache::CompleteForWriteData(
scoped_refptr<net::IOBufferWithSize> buffer,
const std::string& key,
scoped_refptr<base::RefCountedData<disk_cache::EntryWithOpened>>
entry_struct,
int rv) {
if (rv != net::OK) {
CollectStatistics(CacheEntryStatus::kError);
IssueQueuedOperationForEntry(key);
return;
}
DCHECK(entry_struct->data.entry);
int result = net::ERR_FAILED;
{
disk_cache::ScopedEntryPtr disk_entry(entry_struct->data.entry);
if (entry_struct->data.opened) {
CollectStatistics(CacheEntryStatus::kUpdate);
} else {
CollectStatistics(CacheEntryStatus::kCreate);
}
// This call will truncate the data. This is safe to do since we read the
// entire data at the same time currently. If we want to read in parts we
// have to doom the entry first.
result = disk_entry->WriteData(
kDataIndex, 0, buffer.get(), buffer->size(),
base::BindOnce(&GeneratedCodeCache::WriteDataCompleted,
weak_ptr_factory_.GetWeakPtr(), key),
true);
}
if (result != net::ERR_IO_PENDING) {
WriteDataCompleted(key, result);
}
}
void GeneratedCodeCache::WriteDataCompleted(const std::string& key, int rv) {
if (rv < 0) {
CollectStatistics(CacheEntryStatus::kWriteFailed);
// The write failed; we should delete the entry.
DeleteEntryImpl(key);
}
IssueQueuedOperationForEntry(key);
}
void GeneratedCodeCache::FetchEntryImpl(const std::string& key,
ReadDataCallback read_data_callback) {
if (backend_state_ != kInitialized) {
std::move(read_data_callback).Run(base::Time(), std::vector<uint8_t>());
IssueQueuedOperationForEntry(key);
return;
}
scoped_refptr<base::RefCountedData<disk_cache::Entry*>> entry_ptr =
new base::RefCountedData<disk_cache::Entry*>();
net::CompletionOnceCallback callback = base::BindOnce(
&GeneratedCodeCache::OpenCompleteForReadData,
weak_ptr_factory_.GetWeakPtr(), read_data_callback, key, entry_ptr);
// This is a part of loading cycle and hence should run with a high priority.
int result = backend_->OpenEntry(key, net::HIGHEST, &entry_ptr->data,
std::move(callback));
if (result != net::ERR_IO_PENDING) {
OpenCompleteForReadData(read_data_callback, key, entry_ptr, result);
}
}
void GeneratedCodeCache::OpenCompleteForReadData(
ReadDataCallback read_data_callback,
const std::string& key,
scoped_refptr<base::RefCountedData<disk_cache::Entry*>> entry,
int rv) {
if (rv != net::OK) {
CollectStatistics(CacheEntryStatus::kMiss);
std::move(read_data_callback).Run(base::Time(), std::vector<uint8_t>());
IssueQueuedOperationForEntry(key);
return;
}
// There should be a valid entry if the open was successful.
DCHECK(entry->data);
int result = net::ERR_FAILED;
scoped_refptr<net::IOBufferWithSize> buffer;
{
disk_cache::ScopedEntryPtr disk_entry(entry->data);
int size = disk_entry->GetDataSize(kDataIndex);
buffer = base::MakeRefCounted<net::IOBufferWithSize>(size);
net::CompletionOnceCallback callback = base::BindOnce(
&GeneratedCodeCache::ReadDataComplete, weak_ptr_factory_.GetWeakPtr(),
key, read_data_callback, buffer);
result = disk_entry->ReadData(kDataIndex, 0, buffer.get(), size,
std::move(callback));
}
if (result != net::ERR_IO_PENDING) {
ReadDataComplete(key, read_data_callback, buffer, result);
}
}
void GeneratedCodeCache::ReadDataComplete(
const std::string& key,
ReadDataCallback callback,
scoped_refptr<net::IOBufferWithSize> buffer,
int rv) {
if (rv != buffer->size()) {
CollectStatistics(CacheEntryStatus::kMiss);
std::move(callback).Run(base::Time(), std::vector<uint8_t>());
} else {
// DiskCache ensures that the operations that are queued for an entry
// go in order. Hence, we would either read an empty data or read the full
// data. Note that if WriteData failed, buffer->size() is 0 so we handle
// that case here.
CollectStatistics(CacheEntryStatus::kHit);
int64_t raw_response_time = 0;
std::vector<uint8_t> data;
if (buffer->size() >= kResponseTimeSizeInBytes) {
raw_response_time = *(reinterpret_cast<int64_t*>(buffer->data()));
data = std::vector<uint8_t>(buffer->data() + kResponseTimeSizeInBytes,
buffer->data() + buffer->size());
}
base::Time response_time = base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(raw_response_time));
std::move(callback).Run(response_time, data);
}
IssueQueuedOperationForEntry(key);
}
void GeneratedCodeCache::DeleteEntryImpl(const std::string& key) {
if (backend_state_ != kInitialized)
return;
CollectStatistics(CacheEntryStatus::kClear);
backend_->DoomEntry(key, net::LOWEST, net::CompletionOnceCallback());
}
void GeneratedCodeCache::IssueQueuedOperationForEntry(const std::string& key) {
auto it = active_entries_map_.find(key);
DCHECK(it != active_entries_map_.end());
// If no more queued entries then remove the entry to indicate that there are
// no in-progress operations for this key.
if (it->second.empty()) {
active_entries_map_.erase(it);
return;
}
std::unique_ptr<PendingOperation> op = std::move(it->second.front());
// Pop it before issuing the operation. Still retain the queue even if it is
// empty to indicate that there is a in-progress operation.
it->second.pop();
IssueOperation(op.get());
}
bool GeneratedCodeCache::EnqueueAsPendingOperation(
const std::string& key,
std::unique_ptr<PendingOperation> op) {
auto it = active_entries_map_.find(key);
if (it != active_entries_map_.end()) {
it->second.emplace(std::move(op));
return true;
}
// Create a entry to indicate there is a in-progress operation for this key.
active_entries_map_[key] = base::queue<std::unique_ptr<PendingOperation>>();
return false;
}
void GeneratedCodeCache::DoPendingGetBackend(GetBackendCallback user_callback) {
if (backend_state_ == kInitialized) {
std::move(user_callback).Run(backend_.get());
return;
}
DCHECK_EQ(backend_state_, kFailed);
std::move(user_callback).Run(nullptr);
return;
}
void GeneratedCodeCache::SetLastUsedTimeForTest(
const GURL& resource_url,
const GURL& origin_lock,
base::Time time,
base::RepeatingCallback<void(void)> user_callback) {
// This is used only for tests. So reasonable to assume that backend is
// initialized here. All other operations handle the case when backend was not
// yet opened.
DCHECK_EQ(backend_state_, kInitialized);
scoped_refptr<base::RefCountedData<disk_cache::Entry*>> entry_ptr =
new base::RefCountedData<disk_cache::Entry*>();
net::CompletionOnceCallback callback = base::BindOnce(
&GeneratedCodeCache::OpenCompleteForSetLastUsedForTest,
weak_ptr_factory_.GetWeakPtr(), entry_ptr, time, user_callback);
std::string key = GetCacheKey(resource_url, origin_lock);
int result = backend_->OpenEntry(key, net::LOWEST, &entry_ptr->data,
std::move(callback));
if (result != net::ERR_IO_PENDING) {
OpenCompleteForSetLastUsedForTest(entry_ptr, time, user_callback, result);
}
}
void GeneratedCodeCache::OpenCompleteForSetLastUsedForTest(
scoped_refptr<base::RefCountedData<disk_cache::Entry*>> entry,
base::Time time,
base::RepeatingCallback<void(void)> callback,
int rv) {
DCHECK_EQ(rv, net::OK);
DCHECK(entry->data);
{
disk_cache::ScopedEntryPtr disk_entry(entry->data);
disk_entry->SetLastUsedTimeForTest(time);
}
std::move(callback).Run();
}
} // namespace content