blob: 3d93f4040951836cd056f13930d3ba87dbf8a20c [file] [log] [blame]
// 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