// Copyright 2013 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_area.h"

#include <inttypes.h>

#include <algorithm>
#include <cctype>  // for std::isalnum

#include "base/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/process/process_info.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/process_memory_dump.h"
#include "build/build_config.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/browser/dom_storage/session_storage_database_adapter.h"
#include "content/common/dom_storage/dom_storage_map.h"
#include "content/common/dom_storage/dom_storage_types.h"
#include "content/public/browser/browser_thread.h"
#include "storage/browser/database/database_util.h"
#include "storage/common/database/database_identifier.h"
#include "storage/common/fileapi/file_system_util.h"

using storage::DatabaseUtil;

namespace content {

namespace {

// To avoid excessive IO we apply limits to the amount of data being written
// and the frequency of writes. The specific values used are somewhat arbitrary.
constexpr int kMaxBytesPerHour = kPerStorageAreaQuota;
constexpr int kMaxCommitsPerHour = 60;

}  // namespace

bool DOMStorageArea::s_aggressive_flushing_enabled_ = false;

DOMStorageArea::RateLimiter::RateLimiter(size_t desired_rate,
                                         base::TimeDelta time_quantum)
    : rate_(desired_rate), samples_(0), time_quantum_(time_quantum) {
  DCHECK_GT(desired_rate, 0ul);
}

base::TimeDelta DOMStorageArea::RateLimiter::ComputeTimeNeeded() const {
  return time_quantum_ * (samples_ / rate_);
}

base::TimeDelta DOMStorageArea::RateLimiter::ComputeDelayNeeded(
    const base::TimeDelta elapsed_time) const {
  base::TimeDelta time_needed = ComputeTimeNeeded();
  if (time_needed > elapsed_time)
    return time_needed - elapsed_time;
  return base::TimeDelta();
}

DOMStorageArea::CommitBatch::CommitBatch() : clear_all_first(false) {}
DOMStorageArea::CommitBatch::~CommitBatch() {}

size_t DOMStorageArea::CommitBatch::GetDataSize() const {
  return DOMStorageMap::CountBytes(changed_values);
}

DOMStorageArea::CommitBatchHolder::CommitBatchHolder(
    Type type,
    scoped_refptr<CommitBatch> batch)
    : type(type), batch(batch) {}
DOMStorageArea::CommitBatchHolder::CommitBatchHolder(
    const DOMStorageArea::CommitBatchHolder& other) = default;
DOMStorageArea::CommitBatchHolder::~CommitBatchHolder() {}

// static
const base::FilePath::CharType DOMStorageArea::kDatabaseFileExtension[] =
    FILE_PATH_LITERAL(".localstorage");

// static
base::FilePath DOMStorageArea::DatabaseFileNameFromOrigin(
    const url::Origin& origin) {
  std::string filename = storage::GetIdentifierFromOrigin(origin);
  // There is no base::FilePath.AppendExtension() method, so start with just the
  // extension as the filename, and then InsertBeforeExtension the desired
  // name.
  return base::FilePath().Append(kDatabaseFileExtension).
      InsertBeforeExtensionASCII(filename);
}

// static
url::Origin DOMStorageArea::OriginFromDatabaseFileName(
    const base::FilePath& name) {
  DCHECK(name.MatchesExtension(kDatabaseFileExtension));
  std::string origin_id =
      name.BaseName().RemoveExtension().MaybeAsASCII();
  return storage::GetOriginFromIdentifier(origin_id);
}

void DOMStorageArea::EnableAggressiveCommitDelay() {
  s_aggressive_flushing_enabled_ = true;
}

DOMStorageArea::DOMStorageArea(const std::string& namespace_id,
                               std::vector<std::string> original_namespace_ids,
                               const url::Origin& origin,
                               SessionStorageDatabase* session_storage_backing,
                               DOMStorageTaskRunner* task_runner)
    : namespace_id_(namespace_id),
      original_namespace_ids_(std::move(original_namespace_ids)),
      origin_(origin),
      task_runner_(task_runner),
#if defined(OS_ANDROID)
      desired_load_state_(session_storage_backing ? LOAD_STATE_KEYS_ONLY
                                                  : LOAD_STATE_KEYS_AND_VALUES),
#else
      desired_load_state_(LOAD_STATE_KEYS_AND_VALUES),
#endif
      load_state_(session_storage_backing ? LOAD_STATE_UNLOADED
                                          : LOAD_STATE_KEYS_AND_VALUES),
      map_(new DOMStorageMap(
          kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
          desired_load_state_ == LOAD_STATE_KEYS_ONLY)),
      session_storage_backing_(session_storage_backing),
      is_shutdown_(false),
      start_time_(base::TimeTicks::Now()),
      data_rate_limiter_(kMaxBytesPerHour, base::TimeDelta::FromHours(1)),
      commit_rate_limiter_(kMaxCommitsPerHour, base::TimeDelta::FromHours(1)) {
  DCHECK(!namespace_id.empty());
  if (session_storage_backing) {
    backing_.reset(
        new SessionStorageDatabaseAdapter(session_storage_backing, namespace_id,
                                          original_namespace_ids_, origin));
  }
}

DOMStorageArea::~DOMStorageArea() {
}

void DOMStorageArea::ExtractValues(DOMStorageValuesMap* map) {
  if (is_shutdown_)
    return;

  if (load_state_ == LOAD_STATE_KEYS_AND_VALUES) {
    map_->ExtractValues(map);
    return;
  }
  LoadMapAndApplyUncommittedChangesIfNeeded(map);
}

unsigned DOMStorageArea::Length() {
  if (is_shutdown_)
    return 0;
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  return map_->Length();
}

base::NullableString16 DOMStorageArea::Key(unsigned index) {
  if (is_shutdown_)
    return base::NullableString16();
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  return map_->Key(index);
}

base::NullableString16 DOMStorageArea::GetItem(const base::string16& key) {
  if (is_shutdown_)
    return base::NullableString16();
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  return map_->GetItem(key);
}

bool DOMStorageArea::SetItem(const base::string16& key,
                             const base::string16& value,
                             const base::NullableString16& client_old_value,
                             base::NullableString16* old_value) {
  if (is_shutdown_)
    return false;
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  if (!map_->HasOneRef())
    map_ = map_->DeepCopy();
  bool success = map_->SetItem(key, value, old_value);
  if (map_->has_only_keys())
    *old_value = client_old_value;
  if (success && backing_ &&
      (old_value->is_null() || old_value->string() != value)) {
    CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
    if (load_state_ == LOAD_STATE_KEYS_AND_VALUES) {
      // Values are populated later to avoid holding duplicate memory.
      commit_batch->changed_values[key] = base::NullableString16();
    } else {
      commit_batch->changed_values[key] = base::NullableString16(value, false);
    }
  }
  return success;
}

bool DOMStorageArea::RemoveItem(const base::string16& key,
                                const base::NullableString16& client_old_value,
                                base::string16* old_value) {
  if (is_shutdown_)
    return false;
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  if (!map_->HasOneRef())
    map_ = map_->DeepCopy();
  bool success = map_->RemoveItem(key, old_value);
  if (map_->has_only_keys()) {
    DCHECK(!client_old_value.is_null());
    *old_value = client_old_value.string();
  }
  if (success && backing_) {
    CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
    commit_batch->changed_values[key] = base::NullableString16();
  }
  return success;
}

bool DOMStorageArea::Clear() {
  if (is_shutdown_)
    return false;
  LoadMapAndApplyUncommittedChangesIfNeeded(nullptr);
  if (map_->Length() == 0)
    return false;

  map_ = new DOMStorageMap(
      kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
      desired_load_state_ == LOAD_STATE_KEYS_ONLY);

  if (backing_) {
    CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
    commit_batch->clear_all_first = true;
    commit_batch->changed_values.clear();
  }

  return true;
}

void DOMStorageArea::FastClear() {
  if (is_shutdown_)
    return;

  map_ = new DOMStorageMap(
      kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
      desired_load_state_ == LOAD_STATE_KEYS_ONLY);
  // This ensures no load will happen while we're waiting to clear the data
  // from the database.
  load_state_ = desired_load_state_;

  if (backing_) {
    CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
    commit_batch->clear_all_first = true;
    commit_batch->changed_values.clear();
  }
}

DOMStorageArea* DOMStorageArea::ShallowCopy(
    const std::string& destination_namespace_id) {
  DCHECK(!namespace_id_.empty());
  DCHECK(!destination_namespace_id.empty());

  std::vector<std::string> original_namespace_ids;
  original_namespace_ids.push_back(namespace_id_);
  original_namespace_ids.insert(original_namespace_ids.end(),
                                original_namespace_ids_.begin(),
                                original_namespace_ids_.end());
  DOMStorageArea* copy = new DOMStorageArea(
      destination_namespace_id, std::move(original_namespace_ids), origin_,
      session_storage_backing_.get(), task_runner_.get());
  copy->desired_load_state_ = desired_load_state_;
  copy->load_state_ = load_state_;
  copy->map_ = map_;
  copy->is_shutdown_ = is_shutdown_;

  // All the uncommitted changes to this area need to happen before the actual
  // shallow copy is made (scheduled by the upper layer sometime after return).
  if (GetCurrentCommitBatch())
    ScheduleImmediateCommit();
  if (load_state_ != LOAD_STATE_KEYS_AND_VALUES) {
    copy->commit_batches_ = commit_batches_;
    for (auto& it : copy->commit_batches_)
      it.type = CommitBatchHolder::TYPE_CLONE;
  }
  return copy;
}

bool DOMStorageArea::HasUncommittedChanges() const {
  return !commit_batches_.empty();
}

void DOMStorageArea::ScheduleImmediateCommit() {
  DCHECK(HasUncommittedChanges());
  PostCommitTask();
}

void DOMStorageArea::ClearShallowCopiedCommitBatches() {
  if (is_shutdown_)
    return;
  while (!commit_batches_.empty() &&
         commit_batches_.back().type == CommitBatchHolder::TYPE_CLONE) {
    commit_batches_.pop_back();
  }
  original_namespace_ids_.clear();
}

void DOMStorageArea::SetCacheOnlyKeys(bool only_keys) {
  LoadState new_desired_state =
      only_keys ? LOAD_STATE_KEYS_ONLY : LOAD_STATE_KEYS_AND_VALUES;
  if (is_shutdown_ || !backing_ || desired_load_state_ == new_desired_state)
    return;

  desired_load_state_ = new_desired_state;
  // Do not clear values immediately when desired state is set to keys only.
  // Either commit timer or a purge call will clear the map, in case new process
  // tries to open again. When values are desired it is ok to clear the map
  // immediately. The reload only happens when required.
  if (!map_->Length() || desired_load_state_ == LOAD_STATE_KEYS_AND_VALUES)
    UnloadMapIfDesired();
}

void DOMStorageArea::PurgeMemory() {
  DCHECK(!is_shutdown_);

  if (load_state_ == LOAD_STATE_UNLOADED ||  // We're not using any memory.
      !backing_.get() ||                     // We can't purge anything.
      HasUncommittedChanges())  // We leave things alone with changes pending.
    return;

  // Recreate the database object, this frees up the open sqlite connection
  // and its page cache.
  backing_->Reset();

  // Do not set load_state_ to |LOAD_STATE_UNLOADED| if map is empty since
  // FastClear is efficient with no reloads while waiting for clearing database.
  if (!map_ || !map_->Length())
    return;

  // Drop the in memory cache, we'll reload when needed.
  load_state_ = LOAD_STATE_UNLOADED;
  map_ = new DOMStorageMap(
      kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
      desired_load_state_ == LOAD_STATE_KEYS_ONLY);
}

void DOMStorageArea::UnloadMapIfDesired() {
  if (load_state_ == LOAD_STATE_UNLOADED || load_state_ == desired_load_state_)
    return;

  // Do not clear the map if there are uncommitted changes since the commit
  // batch might not have the values populated.
  if (!backing_ || HasUncommittedChanges())
    return;

  if (load_state_ == LOAD_STATE_KEYS_AND_VALUES) {
    scoped_refptr<DOMStorageMap> keys_values = map_;
    map_ = new DOMStorageMap(
        kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
        desired_load_state_ == LOAD_STATE_KEYS_ONLY);
    map_->TakeKeysFrom(keys_values->keys_values());
    load_state_ = LOAD_STATE_KEYS_ONLY;
    return;
  }

  map_ = new DOMStorageMap(
      kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
      desired_load_state_ == LOAD_STATE_KEYS_ONLY);
  load_state_ = LOAD_STATE_UNLOADED;
}

void DOMStorageArea::Shutdown() {
  if (is_shutdown_)
    return;
  is_shutdown_ = true;

  if (GetCurrentCommitBatch()) {
    DCHECK(backing_);
    PopulateCommitBatchValues();
  }

  map_ = nullptr;
  if (!backing_)
    return;

  bool success = task_runner_->PostShutdownBlockingTask(
      FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
      base::BindOnce(&DOMStorageArea::ShutdownInCommitSequence, this));
  DCHECK(success);
}

bool DOMStorageArea::IsMapReloadNeeded() {
  return load_state_ < desired_load_state_;
}

void DOMStorageArea::OnMemoryDump(base::trace_event::ProcessMemoryDump* pmd) {
  task_runner_->AssertIsRunningOnPrimarySequence();
  if (is_shutdown_ || load_state_ == LOAD_STATE_UNLOADED)
    return;

  // Limit the url length to 50 and strip special characters.
  std::string url = origin_.GetURL().spec().substr(0, 50);
  for (size_t index = 0; index < url.size(); ++index) {
    if (!std::isalnum(url[index]))
      url[index] = '_';
  }
  std::string name =
      base::StringPrintf("site_storage/%s/0x%" PRIXPTR, url.c_str(),
                         reinterpret_cast<uintptr_t>(this));

  const char* system_allocator_name =
      base::trace_event::MemoryDumpManager::GetInstance()
          ->system_allocator_pool_name();
  if (!commit_batches_.empty()) {
    size_t commit_batches_size = 0;
    for (const auto& it : commit_batches_)
      commit_batches_size += it.batch->GetDataSize();
    auto* commit_batch_mad = pmd->CreateAllocatorDump(name + "/commit_batch");
    commit_batch_mad->AddScalar(
        base::trace_event::MemoryAllocatorDump::kNameSize,
        base::trace_event::MemoryAllocatorDump::kUnitsBytes,
        commit_batches_size);
    if (system_allocator_name)
      pmd->AddSuballocation(commit_batch_mad->guid(), system_allocator_name);
  }

  // Do not add storage map usage if less than 1KB.
  if (map_->memory_used() < 1024)
    return;

  auto* map_mad = pmd->CreateAllocatorDump(name + "/storage_map");
  map_mad->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
                     base::trace_event::MemoryAllocatorDump::kUnitsBytes,
                     map_->memory_used());
  if (system_allocator_name)
    pmd->AddSuballocation(map_mad->guid(), system_allocator_name);
}

void DOMStorageArea::LoadMapAndApplyUncommittedChangesIfNeeded(
    DOMStorageValuesMap* map) {
  if (!backing_ || (!IsMapReloadNeeded() && !map))
    return;

  DOMStorageValuesMap read_values;
  auto most_recent_clear_all_iter = commit_batches_.begin();
  while (most_recent_clear_all_iter != commit_batches_.end()) {
    if (most_recent_clear_all_iter->batch->clear_all_first)
      break;
    ++most_recent_clear_all_iter;
  }

  if (most_recent_clear_all_iter == commit_batches_.end()) {
    base::TimeTicks before = base::TimeTicks::Now();
    backing_->ReadAllValues(&read_values);

    base::TimeDelta time_to_prime = base::TimeTicks::Now() - before;
    UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage",
                        time_to_prime);

    size_t local_storage_size_kb =
        DOMStorageMap::CountBytes(read_values) / 1024;
    // Track localStorage size, from 0-6MB. Note that the maximum size should be
    // 5MB, but we add some slop since we want to make sure the max size is
    // always above what we see in practice, since histograms can't change.
    UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB",
                                local_storage_size_kb, 1, 6 * 1024, 50);
    if (local_storage_size_kb < 100) {
      UMA_HISTOGRAM_TIMES(
          "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB",
          time_to_prime);
    } else if (local_storage_size_kb < 1000) {
      UMA_HISTOGRAM_TIMES(
          "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB",
          time_to_prime);
    } else {
      UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB",
                          time_to_prime);
    }
  }

  // Apply changes in reverse order of commit batches starting from the most
  // recent commit batch with clear all flag. It is possible that the changes in
  // one or more of the commit batches have already been written to the database
  // and reflected in the map returned by ReadAllValues(). It is okay to
  // re-apply these changes.
  auto it = most_recent_clear_all_iter == commit_batches_.end()
                ? commit_batches_.end()
                : ++most_recent_clear_all_iter;
  while (it != commit_batches_.begin()) {
    --it;
    for (const auto& item : it->batch->changed_values) {
      if (item.second.is_null())
        read_values.erase(item.first);
      else
        read_values[item.first] = item.second;
    }
  }

  if (!IsMapReloadNeeded()) {
    map->swap(read_values);
    return;
  }

  map_ = new DOMStorageMap(
      kPerStorageAreaQuota + kPerStorageAreaOverQuotaAllowance,
      desired_load_state_ == LOAD_STATE_KEYS_ONLY);
  if (desired_load_state_ == LOAD_STATE_KEYS_ONLY) {
    map_->TakeKeysFrom(read_values);
    if (map)
      map->swap(read_values);
  } else {
    map_->SwapValues(&read_values);
    if (map)
      map_->ExtractValues(map);
  }
  load_state_ = desired_load_state_;
}

DOMStorageArea::CommitBatch* DOMStorageArea::CreateCommitBatchIfNeeded() {
  DCHECK(!is_shutdown_);
  DCHECK(backing_);
  if (!GetCurrentCommitBatch()) {
    commit_batches_.emplace_front(CommitBatchHolder(
        CommitBatchHolder::TYPE_CURRENT_BATCH, new CommitBatch()));
    BrowserThread::PostAfterStartupTask(
        FROM_HERE, task_runner_,
        base::BindOnce(&DOMStorageArea::StartCommitTimer, this));
  }
  return GetCurrentCommitBatch()->batch.get();
}

const DOMStorageArea::CommitBatchHolder* DOMStorageArea::GetCurrentCommitBatch()
    const {
  return (!commit_batches_.empty() &&
          commit_batches_.front().type == CommitBatchHolder::TYPE_CURRENT_BATCH)
             ? &commit_batches_.front()
             : nullptr;
}

bool DOMStorageArea::HasCommitBatchInFlight() const {
  for (const auto& batch : commit_batches_) {
    if (batch.type == CommitBatchHolder::TYPE_IN_FLIGHT)
      return true;
  }
  return false;
}

void DOMStorageArea::PopulateCommitBatchValues() {
  task_runner_->AssertIsRunningOnPrimarySequence();
  if (load_state_ != LOAD_STATE_KEYS_AND_VALUES)
    return;
  CommitBatch* current_batch = GetCurrentCommitBatch()->batch.get();
  for (auto& key_value : current_batch->changed_values)
    key_value.second = map_->GetItem(key_value.first);
}

void DOMStorageArea::StartCommitTimer() {
  if (is_shutdown_ || !GetCurrentCommitBatch())
    return;

  // Start a timer to commit any changes that accrue in the batch, but only if
  // no commits are currently in flight. In that case the timer will be
  // started after the commits have happened.
  if (HasCommitBatchInFlight())
    return;

  task_runner_->PostDelayedTask(
      FROM_HERE, base::BindOnce(&DOMStorageArea::OnCommitTimer, this),
      ComputeCommitDelay());
}

base::TimeDelta DOMStorageArea::ComputeCommitDelay() const {
  if (s_aggressive_flushing_enabled_)
    return base::TimeDelta::FromSeconds(1);

  // Delay for a moment after a value is set in anticipation
  // of other values being set, so changes are batched.
  static constexpr base::TimeDelta kCommitDefaultDelaySecs =
      base::TimeDelta::FromSeconds(5);

  base::TimeDelta elapsed_time = base::TimeTicks::Now() - start_time_;
  base::TimeDelta delay =
      std::max(kCommitDefaultDelaySecs,
               std::max(commit_rate_limiter_.ComputeDelayNeeded(elapsed_time),
                        data_rate_limiter_.ComputeDelayNeeded(elapsed_time)));
  UMA_HISTOGRAM_LONG_TIMES("LocalStorage.CommitDelay", delay);
  return delay;
}

void DOMStorageArea::OnCommitTimer() {
  if (is_shutdown_)
    return;

  // It's possible that there is nothing to commit if an immediate
  // commit occured after the timer was scheduled but before it fired.
  if (!GetCurrentCommitBatch())
    return;

  PostCommitTask();
}

void DOMStorageArea::PostCommitTask() {
  if (is_shutdown_ || !GetCurrentCommitBatch())
    return;

  DCHECK(backing_.get());
  CommitBatchHolder& current_batch = commit_batches_.front();
  PopulateCommitBatchValues();
  current_batch.type = CommitBatchHolder::TYPE_IN_FLIGHT;

  commit_rate_limiter_.add_samples(1);
  data_rate_limiter_.add_samples(current_batch.batch->GetDataSize());

  // This method executes on the primary sequence, we schedule
  // a task for immediate execution on the commit sequence.
  task_runner_->AssertIsRunningOnPrimarySequence();
  bool success = task_runner_->PostShutdownBlockingTask(
      FROM_HERE, DOMStorageTaskRunner::COMMIT_SEQUENCE,
      base::BindOnce(&DOMStorageArea::CommitChanges, this,
                     base::RetainedRef(current_batch.batch)));
  DCHECK(success);
}

void DOMStorageArea::CommitChanges(const CommitBatch* commit_batch) {
  // This method executes on the commit sequence.
  task_runner_->AssertIsRunningOnCommitSequence();
  backing_->CommitChanges(commit_batch->clear_all_first,
                          commit_batch->changed_values);
  // TODO(michaeln): what if CommitChanges returns false (e.g., we're trying to
  // commit to a DB which is in an inconsistent state?)
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&DOMStorageArea::OnCommitComplete, this));
}

void DOMStorageArea::OnCommitComplete() {
  // We're back on the primary sequence in this method.
  task_runner_->AssertIsRunningOnPrimarySequence();
  if (is_shutdown_)
    return;

  DCHECK_EQ(CommitBatchHolder::TYPE_IN_FLIGHT, commit_batches_.back().type);
  commit_batches_.pop_back();
  if (GetCurrentCommitBatch() && !HasCommitBatchInFlight()) {
    // More changes have accrued, restart the timer.
    task_runner_->PostDelayedTask(
        FROM_HERE, base::BindOnce(&DOMStorageArea::OnCommitTimer, this),
        ComputeCommitDelay());
  } else {
    // When the desired load state is changed, the unload of map is deferred
    // when there are uncommitted changes. So, try again after committing.
    UnloadMapIfDesired();
  }
}

void DOMStorageArea::ShutdownInCommitSequence() {
  // This method executes on the commit sequence.
  task_runner_->AssertIsRunningOnCommitSequence();
  DCHECK(backing_.get());
  if (GetCurrentCommitBatch()) {
    CommitBatch* batch = GetCurrentCommitBatch()->batch.get();
    // Commit any changes that accrued prior to the timer firing.
    bool success =
        backing_->CommitChanges(batch->clear_all_first, batch->changed_values);
    DCHECK(success);
  }
  commit_batches_.clear();
  backing_.reset();
  session_storage_backing_ = nullptr;
}

}  // namespace content
