blob: 4f2ef627087af5ca04ccd3b5770f9725c962ebc9 [file] [log] [blame]
// Copyright 2021 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/graphics/parkable_image.h"
#include "base/feature_list.h"
#include "base/memory/asan_interface.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.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_copier_base.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/SkData.h"
#include "third_party/skia/include/core/SkRefCnt.h"
namespace blink {
BASE_FEATURE(kDelayParkingImages, base::FEATURE_ENABLED_BY_DEFAULT);
namespace {
void RecordReadStatistics(size_t size,
base::TimeDelta duration,
base::TimeDelta time_since_freeze) {
int throughput_mb_s = duration.is_zero()
? INT_MAX
: base::saturated_cast<int>(
size / duration.InSecondsF() / (1024 * 1024));
// 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 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);
}
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 {
auto data = *iter;
ASAN_POISON_MEMORY_REGION(data.data(), data.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 {
auto data = *iter;
ASAN_UNPOISON_MEMORY_REGION(data.data(), data.size());
} while (iter.Next());
#endif
}
// This should be used to make sure that the last reference to the |this| is
// decremented on the main thread (since that's where the destructor must
// run), for example by posting a task with this to the main thread.
void NotifyWriteToDiskFinished(scoped_refptr<ParkableImageImpl>) {
DCHECK(IsMainThread());
}
} // namespace
// ParkableImageSegmentReader
class ParkableImageSegmentReader : public SegmentReader {
public:
explicit ParkableImageSegmentReader(scoped_refptr<ParkableImage> image);
size_t size() const override;
base::span<const uint8_t> GetSomeData(size_t position) const override;
sk_sp<SkData> GetAsSkData() const override;
void LockData() override;
void UnlockData() override;
private:
~ParkableImageSegmentReader() override = default;
scoped_refptr<ParkableImage> parkable_image_;
size_t available_;
};
ParkableImageSegmentReader::ParkableImageSegmentReader(
scoped_refptr<ParkableImage> image)
: parkable_image_(std::move(image)), available_(parkable_image_->size()) {}
size_t ParkableImageSegmentReader::size() const {
return available_;
}
base::span<const uint8_t> ParkableImageSegmentReader::GetSomeData(
size_t position) const {
if (!parkable_image_) {
return {};
}
base::AutoLock lock(parkable_image_->impl_->lock_);
DCHECK(parkable_image_->impl_->is_locked());
RWBuffer::ROIter iter(parkable_image_->impl_->rw_buffer_.get(), available_);
size_t position_of_block = 0;
return RWBufferGetSomeData(iter, position_of_block, position);
}
sk_sp<SkData> ParkableImageSegmentReader::GetAsSkData() const {
if (!parkable_image_) {
return nullptr;
}
base::AutoLock lock(parkable_image_->impl_->lock_);
parkable_image_->impl_->Unpark();
RWBuffer::ROIter iter(parkable_image_->impl_->rw_buffer_.get(), available_);
if (!iter.HasNext()) { // No need to copy because the data is contiguous.
// We lock here so that we don't get a use-after-free. ParkableImage can
// not be parked while it is locked, so the buffer is valid for the whole
// lifetime of the SkData. We add the ref so that the ParkableImage has a
// longer limetime than the SkData.
parkable_image_->AddRef();
parkable_image_->LockData();
auto data = *iter;
return SkData::MakeWithProc(
data.data(), data.size(),
[](const void* ptr, void* context) -> void {
auto* parkable_image = static_cast<ParkableImage*>(context);
{
base::AutoLock lock(parkable_image->impl_->lock_);
parkable_image->UnlockData();
}
// Don't hold the mutex while we call |Release|, since |Release| can
// free the ParkableImage, if this is the last reference to it;
// Freeing the ParkableImage while the mutex is held causes a UAF when
// the dtor for base::AutoLock is called.
parkable_image->Release();
},
parkable_image_.get());
}
// Data is not contiguous so we need to copy.
return RWBufferCopyAsSkData(iter, available_);
}
void ParkableImageSegmentReader::LockData() {
base::AutoLock lock(parkable_image_->impl_->lock_);
parkable_image_->impl_->Unpark();
parkable_image_->LockData();
}
void ParkableImageSegmentReader::UnlockData() {
base::AutoLock lock(parkable_image_->impl_->lock_);
parkable_image_->UnlockData();
}
constexpr base::TimeDelta ParkableImageImpl::kParkingDelay;
void ParkableImageImpl::Append(SharedBuffer* buffer, size_t offset) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
base::AutoLock lock(lock_);
DCHECK(!is_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(base::as_byte_span(*it), remaining);
}
size_ = rw_buffer_->size();
}
scoped_refptr<SharedBuffer> ParkableImageImpl::Data() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
base::AutoLock 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(*it);
} while (it.Next());
return shared_buffer;
}
bool ParkableImageImpl::CanParkNow() const {
DCHECK(!is_on_disk());
return !TransientlyUnableToPark() && !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_);
base::AutoLock lock(lock_);
DCHECK(!is_frozen());
frozen_time_ = base::TimeTicks::Now();
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());
base::AutoLock lock(parkable_image->lock_);
DCHECK(parkable_image);
DCHECK(parkable_image->reserved_chunk_);
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.AppendSpan(*it);
} while (it.Next());
auto reserved_chunk = std::move(parkable_image->reserved_chunk_);
// Release the lock while writing, so we don't block for too long.
parkable_image->lock_.Release();
base::ElapsedTimer timer;
auto metadata = ParkableImageManager::Instance().data_allocator().Write(
std::move(reserved_chunk), base::as_byte_span(vector));
base::TimeDelta elapsed = timer.Elapsed();
// Acquire the lock again after writing.
parkable_image->lock_.Acquire();
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;
// This ensures that we don't destroy |this| on the background thread at
// the end of this function, if we happen to have the last reference to
// |this|.
//
// We cannot simply check the reference count here, since it may be
// changed racily on another thread, so posting a task is the only safe
// way to proceed.
PostCrossThreadTask(*callback_task_runner, FROM_HERE,
CrossThreadBindOnce(&NotifyWriteToDiskFinished,
std::move(parkable_image)));
} 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());
base::AutoLock 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::TransientlyUnableToPark() const {
if (base::FeatureList::IsEnabled(kDelayParkingImages)) {
// Most images are used only once, for the initial decode at render time.
// Since rendering can happen multiple seconds after the image load (e.g.
// if paint by a synchronous <script> earlier in the document), we instead
// wait up to kParkingDelay before parking an unused image.
return !is_frozen() ||
(base::TimeTicks::Now() - frozen_time_ <= kParkingDelay && !used_);
} else {
return !is_frozen();
}
}
bool ParkableImageImpl::MaybePark(
scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
DCHECK(IsMainThread());
base::AutoLock lock(lock_);
if (background_task_in_progress_)
return true;
if (!CanParkNow())
return false;
if (on_disk_metadata_) {
DiscardData();
return true;
}
auto reserved_chunk =
ParkableImageManager::Instance().data_allocator().TryReserveChunk(size());
if (!reserved_chunk) {
return false;
}
reserved_chunk_ = std::move(reserved_chunk);
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),
std::move(task_runner)));
return true;
}
// static
size_t ParkableImageImpl::ReadFromDiskIntoBuffer(
DiskDataMetadata* on_disk_metadata,
base::span<uint8_t> buffer) {
size_t size = on_disk_metadata->size();
DCHECK_LE(size, buffer.size());
ParkableImageManager::Instance().data_allocator().Read(*on_disk_metadata,
buffer);
return size;
}
void ParkableImageImpl::Unpark() {
// We mark the ParkableImage as having been read here, since any access to
// its data must first make sure it's not on disk.
used_ = true;
if (!is_on_disk()) {
AsanUnpoisonBuffer(rw_buffer_.get());
return;
}
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();
base::TimeDelta time_since_freeze = base::TimeTicks::Now() - frozen_time_;
RecordReadStatistics(on_disk_metadata_->size(), elapsed, time_since_freeze);
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_);
return CreateSegmentReader();
}
void ParkableImage::Freeze() {
DCHECK(impl_);
impl_->Freeze();
}
scoped_refptr<SharedBuffer> ParkableImage::Data() {
DCHECK(impl_);
return impl_->Data();
}
void ParkableImage::Append(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();
}
scoped_refptr<SegmentReader> ParkableImage::CreateSegmentReader() {
return base::MakeRefCounted<ParkableImageSegmentReader>(this);
}
} // namespace blink