Commit d99ef40d authored by Bill Budge's avatar Bill Budge Committed by Commit Bot

[code caching] Handle small data with a single read/write

- Many small reads and writes are observed, especially with the JavaScript
  code cache. Detect these and write all data into the entry's stream 0,
  and clear stream 1. When reading, detect the entry size and for small
  data, skip the stream 1 read and copy the data from the stream 0 read.
- Changes the stream 0 data (again) to a header with response time and
  data size. The data size, while not strictly necessary right now, will
  be needed if we implement de-duplication of identical entries stored by
  multiple origins. We can add the code hash to this header, and the size
  field will disambiguate between small entries and large de-duplicated
  entries where we use the hash as a key to the data.
- This change should make small reads faster than before, as synchronous
  completion of stream 0 reads is observed. Otherwise, there should be no
  change in performance.
- Renames the buffers and completion callbacks to reflect small/large data
  distinction.

Bug: chromium:992991
Change-Id: I6fb5337ef1e4148dd9f300f0a8c85acb401be62e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1834562
Commit-Queue: Bill Budge <bbudge@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarMythri Alle <mythria@chromium.org>
Reviewed-by: default avatarMaks Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/master@{#702667}
parent 4529d179
......@@ -68,6 +68,37 @@ std::string GetCacheKey(const GURL& resource_url, const GURL& origin_lock) {
}
constexpr int kResponseTimeSizeInBytes = sizeof(int64_t);
constexpr int kDataSizeInBytes = sizeof(uint32_t);
constexpr int kHeaderSizeInBytes = kResponseTimeSizeInBytes + kDataSizeInBytes;
// 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
// code entries too.
constexpr int kSmallDataLimit = 4096;
void WriteSmallDataHeader(scoped_refptr<net::IOBufferWithSize> buffer,
const base::Time& response_time,
uint32_t data_size) {
DCHECK_LE(kHeaderSizeInBytes, buffer->size());
int64_t serialized_time =
response_time.ToDeltaSinceWindowsEpoch().InMicroseconds();
memcpy(buffer->data(), &serialized_time, kResponseTimeSizeInBytes);
// Copy size to small data buffer.
memcpy(buffer->data() + kResponseTimeSizeInBytes, &data_size,
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()));
*response_time = base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(raw_response_time));
*data_size =
*(reinterpret_cast<uint32_t*>(buffer->data() + kResponseTimeSizeInBytes));
}
static_assert(mojo_base::BigBuffer::kMaxInlineBytes <=
std::numeric_limits<int>::max(),
......@@ -81,7 +112,6 @@ class BigIOBuffer : public net::IOBufferWithSize {
: net::IOBufferWithSize(nullptr, buffer.size()),
buffer_(std::move(buffer)) {
data_ = reinterpret_cast<char*>(buffer_.data());
DCHECK(data_);
}
explicit BigIOBuffer(size_t size) : net::IOBufferWithSize(nullptr, size) {
buffer_ = mojo_base::BigBuffer(size);
......@@ -135,12 +165,12 @@ class GeneratedCodeCache::PendingOperation {
public:
PendingOperation(Operation op,
const std::string& key,
scoped_refptr<net::IOBufferWithSize> time_buffer,
scoped_refptr<BigIOBuffer> data_buffer)
scoped_refptr<net::IOBufferWithSize> small_buffer,
scoped_refptr<BigIOBuffer> large_buffer)
: op_(op),
key_(key),
time_buffer_(time_buffer),
data_buffer_(data_buffer) {
small_buffer_(small_buffer),
large_buffer_(large_buffer) {
DCHECK_EQ(Operation::kWrite, op_);
}
......@@ -164,24 +194,24 @@ class GeneratedCodeCache::PendingOperation {
Operation operation() const { return op_; }
const std::string& key() const { return key_; }
scoped_refptr<net::IOBufferWithSize> time_buffer() { return time_buffer_; }
scoped_refptr<BigIOBuffer> data_buffer() { return data_buffer_; }
scoped_refptr<net::IOBufferWithSize> small_buffer() { return small_buffer_; }
scoped_refptr<BigIOBuffer> large_buffer() { return large_buffer_; }
ReadDataCallback TakeReadCallback() { return std::move(read_callback_); }
GetBackendCallback TakeBackendCallback() {
return std::move(backend_callback_);
}
// These are used by Fetch operations to hold the buffers we create once the
// These are called by Fetch operations to hold the buffers we create once the
// entry is opened.
void set_time_buffer(scoped_refptr<net::IOBufferWithSize> time_buffer) {
void set_small_buffer(scoped_refptr<net::IOBufferWithSize> small_buffer) {
DCHECK_EQ(Operation::kFetch, op_);
time_buffer_ = time_buffer;
small_buffer_ = small_buffer;
}
// Save fetched data until we can run the callback.
void set_data_buffer(scoped_refptr<BigIOBuffer> data_buffer) {
void set_large_buffer(scoped_refptr<BigIOBuffer> large_buffer) {
DCHECK_EQ(Operation::kFetch, op_);
data_buffer_ = data_buffer;
large_buffer_ = large_buffer;
}
// Verifies that Write/Fetch callbacks are received in the order we expect.
void VerifyCompletions(int expected) {
#if DCHECK_IS_ON()
......@@ -193,8 +223,8 @@ class GeneratedCodeCache::PendingOperation {
private:
const Operation op_;
const std::string key_;
scoped_refptr<net::IOBufferWithSize> time_buffer_;
scoped_refptr<BigIOBuffer> data_buffer_;
scoped_refptr<net::IOBufferWithSize> small_buffer_;
scoped_refptr<BigIOBuffer> large_buffer_;
ReadDataCallback read_callback_;
GetBackendCallback backend_callback_;
#if DCHECK_IS_ON()
......@@ -241,19 +271,28 @@ void GeneratedCodeCache::WriteEntry(const GURL& url,
return;
}
// Response time and data are written separately, to avoid a copy. We need
// two IOBuffers, one for the time and one for the BigBuffer.
scoped_refptr<net::IOBufferWithSize> time_buffer =
base::MakeRefCounted<net::IOBufferWithSize>(kResponseTimeSizeInBytes);
int64_t serialized_time =
response_time.ToDeltaSinceWindowsEpoch().InMicroseconds();
memcpy(time_buffer->data(), &serialized_time, kResponseTimeSizeInBytes);
scoped_refptr<BigIOBuffer> data_buffer =
base::MakeRefCounted<BigIOBuffer>(std::move(data));
// If data is small, combine the header and data into a single write.
scoped_refptr<net::IOBufferWithSize> small_buffer;
scoped_refptr<BigIOBuffer> large_buffer;
uint32_t data_size = static_cast<uint32_t>(data.size());
if (data_size <= kSmallDataLimit) {
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.
large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer());
} else {
small_buffer =
base::MakeRefCounted<net::IOBufferWithSize>(kHeaderSizeInBytes);
large_buffer = base::MakeRefCounted<BigIOBuffer>(std::move(data));
}
WriteSmallDataHeader(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,
time_buffer, data_buffer);
small_buffer, large_buffer);
if (backend_state_ != kInitialized) {
// Insert it into the list of pending operations while the backend is
......@@ -416,47 +455,48 @@ void GeneratedCodeCache::OpenCompleteForWrite(
// There should be a valid entry if the open was successful.
DCHECK(entry);
// The response time must be written first, truncating the data.
auto time_buffer = op->time_buffer();
// Write the small data first, truncating.
auto small_buffer = op->small_buffer();
int result = entry->WriteData(
kResponseTimeStream, 0, time_buffer.get(), kResponseTimeSizeInBytes,
base::BindOnce(&GeneratedCodeCache::WriteResponseTimeComplete,
kSmallDataStream, 0, small_buffer.get(), small_buffer->size(),
base::BindOnce(&GeneratedCodeCache::WriteSmallBufferComplete,
weak_ptr_factory_.GetWeakPtr(), op),
true);
if (result != net::ERR_IO_PENDING) {
WriteResponseTimeComplete(op, result);
WriteSmallBufferComplete(op, result);
}
// Write the data after the response time, truncating the data.
auto data_buffer = op->data_buffer();
result =
entry->WriteData(kDataStream, 0, data_buffer.get(), data_buffer->size(),
base::BindOnce(&GeneratedCodeCache::WriteDataComplete,
weak_ptr_factory_.GetWeakPtr(), op),
true);
// Write the large data, truncating.
auto large_buffer = op->large_buffer();
result = entry->WriteData(
kLargeDataStream, 0, large_buffer.get(), large_buffer->size(),
base::BindOnce(&GeneratedCodeCache::WriteLargeBufferComplete,
weak_ptr_factory_.GetWeakPtr(), op),
true);
if (result != net::ERR_IO_PENDING) {
WriteDataComplete(op, result);
WriteLargeBufferComplete(op, result);
}
}
void GeneratedCodeCache::WriteResponseTimeComplete(PendingOperation* op,
int rv) {
void GeneratedCodeCache::WriteSmallBufferComplete(PendingOperation* op,
int rv) {
DCHECK_EQ(Operation::kWrite, op->operation());
op->VerifyCompletions(0); // WriteDataComplete did not run.
if (rv != kResponseTimeSizeInBytes) {
// The response time write failed; release the time buffer to signal that
op->VerifyCompletions(0); // WriteLargeBufferComplete did not run.
if (rv != op->small_buffer()->size()) {
// The small data write failed; release the small buffer to signal that
// the overall request should also fail.
op->set_time_buffer(nullptr);
op->set_small_buffer(nullptr);
}
// |WriteDataComplete| needs to run and call CloseOperationAndIssueNext.
// |WriteLargeBufferComplete| must run and call CloseOperationAndIssueNext.
}
void GeneratedCodeCache::WriteDataComplete(PendingOperation* op, int rv) {
void GeneratedCodeCache::WriteLargeBufferComplete(PendingOperation* op,
int rv) {
DCHECK_EQ(Operation::kWrite, op->operation());
op->VerifyCompletions(1); // WriteResponseTimeComplete ran.
if (rv != op->data_buffer()->size() || !op->time_buffer()) {
op->VerifyCompletions(1); // WriteSmallBufferComplete ran.
if (rv != op->large_buffer()->size() || !op->small_buffer()) {
// The write failed; record the failure and doom the entry here.
CollectStatistics(CacheEntryStatus::kWriteFailed);
DoomEntry(op);
......@@ -497,66 +537,79 @@ void GeneratedCodeCache::OpenCompleteForRead(
// There should be a valid entry if the open was successful.
DCHECK(entry);
// To avoid a copying the data, we read it in two parts, response time and
// code. Create the buffers and pass them to |op|.
scoped_refptr<net::IOBufferWithSize> time_buffer =
base::MakeRefCounted<net::IOBufferWithSize>(kResponseTimeSizeInBytes);
op->set_time_buffer(time_buffer);
int data_size = entry->GetDataSize(kDataStream);
scoped_refptr<BigIOBuffer> data_buffer =
base::MakeRefCounted<BigIOBuffer>(data_size);
op->set_data_buffer(data_buffer);
// We must read response time first.
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);
// Read the small data first.
int result = entry->ReadData(
kResponseTimeStream, 0, time_buffer.get(), kResponseTimeSizeInBytes,
base::BindOnce(&GeneratedCodeCache::ReadResponseTimeComplete,
kSmallDataStream, 0, small_buffer.get(), small_buffer->size(),
base::BindOnce(&GeneratedCodeCache::ReadSmallBufferComplete,
weak_ptr_factory_.GetWeakPtr(), op));
if (result != net::ERR_IO_PENDING) {
ReadResponseTimeComplete(op, result);
ReadSmallBufferComplete(op, result);
}
// Read the data after the response time.
result =
entry->ReadData(kDataStream, 0, data_buffer.get(), data_buffer->size(),
base::BindOnce(&GeneratedCodeCache::ReadDataComplete,
weak_ptr_factory_.GetWeakPtr(), op));
// Skip the large read if data is in the small read.
if (large_size == 0)
return;
// Read the large data.
result = entry->ReadData(
kLargeDataStream, 0, large_buffer.get(), large_buffer->size(),
base::BindOnce(&GeneratedCodeCache::ReadLargeBufferComplete,
weak_ptr_factory_.GetWeakPtr(), op));
if (result != net::ERR_IO_PENDING) {
ReadDataComplete(op, result);
ReadLargeBufferComplete(op, result);
}
}
void GeneratedCodeCache::ReadResponseTimeComplete(PendingOperation* op,
int rv) {
void GeneratedCodeCache::ReadSmallBufferComplete(PendingOperation* op, int rv) {
DCHECK_EQ(Operation::kFetch, op->operation());
op->VerifyCompletions(0); // ReadDataComplete did not run.
if (rv != kResponseTimeSizeInBytes) {
op->VerifyCompletions(0); // ReadLargeBufferComplete did not run.
if (rv != op->small_buffer()->size() || rv < kHeaderSizeInBytes) {
CollectStatistics(CacheEntryStatus::kMiss);
// The response time read failed; release the time buffer to signal that
// the overall request should also fail.
op->set_time_buffer(nullptr);
return;
// The small data stream read failed or is incomplete; release the buffer
// to signal that the overall request should also fail.
op->set_small_buffer(nullptr);
} else {
// This is considered a cache hit, since the small data was read.
CollectStatistics(CacheEntryStatus::kHit);
}
// This is considered a cache hit, since response time was read.
CollectStatistics(CacheEntryStatus::kHit);
// |ReadDataComplete| needs to run and call CloseOperationAndIssueNext.
// Small reads must finish now since no large read is pending.
if (op->large_buffer()->size() == 0)
ReadLargeBufferComplete(op, 0);
}
void GeneratedCodeCache::ReadDataComplete(PendingOperation* op, int rv) {
void GeneratedCodeCache::ReadLargeBufferComplete(PendingOperation* op, int rv) {
DCHECK_EQ(Operation::kFetch, op->operation());
op->VerifyCompletions(1); // ReadResponseTimeComplete ran.
op->VerifyCompletions(1); // ReadSmallBufferComplete ran.
// Fail the request if either read failed.
if (rv != op->data_buffer()->size() || !op->time_buffer()) {
if (rv != op->large_buffer()->size() || !op->small_buffer()) {
op->TakeReadCallback().Run(base::Time(), mojo_base::BigBuffer());
// Doom this entry since it is inaccessible.
DoomEntry(op);
} else {
int64_t raw_response_time =
*(reinterpret_cast<int64_t*>(op->time_buffer()->data()));
base::Time response_time = base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(raw_response_time));
op->TakeReadCallback().Run(response_time, op->data_buffer()->TakeBuffer());
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));
} else {
op->TakeReadCallback().Run(response_time,
op->large_buffer()->TakeBuffer());
}
}
CloseOperationAndIssueNext(op);
}
......
......@@ -122,7 +122,7 @@ class CONTENT_EXPORT GeneratedCodeCache {
enum Operation { kFetch, kWrite, kDelete, kGetBackend };
// Data streams corresponding to each entry.
enum { kResponseTimeStream = 0, kDataStream = 1 };
enum { kSmallDataStream = 0, kLargeDataStream = 1 };
// Creates a simple_disk_cache backend.
void CreateBackend();
......@@ -138,15 +138,15 @@ class CONTENT_EXPORT GeneratedCodeCache {
void WriteEntryImpl(PendingOperation* op);
void OpenCompleteForWrite(PendingOperation* op,
disk_cache::EntryResult result);
void WriteResponseTimeComplete(PendingOperation* op, int rv);
void WriteDataComplete(PendingOperation* op, int rv);
void WriteSmallBufferComplete(PendingOperation* op, int rv);
void WriteLargeBufferComplete(PendingOperation* op, int rv);
// Fetches entry from cache.
void FetchEntryImpl(PendingOperation* op);
void OpenCompleteForRead(PendingOperation* op,
disk_cache::EntryResult result);
void ReadResponseTimeComplete(PendingOperation* op, int rv);
void ReadDataComplete(PendingOperation* op, int rv);
void ReadSmallBufferComplete(PendingOperation* op, int rv);
void ReadLargeBufferComplete(PendingOperation* op, int rv);
// Deletes entry from cache.
void DeleteEntryImpl(PendingOperation* op);
......
......@@ -17,6 +17,7 @@ namespace content {
class GeneratedCodeCacheTest : public testing::Test {
public:
static const int kLargeSizeInBytes = 8192;
static const int kMaxSizeInBytes = 1024 * 1024;
static constexpr char kInitialUrl[] = "http://example.com/script.js";
static constexpr char kInitialOrigin[] = "http://example.com";
......@@ -153,6 +154,23 @@ TEST_F(GeneratedCodeCacheTest, WriteEntry) {
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, WriteLargeEntry) {
GURL new_url("http://example1.com/script.js");
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
task_environment_.RunUntilIdle();
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, DeleteEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
......@@ -229,6 +247,23 @@ TEST_F(GeneratedCodeCacheTest, WriteEntryPendingOp) {
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, WriteLargeEntryPendingOp) {
GURL new_url("http://example1.com/script1.js");
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kLargeSizeInBytes, 'x');
base::Time response_time = base::Time::Now();
WriteToCache(new_url, origin_lock, large_data, response_time);
task_environment_.RunUntilIdle();
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, DeleteEntryPendingOp) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
......@@ -259,6 +294,63 @@ TEST_F(GeneratedCodeCacheTest, UpdateDataOfExistingEntry) {
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, UpdateDataOfSmallExistingEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
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();
ASSERT_TRUE(received_);
EXPECT_EQ(new_data, received_data_);
EXPECT_EQ(response_time, received_response_time_);
}
TEST_F(GeneratedCodeCacheTest, UpdateDataOfLargeExistingEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kLargeSizeInBytes, '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);
task_environment_.RunUntilIdle();
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, TruncateDataOfLargeExistingEntry) {
GURL url(kInitialUrl);
GURL origin_lock = GURL(kInitialOrigin);
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
std::string large_data(kLargeSizeInBytes, '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);
task_environment_.RunUntilIdle();
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, FetchFailsForNonexistingOrigin) {
InitializeCache(GeneratedCodeCache::CodeCacheType::kJavaScript);
GURL new_origin_lock = GURL("http://not-example.com");
......
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