| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/platform/disk_data_allocator.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/containers/contains.h" |
| #include "base/logging.h" |
| #include "base/synchronization/lock.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/renderer/platform/disk_data_metadata.h" |
| #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h" |
| #include "third_party/blink/renderer/platform/wtf/wtf.h" |
| |
| namespace { |
| constexpr size_t kMB = 1024 * 1024; |
| } |
| |
| namespace blink { |
| |
| DiskDataAllocator::DiskDataAllocator() { |
| if (features::kMaxDiskDataAllocatorCapacityMB.Get() > 0) { |
| has_capacity_limit_ = true; |
| max_capacity_ = features::kMaxDiskDataAllocatorCapacityMB.Get() * kMB; |
| } |
| } |
| |
| DiskDataAllocator::~DiskDataAllocator() = default; |
| |
| bool DiskDataAllocator::may_write() { |
| base::AutoLock locker(lock_); |
| return may_write_; |
| } |
| |
| void DiskDataAllocator::set_may_write_for_testing(bool may_write) { |
| base::AutoLock locker(lock_); |
| may_write_ = may_write; |
| } |
| |
| DiskDataMetadata DiskDataAllocator::FindFreeChunk(size_t size) { |
| // Try to reuse some space. Policy: |
| // 1. Exact fit |
| // 2. Worst fit |
| DiskDataMetadata chosen_chunk{-1, 0}; |
| |
| size_t worst_fit_size = 0; |
| for (const auto& chunk : free_chunks_) { |
| size_t chunk_size = chunk.second; |
| if (size == chunk_size) { |
| chosen_chunk = {chunk.first, chunk.second}; |
| break; |
| } else if (chunk_size > size && chunk_size > worst_fit_size) { |
| chosen_chunk = {chunk.first, chunk.second}; |
| worst_fit_size = chunk.second; |
| } |
| } |
| |
| if (chosen_chunk.start_offset() != -1) { |
| free_chunks_size_ -= size; |
| free_chunks_.erase(chosen_chunk.start_offset()); |
| if (chosen_chunk.size() > size) { |
| std::pair<int64_t, size_t> remainder_chunk = { |
| chosen_chunk.start_offset() + size, chosen_chunk.size() - size}; |
| auto result = free_chunks_.insert(remainder_chunk); |
| DCHECK(result.second); |
| chosen_chunk.size_ = size; |
| } |
| } |
| |
| return chosen_chunk; |
| } |
| |
| void DiskDataAllocator::ReleaseChunk(const DiskDataMetadata& metadata) { |
| DiskDataMetadata chunk = metadata; |
| DCHECK(!base::Contains(free_chunks_, chunk.start_offset())); |
| |
| auto lower_bound = free_chunks_.lower_bound(chunk.start_offset()); |
| DCHECK(free_chunks_.upper_bound(chunk.start_offset()) == |
| free_chunks_.lower_bound(chunk.start_offset())); |
| if (lower_bound != free_chunks_.begin()) { |
| // There is a chunk left. |
| auto left = --lower_bound; |
| // Can merge with the left chunk. |
| int64_t left_chunk_end = left->first + left->second; |
| DCHECK_LE(left_chunk_end, chunk.start_offset()); |
| if (left_chunk_end == chunk.start_offset()) { |
| chunk = {left->first, left->second + chunk.size()}; |
| free_chunks_size_ -= left->second; |
| free_chunks_.erase(left); |
| } |
| } |
| |
| auto right = free_chunks_.upper_bound(chunk.start_offset()); |
| if (right != free_chunks_.end()) { |
| DCHECK_NE(right->first, chunk.start_offset()); |
| int64_t chunk_end = chunk.start_offset() + chunk.size(); |
| DCHECK_LE(chunk_end, right->first); |
| if (right->first == chunk_end) { |
| chunk = {chunk.start_offset(), chunk.size() + right->second}; |
| free_chunks_size_ -= right->second; |
| free_chunks_.erase(right); |
| } |
| } |
| |
| auto result = free_chunks_.insert({chunk.start_offset(), chunk.size()}); |
| DCHECK(result.second); |
| free_chunks_size_ += chunk.size(); |
| } |
| |
| std::unique_ptr<ReservedChunk> DiskDataAllocator::TryReserveChunk(size_t size) { |
| base::AutoLock locker(lock_); |
| if (!may_write_) { |
| return nullptr; |
| } |
| |
| DiskDataMetadata chosen_chunk = FindFreeChunk(size); |
| if (chosen_chunk.start_offset() < 0) { |
| if (has_capacity_limit_ && file_tail_ + size > max_capacity_) { |
| return nullptr; |
| } |
| chosen_chunk = {file_tail_, size}; |
| file_tail_ += size; |
| } |
| |
| #if DCHECK_IS_ON() |
| allocated_chunks_.insert({chosen_chunk.start_offset(), chosen_chunk.size()}); |
| #endif |
| |
| return std::make_unique<ReservedChunk>( |
| this, std::unique_ptr<DiskDataMetadata>(new DiskDataMetadata( |
| chosen_chunk.start_offset(), chosen_chunk.size()))); |
| } |
| |
| std::unique_ptr<DiskDataMetadata> DiskDataAllocator::Write( |
| std::unique_ptr<ReservedChunk> chunk, |
| const void* data) { |
| std::unique_ptr<DiskDataMetadata> metadata = chunk->Take(); |
| DCHECK(metadata); |
| |
| int size_int = static_cast<int>(metadata->size()); |
| const char* data_char = reinterpret_cast<const char*>(data); |
| int written = DoWrite(metadata->start_offset(), data_char, size_int); |
| |
| if (size_int != written) { |
| Discard(std::move(metadata)); |
| |
| // Assume that the error is not transient. This can happen if the disk is |
| // full for instance, in which case it is likely better not to try writing |
| // later. |
| base::AutoLock locker(lock_); |
| may_write_ = false; |
| return nullptr; |
| } |
| |
| return metadata; |
| } |
| |
| void DiskDataAllocator::Read(const DiskDataMetadata& metadata, void* data) { |
| // Doesn't need locking as files support concurrent access, and we don't |
| // update metadata. |
| char* data_char = reinterpret_cast<char*>(data); |
| DoRead(metadata.start_offset(), data_char, |
| base::checked_cast<int>(metadata.size())); |
| |
| #if DCHECK_IS_ON() |
| { |
| base::AutoLock locker(lock_); |
| auto it = allocated_chunks_.find(metadata.start_offset()); |
| DCHECK(it != allocated_chunks_.end()); |
| DCHECK_EQ(metadata.size(), it->second); |
| } |
| #endif |
| } |
| |
| void DiskDataAllocator::Discard(std::unique_ptr<DiskDataMetadata> metadata) { |
| base::AutoLock locker(lock_); |
| DCHECK(may_write_ || file_.IsValid()); |
| |
| #if DCHECK_IS_ON() |
| auto it = allocated_chunks_.find(metadata->start_offset()); |
| DCHECK(it != allocated_chunks_.end()); |
| DCHECK_EQ(metadata->size(), it->second); |
| allocated_chunks_.erase(it); |
| #endif |
| |
| ReleaseChunk(*metadata); |
| } |
| |
| int DiskDataAllocator::DoWrite(int64_t offset, const char* data, int size) { |
| int rv = file_.Write(offset, data, size); |
| |
| // No PCHECK(), since a file writing error is recoverable. |
| if (rv != size) { |
| LOG(ERROR) << "DISK: Cannot write to disk. written = " << rv << " " |
| << base::File::ErrorToString(base::File::GetLastFileError()); |
| } |
| return rv; |
| } |
| |
| void DiskDataAllocator::DoRead(int64_t offset, char* data, int size) { |
| // This happens on the main thread, which is typically not allowed. This is |
| // fine as this is expected to happen rarely, and only be slow with memory |
| // pressure, in which case writing to/reading from disk is better than |
| // swapping out random parts of the memory. See crbug.com/1029320 for details. |
| base::ScopedAllowBlocking allow_blocking; |
| int rv = file_.Read(offset, data, size); |
| // Can only crash, since we cannot continue without the data. |
| PCHECK(rv == size) << "Likely file corruption."; |
| } |
| |
| void DiskDataAllocator::ProvideTemporaryFile(base::File file) { |
| base::AutoLock locker(lock_); |
| DCHECK(IsMainThread()); |
| DCHECK(!file_.IsValid()); |
| DCHECK(!may_write_); |
| |
| file_ = std::move(file); |
| may_write_ = file_.IsValid(); |
| } |
| |
| // static |
| DiskDataAllocator& DiskDataAllocator::Instance() { |
| DEFINE_THREAD_SAFE_STATIC_LOCAL(DiskDataAllocator, instance, ()); |
| return instance; |
| } |
| |
| // static |
| void DiskDataAllocator::Bind( |
| mojo::PendingReceiver<mojom::blink::DiskAllocator> receiver) { |
| DCHECK(!Instance().receiver_.is_bound()); |
| Instance().receiver_.Bind(std::move(receiver)); |
| } |
| |
| } // namespace blink |