| // Copyright 2021 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 "third_party/blink/renderer/platform/graphics/parkable_image.h" |
| |
| #include "base/debug/stack_trace.h" |
| #include "base/feature_list.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/trace_event/trace_event.h" |
| #include "third_party/blink/renderer/platform/graphics/parkable_image_manager.h" |
| #include "third_party/blink/renderer/platform/image-decoders/segment_reader.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/thread.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/worker_pool.h" |
| #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h" |
| #include "third_party/blink/renderer/platform/wtf/sanitizers.h" |
| #include "third_party/skia/include/core/SkRefCnt.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| void RecordReadStatistics(size_t size, base::TimeDelta duration) { |
| int throughput_mb_s = |
| static_cast<int>(size / duration.InSecondsF() / (1024 * 1024)); |
| int size_kb = static_cast<int>(size / 1024); // in KiB |
| |
| // Size should be <1MiB in most cases. |
| base::UmaHistogramCounts10000("Memory.ParkableImage.Read.Size", size_kb); |
| // Size is usually >1KiB, and at most ~10MiB, and throughput ranges from |
| // single-digit MB/s to ~1000MiB/s depending on the CPU/disk, hence the |
| // ranges. |
| base::UmaHistogramCustomMicrosecondsTimes("Memory.ParkableImage.Read.Latency", |
| duration, base::Microseconds(500), |
| base::Seconds(1), 100); |
| base::UmaHistogramCounts1000("Memory.ParkableImage.Read.Throughput", |
| throughput_mb_s); |
| } |
| |
| void RecordWriteStatistics(size_t size, base::TimeDelta duration) { |
| int throughput_mb_s = |
| static_cast<int>(size / duration.InSecondsF() / (1024 * 1024)); |
| int size_kb = static_cast<int>(size / 1024); // in KiB |
| |
| // Size should be <1MiB in most cases. |
| base::UmaHistogramCounts10000("Memory.ParkableImage.Write.Size", size_kb); |
| // Size is usually >1KiB, and at most ~10MiB, and throughput ranges from |
| // single-digit MB/s to ~1000MiB/s depending on the CPU/disk, hence the |
| // ranges. |
| base::UmaHistogramCustomMicrosecondsTimes( |
| "Memory.ParkableImage.Write.Latency", duration, base::Microseconds(500), |
| base::Seconds(1), 100); |
| base::UmaHistogramCounts1000("Memory.ParkableImage.Write.Throughput", |
| throughput_mb_s); |
| } |
| |
| void AsanPoisonBuffer(RWBuffer* rw_buffer) { |
| #if defined(ADDRESS_SANITIZER) |
| if (!rw_buffer || !rw_buffer->size()) |
| return; |
| |
| auto ro_buffer = rw_buffer->MakeROBufferSnapshot(); |
| ROBuffer::Iter iter(ro_buffer); |
| do { |
| ASAN_POISON_MEMORY_REGION(iter.data(), iter.size()); |
| } while (iter.Next()); |
| #endif |
| } |
| |
| void AsanUnpoisonBuffer(RWBuffer* rw_buffer) { |
| #if defined(ADDRESS_SANITIZER) |
| if (!rw_buffer || !rw_buffer->size()) |
| return; |
| |
| auto ro_buffer = rw_buffer->MakeROBufferSnapshot(); |
| ROBuffer::Iter iter(ro_buffer); |
| do { |
| ASAN_UNPOISON_MEMORY_REGION(iter.data(), iter.size()); |
| } while (iter.Next()); |
| #endif |
| } |
| |
| } // namespace |
| |
| const base::Feature kUseParkableImageSegmentReader{ |
| "UseParkableImageSegmentReader", base::FEATURE_DISABLED_BY_DEFAULT}; |
| |
| void ParkableImageImpl::Append(WTF::SharedBuffer* buffer, size_t offset) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| MutexLocker lock(lock_); |
| DCHECK(!frozen_); |
| DCHECK(!is_on_disk()); |
| DCHECK(rw_buffer_); |
| |
| for (auto it = buffer->GetIteratorAt(offset); it != buffer->cend(); ++it) { |
| DCHECK_GE(buffer->size(), rw_buffer_->size() + it->size()); |
| const size_t remaining = buffer->size() - rw_buffer_->size() - it->size(); |
| rw_buffer_->Append(it->data(), it->size(), remaining); |
| } |
| size_ = rw_buffer_->size(); |
| } |
| |
| scoped_refptr<SharedBuffer> ParkableImageImpl::Data() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| MutexLocker lock(lock_); |
| Unpark(); |
| DCHECK(rw_buffer_); |
| scoped_refptr<ROBuffer> ro_buffer(rw_buffer_->MakeROBufferSnapshot()); |
| scoped_refptr<SharedBuffer> shared_buffer = SharedBuffer::Create(); |
| ROBuffer::Iter it(ro_buffer.get()); |
| do { |
| shared_buffer->Append(static_cast<const char*>(it.data()), it.size()); |
| } while (it.Next()); |
| return shared_buffer; |
| } |
| |
| scoped_refptr<SegmentReader> ParkableImageImpl::GetROBufferSegmentReader() { |
| MutexLocker lock(lock_); |
| Unpark(); |
| DCHECK(rw_buffer_); |
| // The locking and unlocking here is only needed to make sure ASAN unpoisons |
| // things correctly here. |
| LockData(); |
| scoped_refptr<ROBuffer> ro_buffer(rw_buffer_->MakeROBufferSnapshot()); |
| scoped_refptr<SegmentReader> segment_reader = |
| SegmentReader::CreateFromROBuffer(std::move(ro_buffer)); |
| UnlockData(); |
| return segment_reader; |
| } |
| |
| bool ParkableImageImpl::CanParkNow() const { |
| DCHECK(!is_on_disk()); |
| return is_frozen() && !is_locked() && rw_buffer_->HasNoSnapshots(); |
| } |
| |
| ParkableImageImpl::ParkableImageImpl(size_t initial_capacity) |
| : rw_buffer_(std::make_unique<RWBuffer>(initial_capacity)) {} |
| |
| ParkableImageImpl::~ParkableImageImpl() { |
| DCHECK(IsMainThread()); |
| DCHECK(!is_locked()); |
| auto& manager = ParkableImageManager::Instance(); |
| if (!is_below_min_parking_size() || !is_frozen()) |
| manager.Remove(this); |
| DCHECK(!manager.IsRegistered(this)); |
| if (on_disk_metadata_) |
| manager.data_allocator().Discard(std::move(on_disk_metadata_)); |
| AsanUnpoisonBuffer(rw_buffer_.get()); |
| } |
| |
| // static |
| scoped_refptr<ParkableImageImpl> ParkableImageImpl::Create( |
| size_t initial_capacity) { |
| return base::MakeRefCounted<ParkableImageImpl>(initial_capacity); |
| } |
| |
| void ParkableImageImpl::Freeze() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| MutexLocker lock(lock_); |
| DCHECK(!frozen_); |
| frozen_ = true; |
| |
| if (is_below_min_parking_size()) { |
| ParkableImageManager::Instance().Remove(this); |
| return; |
| } |
| |
| // If we don't have any snapshots of the current data, that means it could be |
| // parked at any time. |
| // |
| // If we have snapshots, we don't want to poison the buffer, because the |
| // snapshot is allowed to access the buffer's data freely. |
| if (CanParkNow()) |
| AsanPoisonBuffer(rw_buffer_.get()); |
| } |
| |
| void ParkableImageImpl::LockData() { |
| // Calling |Lock| only makes sense if the data is available. |
| DCHECK(rw_buffer_); |
| |
| lock_depth_++; |
| |
| AsanUnpoisonBuffer(rw_buffer_.get()); |
| } |
| |
| void ParkableImageImpl::UnlockData() { |
| // Check that we've locked it already. |
| DCHECK_GT(lock_depth_, 0u); |
| // While locked, we can never write the data to disk. |
| DCHECK(!is_on_disk()); |
| |
| lock_depth_--; |
| |
| // We only poison the buffer if we're able to park after unlocking. |
| // This is to avoid issues when creating a ROBufferSegmentReader from the |
| // ParkableImageImpl. |
| if (CanParkNow()) |
| AsanPoisonBuffer(rw_buffer_.get()); |
| } |
| |
| // static |
| void ParkableImageImpl::WriteToDiskInBackground( |
| scoped_refptr<ParkableImageImpl> parkable_image, |
| scoped_refptr<base::SingleThreadTaskRunner> callback_task_runner) { |
| DCHECK(!IsMainThread()); |
| MutexLocker lock(parkable_image->lock_); |
| |
| DCHECK(ParkableImageManager::IsParkableImagesToDiskEnabled()); |
| DCHECK(parkable_image); |
| DCHECK(!parkable_image->on_disk_metadata_); |
| |
| AsanUnpoisonBuffer(parkable_image->rw_buffer_.get()); |
| |
| scoped_refptr<ROBuffer> ro_buffer = |
| parkable_image->rw_buffer_->MakeROBufferSnapshot(); |
| ROBuffer::Iter it(ro_buffer.get()); |
| |
| Vector<char> vector; |
| vector.ReserveInitialCapacity( |
| base::checked_cast<wtf_size_t>(parkable_image->size())); |
| |
| do { |
| vector.Append(reinterpret_cast<const char*>(it.data()), |
| base::checked_cast<wtf_size_t>(it.size())); |
| } while (it.Next()); |
| |
| // Release the lock while writing, so we don't block for too long. |
| parkable_image->lock_.unlock(); |
| |
| base::ElapsedTimer timer; |
| auto metadata = ParkableImageManager::Instance().data_allocator().Write( |
| vector.data(), vector.size()); |
| base::TimeDelta elapsed = timer.Elapsed(); |
| |
| // Acquire the lock again after writing. |
| parkable_image->lock_.lock(); |
| |
| parkable_image->on_disk_metadata_ = std::move(metadata); |
| |
| // Nothing to do if the write failed except return. Notably, we need to |
| // keep around the data for the ParkableImageImpl in this case. |
| if (!parkable_image->on_disk_metadata_) { |
| parkable_image->background_task_in_progress_ = false; |
| } else { |
| RecordWriteStatistics(parkable_image->on_disk_metadata_->size(), elapsed); |
| ParkableImageManager::Instance().RecordDiskWriteTime(elapsed); |
| PostCrossThreadTask( |
| *callback_task_runner, FROM_HERE, |
| CrossThreadBindOnce(&ParkableImageImpl::MaybeDiscardData, |
| std::move(parkable_image))); |
| } |
| } |
| |
| void ParkableImageImpl::MaybeDiscardData() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK(!is_below_min_parking_size()); |
| |
| MutexLocker lock(lock_); |
| DCHECK(on_disk_metadata_); |
| |
| background_task_in_progress_ = false; |
| |
| // If the image is now unparkable, we need to keep the data around. |
| // This can happen if, for example, in between the time we posted the task to |
| // discard the data and the time MaybeDiscardData is called, we've created a |
| // SegmentReader from |rw_buffer_|, since discarding the data would leave us |
| // with a dangling pointer in the SegmentReader. |
| if (CanParkNow()) |
| DiscardData(); |
| } |
| |
| void ParkableImageImpl::DiscardData() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| DCHECK(!is_locked()); |
| AsanUnpoisonBuffer(rw_buffer_.get()); |
| |
| rw_buffer_ = nullptr; |
| ParkableImageManager::Instance().OnWrittenToDisk(this); |
| } |
| |
| bool ParkableImageImpl::MaybePark() { |
| DCHECK(ParkableImageManager::IsParkableImagesToDiskEnabled()); |
| |
| MutexLocker lock(lock_); |
| |
| if (background_task_in_progress_) |
| return true; |
| |
| if (!CanParkNow()) |
| return false; |
| |
| if (on_disk_metadata_) { |
| DiscardData(); |
| return true; |
| } |
| |
| background_task_in_progress_ = true; |
| |
| // The writing is done on a background thread. We pass a TaskRunner from the |
| // current thread for when we have finished writing. |
| worker_pool::PostTask( |
| FROM_HERE, {base::MayBlock()}, |
| CrossThreadBindOnce(&ParkableImageImpl::WriteToDiskInBackground, |
| scoped_refptr<ParkableImageImpl>(this), |
| Thread::Current()->GetTaskRunner())); |
| return true; |
| } |
| |
| // static |
| size_t ParkableImageImpl::ReadFromDiskIntoBuffer( |
| DiskDataMetadata* on_disk_metadata, |
| void* buffer, |
| size_t capacity) { |
| size_t size = on_disk_metadata->size(); |
| DCHECK(size <= capacity); |
| ParkableImageManager::Instance().data_allocator().Read(*on_disk_metadata, |
| buffer); |
| return size; |
| } |
| |
| void ParkableImageImpl::Unpark() { |
| if (!is_on_disk()) { |
| AsanUnpoisonBuffer(rw_buffer_.get()); |
| return; |
| } |
| |
| DCHECK(ParkableImageManager::IsParkableImagesToDiskEnabled()); |
| |
| TRACE_EVENT1("blink", "ParkableImageImpl::Unpark", "size", size()); |
| |
| DCHECK(on_disk_metadata_); |
| |
| base::ElapsedTimer timer; |
| |
| DCHECK(!rw_buffer_); |
| rw_buffer_ = std::make_unique<RWBuffer>( |
| base::BindOnce(&ParkableImageImpl::ReadFromDiskIntoBuffer, |
| base::Unretained(on_disk_metadata_.get())), |
| size()); |
| |
| base::TimeDelta elapsed = timer.Elapsed(); |
| |
| RecordReadStatistics(on_disk_metadata_->size(), elapsed); |
| ParkableImageManager::Instance().RecordDiskReadTime(elapsed); |
| |
| ParkableImageManager::Instance().OnReadFromDisk(this); |
| |
| DCHECK(rw_buffer_); |
| } |
| |
| size_t ParkableImageImpl::size() const { |
| return size_; |
| } |
| |
| bool ParkableImageImpl::is_below_min_parking_size() const { |
| return size() < ParkableImageImpl::kMinSizeToPark; |
| } |
| |
| bool ParkableImageImpl::is_locked() const { |
| return lock_depth_ != 0; |
| } |
| |
| ParkableImage::ParkableImage(size_t offset) |
| : impl_(ParkableImageManager::Instance().CreateParkableImage(offset)) { |
| ParkableImageManager::Instance().Add(impl_.get()); |
| } |
| |
| ParkableImage::~ParkableImage() { |
| ParkableImageManager::Instance().DestroyParkableImage(std::move(impl_)); |
| } |
| |
| // static |
| scoped_refptr<ParkableImage> ParkableImage::Create(size_t initial_capacity) { |
| return base::MakeRefCounted<ParkableImage>(initial_capacity); |
| } |
| |
| size_t ParkableImage::size() const { |
| DCHECK(impl_); |
| return impl_->size(); |
| } |
| |
| bool ParkableImage::is_on_disk() const { |
| DCHECK(impl_); |
| return impl_->is_on_disk(); |
| } |
| |
| scoped_refptr<SegmentReader> ParkableImage::MakeROSnapshot() { |
| DCHECK(impl_); |
| DCHECK_CALLED_ON_VALID_THREAD(impl_->thread_checker_); |
| |
| if (base::FeatureList::IsEnabled(kUseParkableImageSegmentReader)) { |
| return SegmentReader::CreateFromParkableImage( |
| scoped_refptr<ParkableImage>(this)); |
| } else { |
| return impl_->GetROBufferSegmentReader(); |
| } |
| } |
| |
| void ParkableImage::Freeze() { |
| DCHECK(impl_); |
| impl_->Freeze(); |
| } |
| |
| scoped_refptr<SharedBuffer> ParkableImage::Data() { |
| DCHECK(impl_); |
| return impl_->Data(); |
| } |
| |
| void ParkableImage::Append(WTF::SharedBuffer* buffer, size_t offset) { |
| DCHECK(impl_); |
| impl_->Append(buffer, offset); |
| } |
| |
| void ParkableImage::LockData() { |
| DCHECK(impl_); |
| impl_->LockData(); |
| } |
| |
| void ParkableImage::UnlockData() { |
| DCHECK(impl_); |
| impl_->UnlockData(); |
| } |
| |
| } // namespace blink |