Commit 95d3767a authored by Bill Budge's avatar Bill Budge Committed by Commit Bot

[code cache] De-duplicate large double-keyed cache entries

- Changes very large code entry writing to create an indirection. The
  first entry is double-keyed just like other size entries. However, it
  contains response time, size, and a hashed checksum of the data. The
  checksum is used as a second key for another entry that contains a
  no header and the actual code.
- Changes very large code entry fetches to do a double-keyed read as
  before. If there is a checksum in the header, that is used as a
  second key to read the actual code.
- Every origin that loads a given resource for the first time will
  get a cache miss. To get a cache hit, the origin must first generate
  code and write it to the cache. The code is checksummed using SHA-256
  to make it very unlikely for a malicious origin to store its code to
  the same key as another origin. Origins that generate identical code
  share the same code entry.

Design Document:
https://docs.google.com/document/d/1SCH15oCFJW55jsTJZ0T7XAQIZVryI_IJOIENyL_q4n4/edit

Bug:chromium:936107

Change-Id: If99d2dada102382d51f97286fb3e5d9d2faf3aa6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1846102
Commit-Queue: Bill Budge <bbudge@chromium.org>
Reviewed-by: default avatarMythri Alle <mythria@chromium.org>
Reviewed-by: default avatarChris Palmer <palmer@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarMaksim Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/master@{#707389}
parent 804eeab2
......@@ -6,7 +6,9 @@
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "content/public/common/url_constants.h"
#include "crypto/sha2.h"
#include "net/base/completion_once_callback.h"
#include "net/base/url_util.h"
#include "url/gurl.h"
......@@ -67,20 +69,51 @@ std::string GetCacheKey(const GURL& resource_url, const GURL& origin_lock) {
return key;
}
constexpr int kResponseTimeSizeInBytes = sizeof(int64_t);
constexpr int kDataSizeInBytes = sizeof(uint32_t);
constexpr int kHeaderSizeInBytes = kResponseTimeSizeInBytes + kDataSizeInBytes;
constexpr size_t kResponseTimeSizeInBytes = sizeof(int64_t);
constexpr size_t kDataSizeInBytes = sizeof(uint32_t);
constexpr size_t kHeaderSizeInBytes =
kResponseTimeSizeInBytes + kDataSizeInBytes;
// The SHA-256 checksum is used as the key for the de-duplicated code data. We
// must convert the checksum to a string key in a way that is guaranteed not to
// match a key generated by |GetCacheKey|. A simple way to do this is to convert
// it to a hex number string, which is twice as long as the checksum.
constexpr size_t kSHAKeySizeInBytes = 2 * crypto::kSHA256Length;
// This is the threshold for storing the header and cached code in stream 0,
// which is read into memory on opening an entry. JavaScript code caching stores
// time stamps with no data, or timestamps with just a tag, and we observe many
// 8 and 16 byte reads and writes. Make the threshold larger to speed up many
// 8 and 16 byte reads and writes. Make the threshold larger to speed up small
// code entries too.
constexpr int kSmallDataLimit = 4096;
constexpr size_t kSmallDataLimit = 4096;
// This is the maximum size for code that will be stored under the key generated
// by |GetCacheKey|. Each origin will get its own copy of the generated code for
// a given resource. Code that is larger than this limit will be stored under a
// key derived from the code checksum, and each origin using a given resource
// gets its own small entry under the key generated by |GetCacheKey| that holds
// the hash, enabling a two stage lookup.
constexpr size_t kLargeDataLimit = 64 * 1024;
// Checks that the header data in the small buffer is valid. We may read cache
// entries that were written by a previous version of Chrome which use obsolete
// formats. These reads should fail and be doomed as soon as possible.
bool IsValidHeader(scoped_refptr<net::IOBufferWithSize> small_buffer) {
size_t buffer_size = small_buffer->size();
if (buffer_size < kHeaderSizeInBytes)
return false;
uint32_t data_size;
memcpy(&data_size, small_buffer->data() + kResponseTimeSizeInBytes,
kDataSizeInBytes);
if (data_size <= kSmallDataLimit)
return buffer_size == kHeaderSizeInBytes + data_size;
if (data_size <= kLargeDataLimit)
return buffer_size == kHeaderSizeInBytes;
return buffer_size == kHeaderSizeInBytes + kSHAKeySizeInBytes;
}
void WriteSmallDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
const base::Time& response_time,
uint32_t data_size) {
DCHECK_LE(kHeaderSizeInBytes, buffer->size());
void WriteCommonDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
const base::Time& response_time,
uint32_t data_size) {
DCHECK_LE(static_cast<int>(kHeaderSizeInBytes), buffer->size());
int64_t serialized_time =
response_time.ToDeltaSinceWindowsEpoch().InMicroseconds();
memcpy(buffer->data(), &serialized_time, kResponseTimeSizeInBytes);
......@@ -89,15 +122,16 @@ void WriteSmallDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
kDataSizeInBytes);
}
void ReadSmallDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
base::Time* response_time,
uint32_t* data_size) {
DCHECK_LE(kHeaderSizeInBytes, buffer->size());
int64_t raw_response_time = *(reinterpret_cast<int64_t*>(buffer->data()));
void ReadCommonDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
base::Time* response_time,
uint32_t* data_size) {
DCHECK_LE(static_cast<int>(kHeaderSizeInBytes), buffer->size());
int64_t raw_response_time;
memcpy(&raw_response_time, buffer->data(), kResponseTimeSizeInBytes);
*response_time = base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(raw_response_time));
*data_size =
*(reinterpret_cast<uint32_t*>(buffer->data() + kResponseTimeSizeInBytes));
memcpy(data_size, buffer->data() + kResponseTimeSizeInBytes,
kDataSizeInBytes);
}
static_assert(mojo_base::BigBuffer::kMaxInlineBytes <=
......@@ -171,7 +205,7 @@ class GeneratedCodeCache::PendingOperation {
key_(key),
small_buffer_(small_buffer),
large_buffer_(large_buffer) {
DCHECK_EQ(Operation::kWrite, op_);
DCHECK(Operation::kWrite == op_ || Operation::kWriteWithSHAKey == op_);
}
PendingOperation(Operation op,
......@@ -181,6 +215,21 @@ class GeneratedCodeCache::PendingOperation {
DCHECK_EQ(Operation::kFetch, op_);
}
PendingOperation(Operation op,
const std::string& key,
const base::Time& response_time,
scoped_refptr<net::IOBufferWithSize> small_buffer,
scoped_refptr<BigIOBuffer> large_buffer,
ReadDataCallback read_callback)
: op_(op),
key_(key),
response_time_(response_time),
small_buffer_(small_buffer),
large_buffer_(large_buffer),
read_callback_(std::move(read_callback)) {
DCHECK_EQ(Operation::kFetchWithSHAKey, op_);
}
PendingOperation(Operation op, const std::string& key) : op_(op), key_(key) {
DCHECK_EQ(Operation::kDelete, op_);
}
......@@ -212,12 +261,19 @@ class GeneratedCodeCache::PendingOperation {
large_buffer_ = large_buffer;
}
// This returns the site-specific response time for merged code entries.
const base::Time& response_time() const {
DCHECK_EQ(Operation::kFetchWithSHAKey, op_);
return response_time_;
}
// These are called by write and fetch operations to track buffer completions
// and signal when the operation has finished, and whether it was successful.
bool succeeded() const { return succeeded_; }
bool AddBufferCompletion(bool succeeded) {
DCHECK(op_ == Operation::kWrite || op_ == Operation::kFetch);
DCHECK(op_ == Operation::kWrite || op_ == Operation::kWriteWithSHAKey ||
op_ == Operation::kFetch || op_ == Operation::kFetchWithSHAKey);
if (!succeeded)
succeeded_ = false;
DCHECK_GT(2, completions_);
......@@ -228,6 +284,7 @@ class GeneratedCodeCache::PendingOperation {
private:
const Operation op_;
const std::string key_;
const base::Time response_time_;
scoped_refptr<net::IOBufferWithSize> small_buffer_;
scoped_refptr<BigIOBuffer> large_buffer_;
ReadDataCallback read_callback_;
......@@ -275,37 +332,67 @@ void GeneratedCodeCache::WriteEntry(const GURL& url,
return;
}
// If data is small, combine the header and data into a single write.
// Reject buffers that are large enough to cause overflow problems.
if (data.size() >= std::numeric_limits<int32_t>::max())
return;
scoped_refptr<net::IOBufferWithSize> small_buffer;
scoped_refptr<BigIOBuffer> large_buffer;
uint32_t data_size = static_cast<uint32_t>(data.size());
// We have three different cache entry layouts, depending on data size.
if (data_size <= kSmallDataLimit) {
// 1. Small
// [stream0] response time, size, data
// [stream1] <empty>
small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(
kHeaderSizeInBytes + data.size());
// Copy |data| into the small buffer.
memcpy(small_buffer->data() + kHeaderSizeInBytes, data.data(), data.size());
// We write 0 bytes and truncate stream 1 to clear any stale data.
// Write 0 bytes and truncate stream 1 to clear any stale data.
large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer());
} else {
} else if (data_size <= kLargeDataLimit) {
// 2. Large
// [stream0] response time, size
// [stream1] data
small_buffer =
base::MakeRefCounted<net::IOBufferWithSize>(kHeaderSizeInBytes);
large_buffer = base::MakeRefCounted<BigIOBuffer>(std::move(data));
} else {
// 3. Very Large
// [stream0] response time, size, checksum
// [stream1] <empty>
// [stream0 (checksum key entry)] <empty>
// [stream1 (checksum key entry)] data
uint8_t result[crypto::kSHA256Length];
crypto::SHA256HashString(
base::StringPiece(reinterpret_cast<char*>(data.data()), data.size()),
result, base::size(result));
std::string checksum_key = base::HexEncode(result, base::size(result));
small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(
kHeaderSizeInBytes + kSHAKeySizeInBytes);
// Copy |checksum_key| into the small buffer.
DCHECK_EQ(kSHAKeySizeInBytes, checksum_key.length());
memcpy(small_buffer->data() + kHeaderSizeInBytes, checksum_key.data(),
kSHAKeySizeInBytes);
// Write 0 bytes and truncate stream 1 to clear any stale data.
large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer());
// Issue another write operation for the code, with the checksum as the key
// and nothing in the header.
auto small_buffer2 = base::MakeRefCounted<net::IOBufferWithSize>(0);
auto large_buffer2 = base::MakeRefCounted<BigIOBuffer>(std::move(data));
auto op2 = std::make_unique<PendingOperation>(Operation::kWriteWithSHAKey,
checksum_key, small_buffer2,
large_buffer2);
EnqueueOperation(std::move(op2));
}
WriteSmallDataHeader(small_buffer, response_time, data_size);
WriteCommonDataHeader(small_buffer, response_time, data_size);
// Create the write operation.
std::string key = GetCacheKey(url, origin_lock);
auto op = std::make_unique<PendingOperation>(Operation::kWrite, key,
small_buffer, large_buffer);
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.emplace(std::move(op));
return;
}
EnqueueOperationAndIssueIfNext(std::move(op));
EnqueueOperation(std::move(op));
}
void GeneratedCodeCache::FetchEntry(const GURL& url,
......@@ -321,14 +408,7 @@ void GeneratedCodeCache::FetchEntry(const GURL& url,
std::string key = GetCacheKey(url, origin_lock);
auto op = std::make_unique<PendingOperation>(Operation::kFetch, key,
std::move(read_data_callback));
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.emplace(std::move(op));
return;
}
EnqueueOperationAndIssueIfNext(std::move(op));
EnqueueOperation(std::move(op));
}
void GeneratedCodeCache::DeleteEntry(const GURL& url, const GURL& origin_lock) {
......@@ -340,15 +420,7 @@ void GeneratedCodeCache::DeleteEntry(const GURL& url, const GURL& origin_lock) {
std::string key = GetCacheKey(url, origin_lock);
auto op = std::make_unique<PendingOperation>(Operation::kDelete, key);
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.emplace(std::move(op));
return;
}
EnqueueOperationAndIssueIfNext(std::move(op));
EnqueueOperation(std::move(op));
}
void GeneratedCodeCache::CreateBackend() {
......@@ -386,6 +458,18 @@ void GeneratedCodeCache::DidCreateBackend(
IssuePendingOperations();
}
void GeneratedCodeCache::EnqueueOperation(
std::unique_ptr<PendingOperation> op) {
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
// still being opened.
pending_ops_.emplace(std::move(op));
return;
}
EnqueueOperationAndIssueIfNext(std::move(op));
}
void GeneratedCodeCache::IssuePendingOperations() {
// Issue any operations that were received while creating the backend.
while (!pending_ops_.empty()) {
......@@ -407,9 +491,11 @@ void GeneratedCodeCache::IssuePendingOperations() {
void GeneratedCodeCache::IssueOperation(PendingOperation* op) {
switch (op->operation()) {
case kFetch:
case kFetchWithSHAKey:
FetchEntryImpl(op);
break;
case kWrite:
case kWriteWithSHAKey:
WriteEntryImpl(op);
break;
case kDelete:
......@@ -422,7 +508,8 @@ void GeneratedCodeCache::IssueOperation(PendingOperation* op) {
}
void GeneratedCodeCache::WriteEntryImpl(PendingOperation* op) {
DCHECK_EQ(Operation::kWrite, op->operation());
DCHECK(Operation::kWrite == op->operation() ||
Operation::kWriteWithSHAKey == op->operation());
if (backend_state_ != kInitialized) {
// Silently fail the request.
CloseOperationAndIssueNext(op);
......@@ -442,7 +529,8 @@ void GeneratedCodeCache::WriteEntryImpl(PendingOperation* op) {
void GeneratedCodeCache::OpenCompleteForWrite(
PendingOperation* op,
disk_cache::EntryResult entry_result) {
DCHECK_EQ(Operation::kWrite, op->operation());
DCHECK(Operation::kWrite == op->operation() ||
Operation::kWriteWithSHAKey == op->operation());
if (entry_result.net_error() != net::OK) {
CollectStatistics(CacheEntryStatus::kError);
CloseOperationAndIssueNext(op);
......@@ -459,6 +547,20 @@ void GeneratedCodeCache::OpenCompleteForWrite(
// There should be a valid entry if the open was successful.
DCHECK(entry);
// For merged entries, don't write if the entry already exists.
if (op->operation() == Operation::kWriteWithSHAKey) {
int small_size = entry->GetDataSize(kSmallDataStream);
int large_size = entry->GetDataSize(kLargeDataStream);
if (small_size == 0 && large_size == op->large_buffer()->size()) {
// Skip overwriting with identical data.
CloseOperationAndIssueNext(op);
return;
}
// Otherwise, there shouldn't be any data for this entry yet.
DCHECK_EQ(0, small_size);
DCHECK_EQ(0, large_size);
}
// Write the small data first, truncating.
auto small_buffer = op->small_buffer();
int result = entry->WriteData(
......@@ -486,7 +588,8 @@ void GeneratedCodeCache::OpenCompleteForWrite(
void GeneratedCodeCache::WriteSmallBufferComplete(PendingOperation* op,
int rv) {
DCHECK_EQ(Operation::kWrite, op->operation());
DCHECK(Operation::kWrite == op->operation() ||
Operation::kWriteWithSHAKey == op->operation());
if (op->AddBufferCompletion(rv == op->small_buffer()->size())) {
WriteComplete(op);
}
......@@ -494,14 +597,16 @@ void GeneratedCodeCache::WriteSmallBufferComplete(PendingOperation* op,
void GeneratedCodeCache::WriteLargeBufferComplete(PendingOperation* op,
int rv) {
DCHECK_EQ(Operation::kWrite, op->operation());
DCHECK(Operation::kWrite == op->operation() ||
Operation::kWriteWithSHAKey == op->operation());
if (op->AddBufferCompletion(rv == op->large_buffer()->size())) {
WriteComplete(op);
}
}
void GeneratedCodeCache::WriteComplete(PendingOperation* op) {
DCHECK_EQ(Operation::kWrite, op->operation());
DCHECK(Operation::kWrite == op->operation() ||
Operation::kWriteWithSHAKey == op->operation());
if (!op->succeeded()) {
// The write failed; record the failure and doom the entry here.
CollectStatistics(CacheEntryStatus::kWriteFailed);
......@@ -511,7 +616,8 @@ void GeneratedCodeCache::WriteComplete(PendingOperation* op) {
}
void GeneratedCodeCache::FetchEntryImpl(PendingOperation* op) {
DCHECK_EQ(Operation::kFetch, op->operation());
DCHECK(Operation::kFetch == op->operation() ||
Operation::kFetchWithSHAKey == op->operation());
if (backend_state_ != kInitialized) {
op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer());
CloseOperationAndIssueNext(op);
......@@ -531,7 +637,8 @@ void GeneratedCodeCache::FetchEntryImpl(PendingOperation* op) {
void GeneratedCodeCache::OpenCompleteForRead(
PendingOperation* op,
disk_cache::EntryResult entry_result) {
DCHECK_EQ(Operation::kFetch, op->operation());
DCHECK(Operation::kFetch == op->operation() ||
Operation::kFetchWithSHAKey == op->operation());
if (entry_result.net_error() != net::OK) {
CollectStatistics(CacheEntryStatus::kMiss);
op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer());
......@@ -544,13 +651,20 @@ void GeneratedCodeCache::OpenCompleteForRead(
DCHECK(entry);
int small_size = entry->GetDataSize(kSmallDataStream);
scoped_refptr<net::IOBufferWithSize> small_buffer =
base::MakeRefCounted<net::IOBufferWithSize>(small_size);
op->set_small_buffer(small_buffer);
int large_size = entry->GetDataSize(kLargeDataStream);
scoped_refptr<BigIOBuffer> large_buffer =
base::MakeRefCounted<BigIOBuffer>(large_size);
op->set_large_buffer(large_buffer);
scoped_refptr<net::IOBufferWithSize> small_buffer;
scoped_refptr<BigIOBuffer> large_buffer;
if (op->operation() == Operation::kFetch) {
small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(small_size);
op->set_small_buffer(small_buffer);
large_buffer = base::MakeRefCounted<BigIOBuffer>(large_size);
op->set_large_buffer(large_buffer);
} else {
small_buffer = op->small_buffer();
large_buffer = op->large_buffer();
DCHECK_EQ(small_size, small_buffer->size());
DCHECK_EQ(large_size, large_buffer->size());
}
// Read the small data first.
int result = entry->ReadData(
......@@ -577,8 +691,11 @@ void GeneratedCodeCache::OpenCompleteForRead(
}
void GeneratedCodeCache::ReadSmallBufferComplete(PendingOperation* op, int rv) {
DCHECK_EQ(Operation::kFetch, op->operation());
bool succeeded = rv == op->small_buffer()->size() && rv >= kHeaderSizeInBytes;
DCHECK(Operation::kFetch == op->operation() ||
Operation::kFetchWithSHAKey == op->operation());
bool no_header = op->operation() == Operation::kFetchWithSHAKey;
bool succeeded = (rv == op->small_buffer()->size() &&
(no_header || IsValidHeader(op->small_buffer())));
CollectStatistics(succeeded ? CacheEntryStatus::kHit
: CacheEntryStatus::kMiss);
......@@ -591,30 +708,52 @@ void GeneratedCodeCache::ReadSmallBufferComplete(PendingOperation* op, int rv) {
}
void GeneratedCodeCache::ReadLargeBufferComplete(PendingOperation* op, int rv) {
DCHECK_EQ(Operation::kFetch, op->operation());
DCHECK(Operation::kFetch == op->operation() ||
Operation::kFetchWithSHAKey == op->operation());
if (op->AddBufferCompletion(rv == op->large_buffer()->size()))
ReadComplete(op);
}
void GeneratedCodeCache::ReadComplete(PendingOperation* op) {
DCHECK_EQ(Operation::kFetch, op->operation());
DCHECK(Operation::kFetch == op->operation() ||
Operation::kFetchWithSHAKey == op->operation());
if (!op->succeeded()) {
op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer());
// Doom this entry since it is inaccessible.
DoomEntry(op);
} else {
base::Time response_time;
uint32_t data_size = 0;
ReadSmallDataHeader(op->small_buffer(), &response_time, &data_size);
if (data_size <= kSmallDataLimit) {
// Small data, copy the data from the small buffer.
DCHECK_EQ(0, op->large_buffer()->size());
mojo_base::BigBuffer data(data_size);
memcpy(data.data(), op->small_buffer()->data() + kHeaderSizeInBytes,
data_size);
op->TakeReadCallback().Run(response_time, std::move(data));
if (op->operation() != Operation::kFetchWithSHAKey) {
base::Time response_time;
uint32_t data_size = 0;
ReadCommonDataHeader(op->small_buffer(), &response_time, &data_size);
if (data_size <= kSmallDataLimit) {
// Small data. Copy the data from the small buffer.
DCHECK_EQ(0, op->large_buffer()->size());
mojo_base::BigBuffer data(data_size);
memcpy(data.data(), op->small_buffer()->data() + kHeaderSizeInBytes,
data_size);
op->TakeReadCallback().Run(response_time, std::move(data));
} else if (data_size <= kLargeDataLimit) {
// Large data below the merging threshold. Return the large buffer.
op->TakeReadCallback().Run(response_time,
op->large_buffer()->TakeBuffer());
} else {
// Very large data. Create the second fetch using the checksum as key.
DCHECK_EQ(static_cast<int>(kHeaderSizeInBytes + kSHAKeySizeInBytes),
op->small_buffer()->size());
std::string checksum_key(
op->small_buffer()->data() + kHeaderSizeInBytes,
kSHAKeySizeInBytes);
auto small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(0);
auto large_buffer = base::MakeRefCounted<BigIOBuffer>(data_size);
auto op2 = std::make_unique<PendingOperation>(
Operation::kFetchWithSHAKey, checksum_key, response_time,
small_buffer, large_buffer, op->TakeReadCallback());
EnqueueOperation(std::move(op2));
}
} else {
op->TakeReadCallback().Run(response_time,
// Large merged code data with no header. |op| holds the response time.
op->TakeReadCallback().Run(op->response_time(),
op->large_buffer()->TakeBuffer());
}
}
......@@ -622,7 +761,7 @@ void GeneratedCodeCache::ReadComplete(PendingOperation* op) {
}
void GeneratedCodeCache::DeleteEntryImpl(PendingOperation* op) {
DCHECK(op->operation() == Operation::kDelete);
DCHECK_EQ(Operation::kDelete, op->operation());
DoomEntry(op);
CloseOperationAndIssueNext(op);
}
......
......@@ -119,7 +119,14 @@ class CONTENT_EXPORT GeneratedCodeCache {
enum BackendState { kInitializing, kInitialized, kFailed };
// The operation requested.
enum Operation { kFetch, kWrite, kDelete, kGetBackend };
enum Operation {
kFetch,
kFetchWithSHAKey,
kWrite,
kWriteWithSHAKey,
kDelete,
kGetBackend
};
// Data streams corresponding to each entry.
enum { kSmallDataStream = 0, kLargeDataStream = 1 };
......@@ -130,6 +137,9 @@ class CONTENT_EXPORT GeneratedCodeCache {
scoped_refptr<base::RefCountedData<ScopedBackendPtr>> backend_ptr,
int rv);
// Adds operation to the appropriate queue.
void EnqueueOperation(std::unique_ptr<PendingOperation> op);
// Issues ops that were received while the backend was being initialized.
void IssuePendingOperations();
void IssueOperation(PendingOperation* op);
......
......@@ -6,6 +6,7 @@
#include "base/bind.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/task_environment.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
......@@ -17,8 +18,15 @@ namespace content {
class GeneratedCodeCacheTest : public testing::Test {
public:
static const int kLargeSizeInBytes = 8192;
static const int kMaxSizeInBytes = 1024 * 1024;
// This should be larger than |kSmallDataLimit| in generated_code_cache.cc.
static const size_t kLargeSizeInBytes = 8192;
// This should be larger than |kLargeDataLimit| in generated_code_cache.cc.
// Additionally, this shouldn't exceed 1/8 of the maximum cache size below,
// |kMaxSizeInBytes|.
static const size_t kVeryLargeSizeInBytes = 128 * 1024;
static const size_t kMaxSizeInBytes = 1024 * 1024;
static_assert(kMaxSizeInBytes / kVeryLargeSizeInBytes > 0UL,
"Cache will be too small to hold a very large item");
static constexpr char kInitialUrl[] = "http://example.com/script.js";
static constexpr char kInitialOrigin[] = "http://example.com";
static constexpr char kInitialData[] = "InitialData";
......@@ -121,7 +129,7 @@ class GeneratedCodeCacheTest : public testing::Test {
constexpr char GeneratedCodeCacheTest::kInitialUrl[];
constexpr char GeneratedCodeCacheTest::kInitialOrigin[];
constexpr char GeneratedCodeCacheTest::kInitialData[];
const int GeneratedCodeCacheTest::kMaxSizeInBytes;
const size_t GeneratedCodeCacheTest::kMaxSizeInBytes;
TEST_F(GeneratedCodeCacheTest, CheckResponseTime) {
GURL url(kInitialUrl);
......@@ -131,7 +139,6 @@ TEST_F(GeneratedCodeCacheTest, CheckResponseTime) {
std::string data = "SerializedCodeForScript";
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -160,7 +167,6 @@ TEST_F(GeneratedCodeCacheTest, WriteEntry) {
std::string data = "SerializedCodeForScript";
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -177,7 +183,22 @@ TEST_F(GeneratedCodeCacheTest, WriteLargeEntry) {
std::string large_data(kLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_);
EXPECT_EQ(large_data, received_data_);
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, WriteVeryLargeEntry) {
GURL new_url("http://example1.com/script.js");
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kVeryLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -206,7 +227,6 @@ TEST_F(GeneratedCodeCacheTest, WriteEntryWithEmptyData) {
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, std::string(), response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -223,7 +243,6 @@ TEST_F(GeneratedCodeCacheTest, WriteEntryFailure) {
base::Time response_time = base::Time::Now();
std::string too_big_data(kMaxSizeInBytes * 8, 0);
WriteToCache(url, origin_lock, too_big_data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -245,7 +264,6 @@ TEST_F(GeneratedCodeCacheTest, WriteEntryFailureOutOfOrder) {
base::Time response_time = base::Time::Now();
std::string too_big_data(kMaxSizeInBytes * 8, 0);
WriteToCache(url, origin_lock, too_big_data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -275,7 +293,6 @@ TEST_F(GeneratedCodeCacheTest, WriteEntryPendingOp) {
std::string data = "SerializedCodeForScript";
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -292,7 +309,22 @@ TEST_F(GeneratedCodeCacheTest, WriteLargeEntryPendingOp) {
std::string large_data(kLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_);
EXPECT_EQ(large_data, received_data_);
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, WriteVeryLargeEntryPendingOp) {
GURL new_url("http://example1.com/script1.js");
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kVeryLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
FetchFromCache(new_url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -322,7 +354,6 @@ TEST_F(GeneratedCodeCacheTest, UpdateDataOfExistingEntry) {
std::string new_data = "SerializedCodeForScriptOverwrite";
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -339,7 +370,6 @@ TEST_F(GeneratedCodeCacheTest, UpdateDataOfSmallExistingEntry) {
std::string new_data(kLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -359,7 +389,25 @@ TEST_F(GeneratedCodeCacheTest, UpdateDataOfLargeExistingEntry) {
std::string new_data = large_data + "Overwrite";
response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_);
EXPECT_EQ(new_data, received_data_);
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, UpdateDataOfVeryLargeExistingEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kVeryLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, large_data, response_time);
std::string new_data = large_data + "Overwrite";
response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -379,7 +427,25 @@ TEST_F(GeneratedCodeCacheTest, TruncateDataOfLargeExistingEntry) {
std::string new_data = "SerializedCodeForScriptOverwrite";
response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_);
EXPECT_EQ(new_data, received_data_);
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, TruncateDataOfVeryLargeExistingEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kVeryLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(url, origin_lock, large_data, response_time);
std::string new_data = "SerializedCodeForScriptOverwrite";
response_time = base::Time::Now();
WriteToCache(url, origin_lock, new_data, response_time);
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -409,7 +475,6 @@ TEST_F(GeneratedCodeCacheTest, FetchEntriesFromSameOrigin) {
std::string data_second_resource = "SerializedCodeForSecondResource";
WriteToCache(second_url, origin_lock, data_second_resource, base::Time());
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -433,7 +498,6 @@ TEST_F(GeneratedCodeCacheTest, FetchSucceedsFromDifferentOrigins) {
std::string data_origin1 = "SerializedCodeForSecondOrigin";
WriteToCache(url, origin_lock1, data_origin1, base::Time());
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -446,6 +510,74 @@ TEST_F(GeneratedCodeCacheTest, FetchSucceedsFromDifferentOrigins) {
EXPECT_EQ(data_origin1, received_data_);
}
TEST_F(GeneratedCodeCacheTest, VeryLargeEntriesAreMerged) {
GURL url("http://example.com/script.js");
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
// Write more copies of the same resource than the cache can hold unless they
// are merged by content.
for (size_t i = 0; i < 2 * kMaxSizeInBytes / kVeryLargeSizeInBytes; ++i) {
GURL origin_lock = GURL(std::string("http://example") +
base::NumberToString(i) + std::string(".com"));
std::string large_data(kVeryLargeSizeInBytes, 'x');
WriteToCache(url, origin_lock, large_data, base::Time());
}
for (size_t i = 0; i < 2 * kMaxSizeInBytes / kVeryLargeSizeInBytes; ++i) {
GURL origin_lock = GURL(std::string("http://example") +
base::NumberToString(i) + std::string(".com"));
std::string large_data(kVeryLargeSizeInBytes, 'x');
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
ASSERT_TRUE(received_);
EXPECT_EQ(large_data, received_data_);
received_ = false;
received_data_ = std::string();
}
}
TEST_F(GeneratedCodeCacheTest, StressVeryLargeEntries) {
GURL url("http://example.com/script.js");
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
// Fill the cache with very large data keyed by the SHA-256 checksum.
char data1 = 0;
for (size_t i = 0; i < kMaxSizeInBytes / kVeryLargeSizeInBytes - 1;
++i, ++data1) {
GURL origin_lock = GURL(std::string("http://example") +
base::NumberToString(i) + std::string(".com"));
std::string large_data(kVeryLargeSizeInBytes, data1);
WriteToCache(url, origin_lock, large_data, base::Time());
}
// Fill the cache with new data. The old entries should be purged to make
// room for the new ones.
char data2 = -128;
for (size_t i = 0; i < kMaxSizeInBytes / kVeryLargeSizeInBytes - 1;
++i, ++data2) {
GURL origin_lock = GURL(std::string("http://example") +
base::NumberToString(i) + std::string(".com"));
std::string large_data(kVeryLargeSizeInBytes, data2);
WriteToCache(url, origin_lock, large_data, base::Time());
}
data2 = -128;
for (size_t i = 0; i < kMaxSizeInBytes / kVeryLargeSizeInBytes - 1;
++i, ++data2) {
GURL origin_lock = GURL(std::string("http://example") +
base::NumberToString(i) + std::string(".com"));
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
// We can't depend too strongly on the disk cache storage heuristic. Verify
// that if we received data, it's what we wrote.
if (!received_null_) {
std::string large_data(kVeryLargeSizeInBytes, data2);
EXPECT_EQ(large_data, received_data_);
received_ = false;
received_data_ = std::string();
}
}
}
TEST_F(GeneratedCodeCacheTest, FetchSucceedsEmptyOriginLock) {
GURL url("http://example.com/script.js");
GURL origin_lock = GURL("");
......@@ -453,7 +585,6 @@ TEST_F(GeneratedCodeCacheTest, FetchSucceedsEmptyOriginLock) {
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string data = "SerializedCodeForEmptyOrigin";
WriteToCache(url, origin_lock, data, base::Time());
task_environment_.RunUntilIdle();
FetchFromCache(url, origin_lock);
task_environment_.RunUntilIdle();
......@@ -469,11 +600,9 @@ TEST_F(GeneratedCodeCacheTest, FetchEmptyOriginVsValidOriginLocks) {
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string empty_origin_data = "SerializedCodeForEmptyOrigin";
WriteToCache(url, empty_origin_lock, empty_origin_data, base::Time());
task_environment_.RunUntilIdle();
std::string valid_origin_data = "SerializedCodeForValidOrigin";
WriteToCache(url, origin_lock, valid_origin_data, base::Time());
task_environment_.RunUntilIdle();
FetchFromCache(url, empty_origin_lock);
task_environment_.RunUntilIdle();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment