blob: 1879d7527fc5fb63514d318c3bfa6e7a8ffd14e9 [file] [log] [blame]
// Copyright 2018 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/bindings/parkable_string_manager.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/macros.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/single_thread_task_runner.h"
#include "base/trace_event/memory_allocator_dump.h"
#include "base/trace_event/process_memory_dump.h"
#include "base/trace_event/trace_event.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/bindings/parkable_string.h"
#include "third_party/blink/renderer/platform/disk_data_allocator.h"
#include "third_party/blink/renderer/platform/instrumentation/memory_pressure_listener.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"
namespace blink {
struct ParkableStringManager::Statistics {
size_t original_size;
size_t uncompressed_size;
size_t compressed_original_size;
size_t compressed_size;
size_t metadata_size;
size_t overhead_size;
size_t total_size;
int64_t savings_size;
size_t on_disk_size;
};
namespace {
bool CompressionEnabled() {
return base::FeatureList::IsEnabled(features::kCompressParkableStrings);
}
class OnPurgeMemoryListener : public GarbageCollected<OnPurgeMemoryListener>,
public MemoryPressureListener {
void OnPurgeMemory() override {
if (!CompressionEnabled()) {
return;
}
ParkableStringManager::Instance().PurgeMemory();
}
};
Vector<ParkableStringImpl*> EnumerateStrings(
const ParkableStringManager::StringMap& strings) {
WTF::Vector<ParkableStringImpl*> all_strings;
all_strings.ReserveCapacity(strings.size());
for (const auto& kv : strings)
all_strings.push_back(kv.value);
return all_strings;
}
void MoveString(ParkableStringImpl* string,
ParkableStringManager::StringMap* from,
ParkableStringManager::StringMap* to) {
auto it = from->find(string->digest());
DCHECK(it != from->end());
DCHECK_EQ(it->value, string);
from->erase(it);
auto insert_result = to->insert(string->digest(), string);
DCHECK(insert_result.is_new_entry);
}
} // namespace
const char* ParkableStringManager::kAllocatorDumpName = "parkable_strings";
// Compares not the pointers, but the arrays. Uses pointers to save space.
struct ParkableStringManager::SecureDigestHash {
STATIC_ONLY(SecureDigestHash);
static unsigned GetHash(
const ParkableStringImpl::SecureDigest* const digest) {
// The first bytes of the hash are as good as anything else.
return *reinterpret_cast<const unsigned*>(digest->data());
}
static inline bool Equal(const ParkableStringImpl::SecureDigest* const a,
const ParkableStringImpl::SecureDigest* const b) {
return a == b ||
std::equal(a->data(), a->data() + ParkableStringImpl::kDigestSize,
b->data());
}
static constexpr bool safe_to_compare_to_empty_or_deleted = false;
};
// static
ParkableStringManagerDumpProvider*
ParkableStringManagerDumpProvider::Instance() {
static ParkableStringManagerDumpProvider instance;
return &instance;
}
bool ParkableStringManagerDumpProvider::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* pmd) {
return ParkableStringManager::Instance().OnMemoryDump(pmd);
}
ParkableStringManagerDumpProvider::~ParkableStringManagerDumpProvider() =
default;
ParkableStringManagerDumpProvider::ParkableStringManagerDumpProvider() =
default;
ParkableStringManager& ParkableStringManager::Instance() {
DEFINE_STATIC_LOCAL(ParkableStringManager, instance, ());
return instance;
}
ParkableStringManager::~ParkableStringManager() = default;
bool ParkableStringManager::OnMemoryDump(
base::trace_event::ProcessMemoryDump* pmd) {
DCHECK(IsMainThread());
base::trace_event::MemoryAllocatorDump* dump =
pmd->CreateAllocatorDump(kAllocatorDumpName);
Statistics stats = ComputeStatistics();
dump->AddScalar("size", "bytes", stats.total_size);
dump->AddScalar("original_size", "bytes", stats.original_size);
dump->AddScalar("uncompressed_size", "bytes", stats.uncompressed_size);
dump->AddScalar("compressed_size", "bytes", stats.compressed_size);
dump->AddScalar("metadata_size", "bytes", stats.metadata_size);
dump->AddScalar("overhead_size", "bytes", stats.overhead_size);
// Has to be uint64_t.
dump->AddScalar("savings_size", "bytes",
stats.savings_size > 0 ? stats.savings_size : 0);
dump->AddScalar("on_disk_size", "bytes", stats.on_disk_size);
dump->AddScalar("on_disk_footprint", "bytes",
data_allocator().disk_footprint());
dump->AddScalar("on_disk_free_chunks", "bytes",
data_allocator().free_chunks_size());
pmd->AddSuballocation(dump->guid(),
WTF::Partitions::kAllocatedObjectPoolName);
return true;
}
// static
bool ParkableStringManager::ShouldPark(const StringImpl& string) {
// Don't attempt to park strings smaller than this size.
static constexpr unsigned int kSizeThreshold = 10000;
// TODO(lizeb): Consider parking non-main thread strings.
return string.length() > kSizeThreshold && IsMainThread() &&
CompressionEnabled();
}
scoped_refptr<ParkableStringImpl> ParkableStringManager::Add(
scoped_refptr<StringImpl>&& string) {
DCHECK(IsMainThread());
ScheduleAgingTaskIfNeeded();
auto string_impl = string;
auto digest = ParkableStringImpl::HashString(string_impl.get());
DCHECK(digest.get());
auto it = unparked_strings_.find(digest.get());
if (it != unparked_strings_.end())
return it->value;
it = parked_strings_.find(digest.get());
if (it != parked_strings_.end())
return it->value;
it = on_disk_strings_.find(digest.get());
if (it != on_disk_strings_.end())
return it->value;
// No hit, new unparked string.
auto new_parkable = ParkableStringImpl::MakeParkable(std::move(string_impl),
std::move(digest));
auto insert_result =
unparked_strings_.insert(new_parkable->digest(), new_parkable.get());
DCHECK(insert_result.is_new_entry);
// Lazy registration because registering too early can cause crashes on Linux,
// see crbug.com/930117, and registering without any strings is pointless
// anyway.
if (!did_register_memory_pressure_listener_) {
// No need to ever unregister, as the only ParkableStringManager instance
// lives forever.
MemoryPressureListenerRegistry::Instance().RegisterClient(
MakeGarbageCollected<OnPurgeMemoryListener>());
did_register_memory_pressure_listener_ = true;
}
if (!has_posted_unparking_time_accounting_task_) {
scoped_refptr<base::SingleThreadTaskRunner> task_runner =
Thread::Current()->GetTaskRunner();
DCHECK(task_runner);
task_runner->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ParkableStringManager::RecordStatisticsAfter5Minutes,
base::Unretained(this)),
base::TimeDelta::FromMinutes(5));
has_posted_unparking_time_accounting_task_ = true;
}
return new_parkable;
}
void ParkableStringManager::Remove(ParkableStringImpl* string) {
DCHECK(IsMainThread());
DCHECK(string->may_be_parked());
DCHECK(string->digest());
StringMap* map = nullptr;
if (string->is_on_disk())
map = &on_disk_strings_;
else if (string->is_parked())
map = &parked_strings_;
else
map = &unparked_strings_;
auto it = map->find(string->digest());
DCHECK(it != map->end());
map->erase(it);
}
void ParkableStringManager::OnParked(ParkableStringImpl* newly_parked_string) {
DCHECK(IsMainThread());
DCHECK(newly_parked_string->may_be_parked());
DCHECK(newly_parked_string->is_parked());
MoveString(newly_parked_string, &unparked_strings_, &parked_strings_);
}
void ParkableStringManager::OnWrittenToDisk(
ParkableStringImpl* newly_written_string) {
DCHECK(IsMainThread());
DCHECK(newly_written_string->may_be_parked());
DCHECK(newly_written_string->is_on_disk());
MoveString(newly_written_string, &parked_strings_, &on_disk_strings_);
}
void ParkableStringManager::OnReadFromDisk(ParkableStringImpl* string) {
DCHECK(IsMainThread());
DCHECK(string->may_be_parked());
DCHECK(string->is_on_disk());
MoveString(string, &on_disk_strings_, &parked_strings_);
// Does not call ScheduleAgingTaskIfNeeded() since OnUnparked() will be called
// when the string is unparked (in the same main thread task).
}
void ParkableStringManager::OnUnparked(ParkableStringImpl* was_parked_string) {
DCHECK(IsMainThread());
DCHECK(was_parked_string->may_be_parked());
DCHECK(!was_parked_string->is_parked());
MoveString(was_parked_string, &parked_strings_, &unparked_strings_);
ScheduleAgingTaskIfNeeded();
}
void ParkableStringManager::ParkAll(ParkableStringImpl::ParkingMode mode) {
DCHECK(IsMainThread());
DCHECK(CompressionEnabled());
size_t total_size = 0;
for (const auto& kv : parked_strings_)
total_size += kv.value->CharactersSizeInBytes();
// Parking may be synchronous, need to copy values first.
// In case of synchronous parking, |ParkableStringImpl::Park()| calls
// |OnParked()|, which moves the string from |unparked_strings_|
// to |parked_strings_|, hence the need to copy values first.
//
// Efficiency: In practice, either we are parking strings for the first time,
// and |unparked_strings_| can contain a few 10s of strings (and we will
// trigger expensive compression), or this is a subsequent one, and
// |unparked_strings_| will have few entries.
auto unparked = EnumerateStrings(unparked_strings_);
for (ParkableStringImpl* str : unparked) {
str->Park(mode);
total_size += str->CharactersSizeInBytes();
}
}
size_t ParkableStringManager::Size() const {
DCHECK(IsMainThread());
return parked_strings_.size() + unparked_strings_.size();
}
void ParkableStringManager::RecordStatisticsAfter5Minutes() const {
base::UmaHistogramTimes("Memory.ParkableString.MainThreadTime.5min",
total_unparking_time_);
if (base::ThreadTicks::IsSupported()) {
base::UmaHistogramTimes("Memory.ParkableString.ParkingThreadTime.5min",
total_parking_thread_time_);
}
Statistics stats = ComputeStatistics();
base::UmaHistogramCounts100000("Memory.ParkableString.TotalSizeKb.5min",
stats.original_size / 1000);
base::UmaHistogramCounts100000("Memory.ParkableString.CompressedSizeKb.5min",
stats.compressed_size / 1000);
size_t savings = stats.compressed_original_size - stats.compressed_size;
base::UmaHistogramCounts100000("Memory.ParkableString.SavingsKb.5min",
savings / 1000);
if (stats.compressed_original_size != 0) {
size_t ratio_percentage =
(100 * stats.compressed_size) / stats.compressed_original_size;
base::UmaHistogramPercentageObsoleteDoNotUse(
"Memory.ParkableString.CompressionRatio.5min", ratio_percentage);
}
// May not be usable, e.g. Incognito, permission or write failure.
if (features::IsParkableStringsToDiskEnabled()) {
base::UmaHistogramBoolean("Memory.ParkableString.DiskIsUsable.5min",
data_allocator().may_write());
}
// These metrics only make sense if the disk allocator is used.
if (data_allocator().may_write()) {
base::UmaHistogramTimes("Memory.ParkableString.DiskWriteTime.5min",
total_disk_write_time_);
base::UmaHistogramTimes("Memory.ParkableString.DiskReadTime.5min",
total_disk_read_time_);
base::UmaHistogramCounts100000(
"Memory.ParkableString.MemorySavingsKb.5min",
std::max(0, static_cast<int>(stats.savings_size)) / 1000);
base::UmaHistogramCounts100000("Memory.ParkableString.OnDiskSizeKb.5min",
stats.on_disk_size / 1000);
base::UmaHistogramCounts100000(
"Memory.ParkableString.OnDiskFootprintKb.5min",
static_cast<int>(data_allocator().disk_footprint()) / 1000);
}
}
void ParkableStringManager::AgeStringsAndPark() {
DCHECK(CompressionEnabled());
TRACE_EVENT0("blink", "ParkableStringManager::AgeStringsAndPark");
has_pending_aging_task_ = false;
auto unparked = EnumerateStrings(unparked_strings_);
auto parked = EnumerateStrings(parked_strings_);
bool can_make_progress = false;
for (ParkableStringImpl* str : unparked) {
if (str->MaybeAgeOrParkString() ==
ParkableStringImpl::AgeOrParkResult::kSuccessOrTransientFailure) {
can_make_progress = true;
}
}
for (ParkableStringImpl* str : parked) {
if (str->MaybeAgeOrParkString() ==
ParkableStringImpl::AgeOrParkResult::kSuccessOrTransientFailure) {
can_make_progress = true;
}
}
// Some strings will never be parkable because there are lasting external
// references to them. Don't endlessely reschedule the aging task if we are
// not making progress (that is, no new string was either aged or parked).
//
// This ensures that the tasks will stop getting scheduled, assuming that
// the renderer is otherwise idle. Note that we cannot use "idle" tasks as
// we need to age and park strings after the renderer becomes idle, meaning
// that this has to run when the idle tasks are not. As a consequence, it
// is important to make sure that this will not reschedule tasks forever.
bool reschedule =
(!unparked_strings_.IsEmpty() || !parked_strings_.IsEmpty()) &&
can_make_progress;
if (reschedule)
ScheduleAgingTaskIfNeeded();
}
void ParkableStringManager::ScheduleAgingTaskIfNeeded() {
if (!CompressionEnabled())
return;
if (has_pending_aging_task_)
return;
scoped_refptr<base::SingleThreadTaskRunner> task_runner =
Thread::Current()->GetTaskRunner();
task_runner->PostDelayedTask(
FROM_HERE,
base::BindOnce(&ParkableStringManager::AgeStringsAndPark,
base::Unretained(this)),
base::TimeDelta::FromSeconds(kAgingIntervalInSeconds));
has_pending_aging_task_ = true;
}
void ParkableStringManager::PurgeMemory() {
DCHECK(IsMainThread());
DCHECK(CompressionEnabled());
ParkAll(ParkableStringImpl::ParkingMode::kCompress);
}
ParkableStringManager::Statistics ParkableStringManager::ComputeStatistics()
const {
ParkableStringManager::Statistics stats = {};
// The digest has an inline capacity set to the digest size, hence sizeof() is
// accurate.
constexpr size_t kParkableStringImplActualSize =
sizeof(ParkableStringImpl) + sizeof(ParkableStringImpl::ParkableMetadata);
for (const auto& kv : unparked_strings_) {
ParkableStringImpl* str = kv.value;
size_t size = str->CharactersSizeInBytes();
stats.original_size += size;
stats.uncompressed_size += size;
stats.metadata_size += kParkableStringImplActualSize;
if (str->has_compressed_data())
stats.overhead_size += str->compressed_size();
if (str->has_on_disk_data())
stats.on_disk_size += str->on_disk_size();
// Since ParkableStringManager wants to have a finer breakdown of memory
// footprint, this doesn't directly use
// |ParkableStringImpl::MemoryFootprintForDump()|. However we want the two
// computations to be consistent, hence the DCHECK().
size_t memory_footprint =
(str->has_compressed_data() ? str->compressed_size() : 0) + size +
kParkableStringImplActualSize;
DCHECK_EQ(memory_footprint, str->MemoryFootprintForDump());
}
for (const auto& kv : parked_strings_) {
ParkableStringImpl* str = kv.value;
size_t size = str->CharactersSizeInBytes();
stats.compressed_original_size += size;
stats.original_size += size;
stats.compressed_size += str->compressed_size();
stats.metadata_size += kParkableStringImplActualSize;
if (str->has_on_disk_data())
stats.on_disk_size += str->on_disk_size();
// See comment above.
size_t memory_footprint =
str->compressed_size() + kParkableStringImplActualSize;
DCHECK_EQ(memory_footprint, str->MemoryFootprintForDump());
}
for (const auto& kv : on_disk_strings_) {
ParkableStringImpl* str = kv.value;
size_t size = str->CharactersSizeInBytes();
stats.original_size += size;
stats.metadata_size += kParkableStringImplActualSize;
stats.on_disk_size += str->on_disk_size();
}
stats.total_size = stats.uncompressed_size + stats.compressed_size +
stats.metadata_size + stats.overhead_size;
size_t memory_footprint = stats.compressed_size + stats.uncompressed_size +
stats.metadata_size + stats.overhead_size;
stats.savings_size =
stats.original_size - static_cast<int64_t>(memory_footprint);
return stats;
}
void ParkableStringManager::ResetForTesting() {
has_pending_aging_task_ = false;
has_posted_unparking_time_accounting_task_ = false;
did_register_memory_pressure_listener_ = false;
total_unparking_time_ = base::TimeDelta();
total_parking_thread_time_ = base::TimeDelta();
total_disk_read_time_ = base::TimeDelta();
total_disk_write_time_ = base::TimeDelta();
unparked_strings_.clear();
parked_strings_.clear();
on_disk_strings_.clear();
allocator_for_testing_ = nullptr;
}
ParkableStringManager::ParkableStringManager()
: has_pending_aging_task_(false),
has_posted_unparking_time_accounting_task_(false),
did_register_memory_pressure_listener_(false),
allocator_for_testing_(nullptr) {}
} // namespace blink