| // 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 <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/platform/platform.h" |
| #include "third_party/blink/renderer/platform/bindings/parkable_string.h" |
| #include "third_party/blink/renderer/platform/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; |
| }; |
| |
| namespace { |
| |
| enum class CompressionMode { kDisabled, kBackground, kForeground }; |
| |
| // Compression mode. Foreground parking takes precedence over background, and |
| // both are mutually exclusive. |
| CompressionMode GetCompressionMode() { |
| if (base::FeatureList::IsEnabled(kCompressParkableStringsInForeground)) |
| return CompressionMode::kForeground; |
| if (base::FeatureList::IsEnabled(kCompressParkableStringsInBackground)) |
| return CompressionMode::kBackground; |
| return CompressionMode::kDisabled; |
| } |
| |
| class OnPurgeMemoryListener : public GarbageCollected<OnPurgeMemoryListener>, |
| public MemoryPressureListener { |
| USING_GARBAGE_COLLECTED_MIXIN(OnPurgeMemoryListener); |
| |
| void OnPurgeMemory() override { |
| // Memory pressure compression is enabled for all modes. |
| if (GetCompressionMode() == CompressionMode::kDisabled) |
| return; |
| ParkableStringManager::Instance().PurgeMemory(); |
| } |
| }; |
| |
| void RecordMemoryStatistics(const ParkableStringManager::Statistics& stats, |
| const std::string& suffix) { |
| base::UmaHistogramCounts100000("Memory.ParkableString.TotalSizeKb" + suffix, |
| stats.original_size / 1000); |
| base::UmaHistogramCounts100000( |
| "Memory.ParkableString.CompressedSizeKb" + suffix, |
| stats.compressed_size / 1000); |
| size_t savings = stats.compressed_original_size - stats.compressed_size; |
| base::UmaHistogramCounts100000("Memory.ParkableString.SavingsKb" + suffix, |
| savings / 1000); |
| |
| if (stats.compressed_original_size != 0) { |
| size_t ratio_percentage = |
| (100 * stats.compressed_size) / stats.compressed_original_size; |
| base::UmaHistogramPercentage( |
| "Memory.ParkableString.CompressionRatio" + suffix, ratio_percentage); |
| } |
| } |
| |
| } // namespace |
| |
| struct ParkableStringManager::ParkableStringImplHash { |
| STATIC_ONLY(ParkableStringImplHash); |
| |
| static unsigned GetHash(ParkableStringImpl* key) { return key->GetHash(); } |
| |
| static inline bool Equal(const ParkableStringImpl* a, |
| const ParkableStringImpl* b) { |
| return a->Equal(*b); |
| } |
| |
| static constexpr bool safe_to_compare_to_empty_or_deleted = false; |
| }; |
| |
| struct ParkableStringManager::ParkableStringImplTranslator { |
| STATIC_ONLY(ParkableStringImplTranslator); |
| |
| static unsigned GetHash(const scoped_refptr<StringImpl>& string) { |
| return string->GetHash(); |
| } |
| |
| static bool Equal(const ParkableStringImpl* parkable, |
| const scoped_refptr<StringImpl>& string) { |
| return parkable->Equal(string); |
| } |
| |
| static void Translate(ParkableStringImpl*& new_parkable, |
| scoped_refptr<StringImpl>&& string, |
| unsigned hash) { |
| new_parkable = new ParkableStringImpl( |
| std::move(string), ParkableStringImpl::ParkableState::kParkable); |
| DCHECK_EQ(hash, new_parkable->GetHash()); |
| } |
| }; |
| |
| // 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() { |
| DCHECK(IsMainThread()); |
| DEFINE_STATIC_LOCAL(ParkableStringManager, instance, ()); |
| return instance; |
| } |
| |
| ParkableStringManager::~ParkableStringManager() = default; |
| |
| void ParkableStringManager::SetRendererBackgrounded(bool backgrounded) { |
| DCHECK(IsMainThread()); |
| backgrounded_ = backgrounded; |
| |
| if (GetCompressionMode() != CompressionMode::kBackground) |
| return; |
| |
| if (backgrounded_) { |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner = |
| Thread::Current()->GetTaskRunner(); |
| DCHECK(task_runner); |
| task_runner->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&ParkableStringManager::ParkAllIfRendererBackgrounded, |
| base::Unretained(this), |
| ParkableStringImpl::ParkingMode::kAlways), |
| base::TimeDelta::FromSeconds(kParkingDelayInSeconds)); |
| // We only want to record statistics in the following case: a foreground tab |
| // goes to background, and stays in background until the stats are recorded, |
| // to make analysis simpler. |
| // |
| // To that end: |
| // 1. Don't post a recording task if one has been posted and hasn't run yet. |
| // 2. Any background -> foreground transition between now and the |
| // recording task running cancels the task. |
| // |
| // Also drop strings that can be dropped cheaply in this task, to prevent |
| // used-once strings from increasing memory usage. |
| if (!waiting_to_record_stats_) { |
| task_runner->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&ParkableStringManager:: |
| DropStringsWithCompressedDataAndRecordStatistics, |
| base::Unretained(this)), |
| base::TimeDelta::FromSeconds(kParkingDelayInSeconds + |
| kStatisticsRecordingDelayInSeconds)); |
| waiting_to_record_stats_ = true; |
| should_record_stats_ = true; |
| } |
| } else { |
| // See (2) above. |
| if (waiting_to_record_stats_) { |
| should_record_stats_ = false; |
| } |
| } |
| } |
| |
| bool ParkableStringManager::IsRendererBackgrounded() const { |
| DCHECK(IsMainThread()); |
| return backgrounded_; |
| } |
| |
| bool ParkableStringManager::OnMemoryDump( |
| base::trace_event::ProcessMemoryDump* pmd) { |
| DCHECK(IsMainThread()); |
| base::trace_event::MemoryAllocatorDump* dump = |
| pmd->CreateAllocatorDump("parkable_strings"); |
| |
| 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); |
| |
| 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() && |
| GetCompressionMode() != CompressionMode::kDisabled; |
| } |
| |
| scoped_refptr<ParkableStringImpl> ParkableStringManager::Add( |
| scoped_refptr<StringImpl>&& string) { |
| DCHECK(IsMainThread()); |
| |
| ScheduleAgingTaskIfNeeded(); |
| |
| scoped_refptr<StringImpl> string_ptr(string); |
| // Hit in either the unparked or parked strings. |
| auto it = unparked_strings_ |
| .Find<ParkableStringImplTranslator, scoped_refptr<StringImpl>>( |
| string_ptr); |
| if (it != unparked_strings_.end()) |
| return *it; |
| |
| // This is an "expensive hit", as we unparked then discarded the unparked |
| // representation of a string. |
| // |
| // If this is problematic, we can unpark the string for "free" (since the |
| // incoming) string is unparked and has the same content, or change the |
| // interface. Note that at the same time the hit means that we avoided an |
| // expensive compression task. |
| it = parked_strings_ |
| .Find<ParkableStringImplTranslator, scoped_refptr<StringImpl>>( |
| string_ptr); |
| if (it != parked_strings_.end()) |
| return *it; |
| |
| // No hit, new unparked string. |
| auto add_result = |
| unparked_strings_.AddWithTranslator<ParkableStringImplTranslator, |
| scoped_refptr<StringImpl>>( |
| std::move(string_ptr)); |
| DCHECK(add_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_ && |
| GetCompressionMode() == CompressionMode::kForeground) { |
| 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 base::AdoptRef(*add_result.stored_value); |
| } |
| |
| void ParkableStringManager::Remove(ParkableStringImpl* string) { |
| DCHECK(IsMainThread()); |
| DCHECK(string->may_be_parked()); |
| if (string->is_parked()) { |
| auto it = parked_strings_.find(string); |
| DCHECK(it != parked_strings_.end()); |
| parked_strings_.erase(it); |
| } else { |
| auto it = unparked_strings_.find(string); |
| DCHECK(it != unparked_strings_.end()); |
| unparked_strings_.erase(it); |
| } |
| } |
| |
| void ParkableStringManager::OnParked(ParkableStringImpl* newly_parked_string) { |
| DCHECK(IsMainThread()); |
| DCHECK(newly_parked_string->may_be_parked()); |
| DCHECK(newly_parked_string->is_parked()); |
| auto it = unparked_strings_.find(newly_parked_string); |
| DCHECK(it != unparked_strings_.end()); |
| DCHECK_EQ(*it, newly_parked_string); |
| unparked_strings_.erase(it); |
| parked_strings_.insert(newly_parked_string); |
| } |
| |
| void ParkableStringManager::OnUnparked(ParkableStringImpl* was_parked_string) { |
| DCHECK(IsMainThread()); |
| DCHECK(was_parked_string->may_be_parked()); |
| DCHECK(!was_parked_string->is_parked()); |
| auto it = parked_strings_.find(was_parked_string); |
| DCHECK(it != parked_strings_.end()); |
| DCHECK_EQ(*it, was_parked_string); |
| parked_strings_.erase(it); |
| unparked_strings_.insert(was_parked_string); |
| ScheduleAgingTaskIfNeeded(); |
| } |
| |
| void ParkableStringManager::RecordUnparkingTime( |
| base::TimeDelta unparking_time) { |
| total_unparking_time_ += unparking_time; |
| } |
| |
| void ParkableStringManager::ParkAll(ParkableStringImpl::ParkingMode mode) { |
| DCHECK(IsMainThread()); |
| DCHECK_NE(CompressionMode::kDisabled, GetCompressionMode()); |
| |
| size_t total_size = 0; |
| for (ParkableStringImpl* str : parked_strings_) |
| total_size += str->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. |
| WTF::Vector<ParkableStringImpl*> unparked = GetUnparkedStrings(); |
| |
| for (ParkableStringImpl* str : unparked) { |
| str->Park(mode); |
| total_size += str->CharactersSizeInBytes(); |
| } |
| |
| // Only collect stats for "full" parking calls in background. |
| if (mode == ParkableStringImpl::ParkingMode::kAlways && |
| IsRendererBackgrounded()) { |
| size_t total_size_kb = total_size / 1000; |
| UMA_HISTOGRAM_COUNTS_100000("Memory.MovableStringsTotalSizeKb", |
| total_size_kb); |
| UMA_HISTOGRAM_COUNTS_1000("Memory.MovableStringsCount", Size()); |
| } |
| } |
| |
| void ParkableStringManager::ParkAllIfRendererBackgrounded( |
| ParkableStringImpl::ParkingMode mode) { |
| DCHECK(IsMainThread()); |
| |
| if (IsRendererBackgrounded()) |
| ParkAll(mode); |
| } |
| |
| size_t ParkableStringManager::Size() const { |
| return parked_strings_.size() + unparked_strings_.size(); |
| } |
| |
| void ParkableStringManager::DropStringsWithCompressedDataAndRecordStatistics() { |
| DCHECK(IsMainThread()); |
| DCHECK_EQ(CompressionMode::kBackground, GetCompressionMode()); |
| DCHECK(waiting_to_record_stats_); |
| waiting_to_record_stats_ = false; |
| if (!should_record_stats_) |
| return; |
| // See |SetRendererBackgrounded()|, is |should_record_stats_| is true then the |
| // renderer is still backgrounded_. |
| DCHECK(IsRendererBackgrounded()); |
| |
| // We are in the background, drop all the ParkableStrings we can without |
| // costing any CPU (as we already have the compressed representation). |
| ParkAllIfRendererBackgrounded( |
| ParkableStringImpl::ParkingMode::kIfCompressedDataExists); |
| |
| Statistics stats = ComputeStatistics(); |
| RecordMemoryStatistics(stats, ""); |
| } |
| |
| 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(); |
| RecordMemoryStatistics(stats, ".5min"); |
| } |
| |
| void ParkableStringManager::AgeStringsAndPark() { |
| if (GetCompressionMode() != CompressionMode::kForeground) |
| return; |
| |
| TRACE_EVENT0("blink", "ParkableStringManager::AgeStringsAndPark"); |
| has_pending_aging_task_ = false; |
| |
| WTF::Vector<ParkableStringImpl*> unparked = GetUnparkedStrings(); |
| bool can_make_progress = false; |
| for (ParkableStringImpl* str : unparked) { |
| 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() && can_make_progress; |
| if (reschedule) |
| ScheduleAgingTaskIfNeeded(); |
| } |
| |
| void ParkableStringManager::ScheduleAgingTaskIfNeeded() { |
| if (GetCompressionMode() != CompressionMode::kForeground) |
| 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_NE(CompressionMode::kDisabled, GetCompressionMode()); |
| |
| ParkAll(ParkableStringImpl::ParkingMode::kAlways); |
| // Critical memory pressure: drop compressed data for strings that we cannot |
| // park now. |
| // |
| // After |ParkAll()| has been called, parkable strings have either been parked |
| // synchronously (and no longer in |unparked_strings_|), or being parked and |
| // purging is a no-op. |
| if (!IsRendererBackgrounded()) { |
| for (ParkableStringImpl* str : unparked_strings_) |
| str->PurgeMemory(); |
| } |
| } |
| |
| Vector<ParkableStringImpl*> ParkableStringManager::GetUnparkedStrings() const { |
| WTF::Vector<ParkableStringImpl*> unparked; |
| unparked.ReserveCapacity(unparked_strings_.size()); |
| for (ParkableStringImpl* str : unparked_strings_) |
| unparked.push_back(str); |
| |
| return unparked; |
| } |
| |
| ParkableStringManager::Statistics ParkableStringManager::ComputeStatistics() |
| const { |
| ParkableStringManager::Statistics stats = {}; |
| |
| for (ParkableStringImpl* str : unparked_strings_) { |
| size_t size = str->CharactersSizeInBytes(); |
| stats.original_size += size; |
| stats.uncompressed_size += size; |
| stats.metadata_size += sizeof(ParkableStringImpl); |
| |
| if (str->has_compressed_data()) |
| stats.overhead_size += str->compressed_size(); |
| } |
| |
| for (ParkableStringImpl* str : parked_strings_) { |
| size_t size = str->CharactersSizeInBytes(); |
| stats.compressed_original_size += size; |
| stats.original_size += size; |
| stats.compressed_size += str->compressed_size(); |
| stats.metadata_size += sizeof(ParkableStringImpl); |
| } |
| |
| 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() { |
| backgrounded_ = false; |
| waiting_to_record_stats_ = false; |
| has_pending_aging_task_ = false; |
| should_record_stats_ = 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(); |
| unparked_strings_.clear(); |
| parked_strings_.clear(); |
| } |
| |
| ParkableStringManager::ParkableStringManager() |
| : backgrounded_(false), |
| waiting_to_record_stats_(false), |
| has_pending_aging_task_(false), |
| should_record_stats_(false), |
| has_posted_unparking_time_accounting_task_(false), |
| did_register_memory_pressure_listener_(false), |
| total_unparking_time_(), |
| total_parking_thread_time_(), |
| unparked_strings_(), |
| parked_strings_() {} |
| |
| } // namespace blink |