| // Copyright (c) 2012 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 "content/browser/dom_storage/dom_storage_context_impl.h" |
| |
| #include <inttypes.h> |
| #include <stddef.h> |
| #include <stdlib.h> |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/files/file_util.h" |
| #include "base/guid.h" |
| #include "base/location.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/system/sys_info.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/memory_dump_manager.h" |
| #include "base/trace_event/process_memory_dump.h" |
| #include "content/browser/dom_storage/dom_storage_area.h" |
| #include "content/browser/dom_storage/dom_storage_database.h" |
| #include "content/browser/dom_storage/dom_storage_namespace.h" |
| #include "content/browser/dom_storage/dom_storage_task_runner.h" |
| #include "content/browser/dom_storage/session_storage_database.h" |
| #include "content/common/dom_storage/dom_storage_types.h" |
| #include "content/public/browser/dom_storage_context.h" |
| #include "content/public/browser/session_storage_usage_info.h" |
| #include "content/public/browser/storage_usage_info.h" |
| #include "storage/browser/quota/special_storage_policy.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| namespace { |
| |
| // Limits on the cache size and number of areas in memory, over which the areas |
| // are purged. |
| #if defined(OS_ANDROID) |
| const unsigned kMaxDomStorageAreaCount = 20; |
| const size_t kMaxDomStorageCacheSize = 2 * 1024 * 1024; |
| #else |
| const unsigned kMaxDomStorageAreaCount = 100; |
| const size_t kMaxDomStorageCacheSize = 20 * 1024 * 1024; |
| #endif |
| |
| const int kSessionStoraceScavengingSeconds = 60; |
| |
| // Aggregates statistics from all the namespaces. |
| DOMStorageNamespace::UsageStatistics GetTotalNamespaceStatistics( |
| const DOMStorageContextImpl::StorageNamespaceMap& namespace_map) { |
| DOMStorageNamespace::UsageStatistics total_stats = {0}; |
| for (const auto& it : namespace_map) { |
| DOMStorageNamespace::UsageStatistics stats = |
| it.second->GetUsageStatistics(); |
| total_stats.total_cache_size += stats.total_cache_size; |
| total_stats.total_area_count += stats.total_area_count; |
| total_stats.inactive_area_count += stats.inactive_area_count; |
| } |
| return total_stats; |
| } |
| |
| } // namespace |
| |
| DOMStorageContextImpl::DOMStorageContextImpl( |
| const base::FilePath& sessionstorage_directory, |
| storage::SpecialStoragePolicy* special_storage_policy, |
| scoped_refptr<DOMStorageTaskRunner> task_runner) |
| : sessionstorage_directory_(sessionstorage_directory), |
| task_runner_(std::move(task_runner)), |
| is_shutdown_(false), |
| force_keep_session_state_(false), |
| special_storage_policy_(special_storage_policy), |
| scavenging_started_(false), |
| is_low_end_device_(base::SysInfo::IsLowEndDevice()) { |
| // Tests may run without task runners. |
| if (task_runner_) { |
| // Registering dump provider is safe even outside the task runner. |
| base::trace_event::MemoryDumpManager::GetInstance() |
| ->RegisterDumpProviderWithSequencedTaskRunner( |
| this, "DOMStorage", task_runner_->GetSequencedTaskRunner( |
| DOMStorageTaskRunner::PRIMARY_SEQUENCE), |
| base::trace_event::MemoryDumpProvider::Options()); |
| } |
| } |
| |
| DOMStorageContextImpl::~DOMStorageContextImpl() { |
| DCHECK(is_shutdown_); |
| if (session_storage_database_.get()) { |
| // SessionStorageDatabase shouldn't be deleted right away: deleting it will |
| // potentially involve waiting in leveldb::DBImpl::~DBImpl, and waiting |
| // shouldn't happen on this thread. |
| SessionStorageDatabase* to_release = session_storage_database_.get(); |
| to_release->AddRef(); |
| session_storage_database_ = nullptr; |
| task_runner_->PostShutdownBlockingTask( |
| FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE, |
| base::BindOnce(&SessionStorageDatabase::Release, |
| base::Unretained(to_release))); |
| } |
| } |
| |
| DOMStorageNamespace* DOMStorageContextImpl::GetStorageNamespace( |
| const std::string& namespace_id) { |
| if (is_shutdown_) |
| return nullptr; |
| auto found = namespaces_.find(namespace_id); |
| if (found == namespaces_.end()) |
| return nullptr; |
| return found->second.get(); |
| } |
| |
| void DOMStorageContextImpl::GetSessionStorageUsage( |
| std::vector<SessionStorageUsageInfo>* infos) { |
| if (!session_storage_database_.get()) { |
| for (const auto& entry : namespaces_) { |
| std::vector<url::Origin> origins; |
| entry.second->GetOriginsWithAreas(&origins); |
| for (const url::Origin& origin : origins) { |
| SessionStorageUsageInfo info; |
| info.namespace_id = entry.second->namespace_id(); |
| info.origin = origin.GetURL(); |
| infos->push_back(info); |
| } |
| } |
| return; |
| } |
| |
| std::map<std::string, std::vector<url::Origin>> namespaces_and_origins; |
| session_storage_database_->ReadNamespacesAndOrigins( |
| &namespaces_and_origins); |
| for (auto it = namespaces_and_origins.cbegin(); |
| it != namespaces_and_origins.cend(); ++it) { |
| for (auto origin_it = it->second.cbegin(); origin_it != it->second.cend(); |
| ++origin_it) { |
| SessionStorageUsageInfo info; |
| info.namespace_id = it->first; |
| info.origin = origin_it->GetURL(); |
| infos->push_back(info); |
| } |
| } |
| } |
| |
| void DOMStorageContextImpl::DeleteSessionStorage( |
| const SessionStorageUsageInfo& usage_info) { |
| DCHECK(!is_shutdown_); |
| DOMStorageNamespace* dom_storage_namespace = nullptr; |
| |
| auto it = namespaces_.find(usage_info.namespace_id); |
| if (it != namespaces_.end()) { |
| dom_storage_namespace = it->second.get(); |
| } else { |
| CreateSessionNamespace(usage_info.namespace_id); |
| dom_storage_namespace = GetStorageNamespace(usage_info.namespace_id); |
| } |
| dom_storage_namespace->DeleteSessionStorageOrigin( |
| url::Origin::Create(usage_info.origin)); |
| // Synthesize a 'cleared' event if the area is open so CachedAreas in |
| // renderers get emptied out too. |
| DOMStorageArea* area = dom_storage_namespace->GetOpenStorageArea( |
| url::Origin::Create(usage_info.origin)); |
| if (area) |
| NotifyAreaCleared(area, usage_info.origin); |
| } |
| |
| void DOMStorageContextImpl::Flush() { |
| for (auto& entry : namespaces_) |
| entry.second->Flush(); |
| } |
| |
| void DOMStorageContextImpl::Shutdown() { |
| if (task_runner_) |
| task_runner_->AssertIsRunningOnPrimarySequence(); |
| is_shutdown_ = true; |
| StorageNamespaceMap::const_iterator it = namespaces_.begin(); |
| for (; it != namespaces_.end(); ++it) |
| it->second->Shutdown(); |
| |
| base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider( |
| this); |
| |
| if (!session_storage_database_.get()) |
| return; |
| |
| // Respect the content policy settings about what to |
| // keep and what to discard. |
| if (force_keep_session_state_) |
| return; // Keep everything. |
| |
| bool has_session_only_origins = |
| special_storage_policy_.get() && |
| special_storage_policy_->HasSessionOnlyOrigins(); |
| |
| if (has_session_only_origins) { |
| // We may have to delete something. We continue on the |
| // commit sequence after area shutdown tasks have cycled |
| // thru that sequence (and closed their database files). |
| bool success = task_runner_->PostShutdownBlockingTask( |
| FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE, |
| base::BindOnce(&DOMStorageContextImpl::ClearSessionOnlyOrigins, this)); |
| DCHECK(success); |
| } |
| } |
| |
| void DOMStorageContextImpl::AddEventObserver(EventObserver* observer) { |
| event_observers_.AddObserver(observer); |
| } |
| |
| void DOMStorageContextImpl::RemoveEventObserver(EventObserver* observer) { |
| event_observers_.RemoveObserver(observer); |
| } |
| |
| void DOMStorageContextImpl::NotifyItemSet( |
| const DOMStorageArea* area, |
| const base::string16& key, |
| const base::string16& new_value, |
| const base::NullableString16& old_value, |
| const GURL& page_url) { |
| for (auto& observer : event_observers_) |
| observer.OnDOMStorageItemSet(area, key, new_value, old_value, page_url); |
| } |
| |
| void DOMStorageContextImpl::NotifyItemRemoved( |
| const DOMStorageArea* area, |
| const base::string16& key, |
| const base::string16& old_value, |
| const GURL& page_url) { |
| for (auto& observer : event_observers_) |
| observer.OnDOMStorageItemRemoved(area, key, old_value, page_url); |
| } |
| |
| void DOMStorageContextImpl::NotifyAreaCleared( |
| const DOMStorageArea* area, |
| const GURL& page_url) { |
| for (auto& observer : event_observers_) |
| observer.OnDOMStorageAreaCleared(area, page_url); |
| } |
| |
| // Used to diagnose unknown namespace_ids given to the ipc message filter. |
| base::Optional<bad_message::BadMessageReason> |
| DOMStorageContextImpl::DiagnoseSessionNamespaceId( |
| const std::string& namespace_id) { |
| if (base::ContainsValue(recently_deleted_session_ids_, namespace_id)) |
| return bad_message::DSH_DELETED_SESSION_ID; |
| return bad_message::DSH_NOT_ALLOCATED_SESSION_ID; |
| } |
| |
| void DOMStorageContextImpl::CreateSessionNamespace( |
| const std::string& namespace_id) { |
| if (is_shutdown_) |
| return; |
| DCHECK(!namespace_id.empty()); |
| // There are many browser tests that 'quit and restore' the browser but not |
| // the profile (and instead just reuse the old profile). This is unfortunate |
| // and doesn't actually test the 'restore' feature of session storage. |
| if (namespaces_.find(namespace_id) != namespaces_.end()) |
| return; |
| namespaces_[namespace_id] = new DOMStorageNamespace( |
| namespace_id, session_storage_database_.get(), task_runner_.get()); |
| } |
| |
| void DOMStorageContextImpl::DeleteSessionNamespace( |
| const std::string& namespace_id, |
| bool should_persist_data) { |
| DCHECK(!namespace_id.empty()); |
| StorageNamespaceMap::const_iterator it = namespaces_.find(namespace_id); |
| if (it == namespaces_.end()) |
| return; |
| if (session_storage_database_.get()) { |
| if (!should_persist_data) { |
| task_runner_->PostShutdownBlockingTask( |
| FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE, |
| base::BindOnce( |
| base::IgnoreResult(&SessionStorageDatabase::DeleteNamespace), |
| session_storage_database_, namespace_id)); |
| } else { |
| // Ensure that the data gets committed before we shut down. |
| it->second->Shutdown(); |
| if (!scavenging_started_) { |
| // Protect the persistent namespace ID from scavenging. |
| protected_session_ids_.insert(namespace_id); |
| } |
| } |
| } |
| namespaces_.erase(namespace_id); |
| |
| recently_deleted_session_ids_.push_back(namespace_id); |
| if (recently_deleted_session_ids_.size() > 10) |
| recently_deleted_session_ids_.pop_front(); |
| } |
| |
| void DOMStorageContextImpl::CloneSessionNamespace( |
| const std::string& existing_id, |
| const std::string& new_id) { |
| if (is_shutdown_) |
| return; |
| DCHECK(!existing_id.empty()); |
| DCHECK(!new_id.empty()); |
| auto found = namespaces_.find(existing_id); |
| if (found != namespaces_.end()) { |
| namespaces_[new_id] = found->second->Clone(new_id); |
| return; |
| } |
| CreateSessionNamespace(new_id); |
| } |
| |
| void DOMStorageContextImpl::ClearSessionOnlyOrigins() { |
| if (session_storage_database_.get()) { |
| std::vector<SessionStorageUsageInfo> infos; |
| GetSessionStorageUsage(&infos); |
| for (size_t i = 0; i < infos.size(); ++i) { |
| const url::Origin& origin = url::Origin::Create(infos[i].origin); |
| if (special_storage_policy_->IsStorageProtected(origin.GetURL())) |
| continue; |
| if (!special_storage_policy_->IsStorageSessionOnly(origin.GetURL())) |
| continue; |
| session_storage_database_->DeleteArea(infos[i].namespace_id, origin); |
| } |
| } |
| } |
| |
| void DOMStorageContextImpl::SetSaveSessionStorageOnDisk() { |
| DCHECK(namespaces_.empty()); |
| if (!sessionstorage_directory_.empty()) { |
| session_storage_database_ = new SessionStorageDatabase( |
| sessionstorage_directory_, task_runner_->GetSequencedTaskRunner( |
| DOMStorageTaskRunner::COMMIT_SEQUENCE)); |
| } |
| } |
| |
| void DOMStorageContextImpl::StartScavengingUnusedSessionStorage() { |
| if (session_storage_database_.get()) { |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&DOMStorageContextImpl::FindUnusedNamespaces, this), |
| base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds)); |
| } |
| } |
| |
| void DOMStorageContextImpl::PurgeMemory(PurgeOption purge_option) { |
| if (is_shutdown_) |
| return; |
| |
| DOMStorageNamespace::UsageStatistics initial_stats = |
| GetTotalNamespaceStatistics(namespaces_); |
| if (!initial_stats.total_area_count) |
| return; |
| |
| // Track the total localStorage cache size. |
| UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageCacheSizeInKB", |
| initial_stats.total_cache_size / 1024, 1, 100000, |
| 50); |
| |
| const char* purge_reason = nullptr; |
| if (purge_option == PURGE_IF_NEEDED) { |
| // Purging is done based on the cache sizes without including the database |
| // size since it can be expensive trying to estimate the sqlite usage for |
| // all databases. For low end devices purge all inactive areas. |
| if (initial_stats.total_cache_size > kMaxDomStorageCacheSize) |
| purge_reason = "SizeLimitExceeded"; |
| else if (initial_stats.total_area_count > kMaxDomStorageAreaCount) |
| purge_reason = "AreaCountLimitExceeded"; |
| else if (is_low_end_device_) |
| purge_reason = "InactiveOnLowEndDevice"; |
| if (!purge_reason) |
| return; |
| |
| purge_option = PURGE_UNOPENED; |
| } else { |
| if (purge_option == PURGE_AGGRESSIVE) |
| purge_reason = "AggressivePurgeTriggered"; |
| else |
| purge_reason = "ModeratePurgeTriggered"; |
| } |
| |
| // Return if no areas can be purged with the given option. |
| bool aggressively = purge_option == PURGE_AGGRESSIVE; |
| if (!aggressively && !initial_stats.inactive_area_count) { |
| return; |
| } |
| for (const auto& it : namespaces_) |
| it.second->PurgeMemory(aggressively); |
| |
| // Track the size of cache purged. |
| size_t purged_size_kib = |
| (initial_stats.total_cache_size - |
| GetTotalNamespaceStatistics(namespaces_).total_cache_size) / |
| 1024; |
| std::string full_histogram_name = |
| std::string("LocalStorage.BrowserLocalStorageCachePurgedInKB.") + |
| purge_reason; |
| base::HistogramBase* histogram = base::Histogram::FactoryGet( |
| full_histogram_name, 1, 100000, 50, |
| base::HistogramBase::kUmaTargetedHistogramFlag); |
| if (histogram) |
| histogram->Add(purged_size_kib); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageCachePurgedInKB", |
| purged_size_kib, 1, 100000, 50); |
| } |
| |
| bool DOMStorageContextImpl::OnMemoryDump( |
| const base::trace_event::MemoryDumpArgs& args, |
| base::trace_event::ProcessMemoryDump* pmd) { |
| if (session_storage_database_) |
| session_storage_database_->OnMemoryDump(pmd); |
| if (args.level_of_detail == |
| base::trace_event::MemoryDumpLevelOfDetail::BACKGROUND) { |
| DOMStorageNamespace::UsageStatistics total_stats = |
| GetTotalNamespaceStatistics(namespaces_); |
| auto* mad = pmd->CreateAllocatorDump(base::StringPrintf( |
| "site_storage/session_storage/0x%" PRIXPTR "/cache_size", |
| reinterpret_cast<uintptr_t>(this))); |
| mad->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize, |
| base::trace_event::MemoryAllocatorDump::kUnitsBytes, |
| total_stats.total_cache_size); |
| mad->AddScalar("inactive_areas", |
| base::trace_event::MemoryAllocatorDump::kUnitsObjects, |
| total_stats.inactive_area_count); |
| mad->AddScalar("total_areas", |
| base::trace_event::MemoryAllocatorDump::kUnitsObjects, |
| total_stats.total_area_count); |
| return true; |
| } |
| for (const auto& it : namespaces_) { |
| it.second->OnMemoryDump(pmd); |
| } |
| return true; |
| } |
| |
| void DOMStorageContextImpl::FindUnusedNamespaces() { |
| DCHECK(session_storage_database_.get()); |
| if (scavenging_started_) |
| return; |
| scavenging_started_ = true; |
| std::set<std::string> namespace_ids_in_use; |
| for (StorageNamespaceMap::const_iterator it = namespaces_.begin(); |
| it != namespaces_.end(); ++it) |
| namespace_ids_in_use.insert(it->second->namespace_id()); |
| std::set<std::string> protected_session_ids; |
| protected_session_ids.swap(protected_session_ids_); |
| task_runner_->PostShutdownBlockingTask( |
| FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE, |
| base::BindOnce( |
| &DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence, this, |
| namespace_ids_in_use, protected_session_ids)); |
| } |
| |
| void DOMStorageContextImpl::FindUnusedNamespacesInCommitSequence( |
| const std::set<std::string>& namespace_ids_in_use, |
| const std::set<std::string>& protected_session_ids) { |
| DCHECK(session_storage_database_.get()); |
| // Delete all namespaces which don't have an associated DOMStorageNamespace |
| // alive. |
| std::map<std::string, std::vector<url::Origin>> namespaces_and_origins; |
| session_storage_database_->ReadNamespacesAndOrigins(&namespaces_and_origins); |
| for (auto it = namespaces_and_origins.cbegin(); |
| it != namespaces_and_origins.cend(); ++it) { |
| if (namespace_ids_in_use.find(it->first) == namespace_ids_in_use.end() && |
| protected_session_ids.find(it->first) == protected_session_ids.end()) { |
| deletable_namespace_ids_.push_back(it->first); |
| } |
| } |
| if (!deletable_namespace_ids_.empty()) { |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&DOMStorageContextImpl::DeleteNextUnusedNamespace, this), |
| base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds)); |
| } |
| } |
| |
| void DOMStorageContextImpl::DeleteNextUnusedNamespace() { |
| if (is_shutdown_) |
| return; |
| task_runner_->PostShutdownBlockingTask( |
| FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE, |
| base::BindOnce( |
| &DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence, |
| this)); |
| } |
| |
| void DOMStorageContextImpl::DeleteNextUnusedNamespaceInCommitSequence() { |
| if (deletable_namespace_ids_.empty()) |
| return; |
| const std::string& persistent_id = deletable_namespace_ids_.back(); |
| session_storage_database_->DeleteNamespace(persistent_id); |
| deletable_namespace_ids_.pop_back(); |
| if (!deletable_namespace_ids_.empty()) { |
| task_runner_->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&DOMStorageContextImpl::DeleteNextUnusedNamespace, this), |
| base::TimeDelta::FromSeconds(kSessionStoraceScavengingSeconds)); |
| } |
| } |
| |
| } // namespace content |