chromium/ios/chrome/browser/sessions/model/session_restoration_service_impl.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/sessions/model/session_restoration_service_impl.h"

#import "base/check.h"
#import "base/check_op.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_util.h"
#import "base/functional/bind.h"
#import "base/functional/callback_helpers.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/ranges/algorithm.h"
#import "ios/chrome/browser/sessions/model/proto/storage.pb.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_internal_util.h"
#import "ios/chrome/browser/sessions/model/session_io_request.h"
#import "ios/chrome/browser/sessions/model/session_loading.h"
#import "ios/chrome/browser/sessions/model/session_restoration_web_state_list_observer.h"
#import "ios/chrome/browser/sessions/model/web_state_list_serialization.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/web/public/session/proto/metadata.pb.h"
#import "ios/web/public/session/proto/storage.pb.h"
#import "ios/web/public/web_state.h"

namespace {

// Maximum size of session state NSData objects.
const int kMaxSessionState = 5 * 1024 * 1024;

// Information about an orphaned WebState.
struct OrphanInfo {
  std::string session_id;
  web::proto::WebStateMetadataStorage metadata;
};

// Deletes all files and directory in `path` not present in `items_to_keep`.
void DeleteUnknownContent(const base::FilePath& path,
                          const std::set<base::FilePath>& items_to_keep) {
  std::vector<base::FilePath> items_to_remove;
  base::FileEnumerator e(path, false, base::FileEnumerator::NAMES_ONLY);
  for (base::FilePath name = e.Next(); !name.empty(); name = e.Next()) {
    if (!base::Contains(items_to_keep, name)) {
      items_to_remove.push_back(name);
    }
  }

  for (const auto& item : items_to_remove) {
    std::ignore = base::DeletePathRecursively(item);
  }
}

// Loads WebState storage from `web_state_dir` into `storage`.
web::proto::WebStateStorage LoadWebStateStorage(const base::FilePath& path) {
  web::proto::WebStateStorage storage;
  bool success = ios::sessions::ParseProto(path, storage);
  DCHECK(success);
  return storage;
}

// Loads Webstate native session from `web_state_dir`. It is okay if the file
// is missing, in that case the function return `nil`.
NSData* LoadWebStateSession(const base::FilePath& path) {
  return ios::sessions::ReadFile(path);
}

// Helper function used to construct a WebStateFactory callback for use
// with DeserializeWebStateList() function.
std::unique_ptr<web::WebState> CreateWebState(
    const base::FilePath& session_dir,
    ChromeBrowserState* browser_state,
    web::WebStateID web_state_id,
    web::proto::WebStateMetadataStorage metadata) {
  const base::FilePath web_state_dir =
      ios::sessions::WebStateDirectory(session_dir, web_state_id);

  const base::FilePath web_state_storage_path =
      web_state_dir.Append(kWebStateStorageFilename);

  const base::FilePath web_state_session_path =
      web_state_dir.Append(kWebStateSessionFilename);

  auto web_state = web::WebState::CreateWithStorage(
      browser_state, web_state_id, std::move(metadata),
      base::BindOnce(&LoadWebStateStorage, web_state_storage_path),
      base::BindOnce(&LoadWebStateSession, web_state_session_path));

  return web_state;
}

// Delete data for discarded sessions with `identifiers` in `storage_path`
// on a background thread.
void DeleteDataForSessions(const base::FilePath& storage_path,
                           const std::set<std::string>& identifiers) {
  for (const std::string& identifier : identifiers) {
    const base::FilePath path = storage_path.Append(identifier);
    std::ignore = ios::sessions::DeleteRecursively(path);
  }
}

// An output iterator that counts how many time it has been incremented.
// Allows to check if sets has non-empty intersection without allocating.
template <typename T1, typename T2>
struct CountingOutputIterator {
  CountingOutputIterator& operator++() {
    ++count;
    return *this;
  }
  CountingOutputIterator& operator++(int) {
    ++count;
    return *this;
  }

  CountingOutputIterator& operator*() { return *this; }
  CountingOutputIterator& operator=(const T1&) { return *this; }
  CountingOutputIterator& operator=(const T2&) { return *this; }

  uint32_t count = 0;
};

// Override of CountingOutputIterator<T1, T2> when types are identical.
template <typename T>
struct CountingOutputIterator<T, T> {
  CountingOutputIterator& operator++() {
    ++count;
    return *this;
  }
  CountingOutputIterator& operator++(int) {
    ++count;
    return *this;
  }

  CountingOutputIterator& operator*() { return *this; }
  CountingOutputIterator& operator=(const T&) { return *this; }

  uint32_t count = 0;
};

// Returns whether the two sets have non-empty intersection.
template <typename Range1, typename Range2>
constexpr bool HasIntersection(Range1&& range1, Range2&& range2) {
  auto result = base::ranges::set_intersection(
      std::forward<Range1>(range1), std::forward<Range2>(range2),
      CountingOutputIterator<decltype(*range1.begin()),
                             decltype(*range2.begin())>{});
  return result.count != 0;
}

// Returns a WebStateMetadataMap from `storage`.
WebStateMetadataMap MetadataMapFromStorage(
    const ios::proto::WebStateListStorage& storage) {
  WebStateMetadataMap result;
  for (const auto& item : storage.items()) {
    DCHECK(web::WebStateID::IsValidValue(item.identifier()));
    const web::WebStateID web_state_id =
        web::WebStateID::FromSerializedValue(item.identifier());

    result.insert(std::make_pair(web_state_id, item.metadata()));
  }
  return result;
}

// Updates `metadata_map` to contains data for all items in `web_state_list`
// and only those items. It will remove irrelevant mappings and add missing
// ones.
void UpdateMetadataMap(WebStateMetadataMap& metadata_map,
                       const WebStateList* web_state_list) {
  WebStateMetadataMap old_metadata_map;
  std::swap(metadata_map, old_metadata_map);

  const int count = web_state_list->count();
  for (int index = 0; index < count; ++index) {
    const web::WebState* web_state = web_state_list->GetWebStateAt(index);
    const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();
    auto iter = old_metadata_map.find(web_state_id);

    web::proto::WebStateMetadataStorage storage;
    if (iter != old_metadata_map.end()) {
      storage = std::move(iter->second);
    } else {
      web_state->SerializeMetadataToProto(storage);
    }

    metadata_map.insert(std::make_pair(web_state_id, std::move(storage)));
  }

  DCHECK_EQ(metadata_map.size(), static_cast<size_t>(count));
}

// Callback invoked for each WebState by `IterateDataForSessionAtPath`.
using SessionDataIterator =
    base::RepeatingCallback<void(web::WebStateID, web::proto::WebStateStorage)>;

// Loads data for session at `session_dir` invoking `iterator` for each
// WebState.
void IterateDataForSessionAtPath(const base::FilePath& session_dir,
                                 const SessionDataIterator& iterator) {
  const ios::proto::WebStateListStorage session =
      ios::sessions::LoadSessionStorage(session_dir);

  for (const auto& item : session.items()) {
    DCHECK(web::WebStateID::IsValidValue(item.identifier()));
    web::WebStateID web_state_id =
        web::WebStateID::FromSerializedValue(item.identifier());

    const base::FilePath web_state_storage_path =
        ios::sessions::WebStateDirectory(session_dir, web_state_id)
            .Append(kWebStateStorageFilename);

    iterator.Run(web_state_id, LoadWebStateStorage(web_state_storage_path));
  }
}

}  // anonymous namespace

// Class storing information about a WebStateList tracked by the
// SessionRestorationServiceImpl.
class SessionRestorationServiceImpl::WebStateListInfo {
 public:
  using WebStateListDirtyCallback =
      SessionRestorationWebStateListObserver::WebStateListDirtyCallback;

  // Constructor taking the `identifier` used to derive the path to the
  // storage on disk, the `web_state_list` to observe and a `callback`
  // invoked when the list or its content is considered dirty.
  //
  // If `original_info` is not null, then this objects corresponds to a
  // backup Browser (see SessionRestorationService::AttachBackup(...) for
  // more details). The pointer is used to represents the `has_backup` and
  // to ensure the objects are destroyed in the correct order.
  WebStateListInfo(const std::string& identifier,
                   WebStateList* web_state_list,
                   WebStateListInfo* original_info,
                   WebStateListDirtyCallback callback);
  ~WebStateListInfo();

  // Getter and setter for the bool storing whether it is possible to
  // load session synchronously for this WebStateList.
  bool can_load_synchronously() const { return can_load_synchronously_; }
  void set_cannot_load_synchronously() { can_load_synchronously_ = false; }

  // Returns the `identifier` used to derive the path to the storage.
  const std::string& identifier() const { return identifier_; }

  // Returns whether the Browser is registered as backup for another Browser.
  bool is_backup() const { return original_info_.get() != nullptr; }

  // Returns whether the Browser has an attached backup.
  bool has_backup() const { return backup_info_.get() != nullptr; }

  // Adds `web_state_id` to the list of expected unrealized WebState. This
  // correspond to a WebState created via `CreateUnrealizedWebState()`.
  void add_expected_id(web::WebStateID web_state_id) {
    expected_ids_.insert(web_state_id);
  }

  // Removes `web_state_id` from the list of expected unrealized WebState.
  void remove_expected_id(web::WebStateID web_state_id) {
    expected_ids_.erase(web_state_id);
  }

  // Returns whether `web_state_id` is in the list of expected unrealized
  // WebState or not. This is used to determine whether the WebState should
  // be adopted (i.e. its storage copied from another Browser) or not.
  bool is_id_expected(web::WebStateID web_state_id) const {
    return base::Contains(expected_ids_, web_state_id);
  }

  // Returns the `observer`.
  SessionRestorationWebStateListObserver& observer() { return observer_; }

  // Returns the WebStateMetadataMap.
  WebStateMetadataMap& metadata_map() { return metadata_map_; }

 private:
  const std::string identifier_;
  WebStateMetadataMap metadata_map_;
  SessionRestorationWebStateListObserver observer_;
  std::set<web::WebStateID> expected_ids_;
  bool can_load_synchronously_ = true;
  raw_ptr<WebStateListInfo> original_info_;
  raw_ptr<WebStateListInfo> backup_info_;
};

SessionRestorationServiceImpl::WebStateListInfo::WebStateListInfo(
    const std::string& identifier,
    WebStateList* web_state_list,
    WebStateListInfo* original_info,
    WebStateListDirtyCallback callback)
    : identifier_(identifier),
      observer_(web_state_list, std::move(callback)),
      original_info_(original_info) {
  DCHECK(!identifier_.empty());
  if (original_info_) {
    DCHECK(!original_info_->has_backup());
    original_info_->backup_info_ = this;
  }
}

SessionRestorationServiceImpl::WebStateListInfo::~WebStateListInfo() {
  DCHECK(!backup_info_);
  if (original_info_) {
    DCHECK_EQ(original_info_->backup_info_.get(), this);
    original_info_->backup_info_ = nullptr;
    original_info_ = nullptr;
  }
}

// Safety considerations of SessionRestorationServiceImpl:
//
// As can be seen from the API, SessionRestorationServiceImpl allow to load the
// data from disk synchronously but save all data on a background sequence. To
// ensure this is safe, WebStateListInfo store a boolean that records whether
// any file modification have been scheduled on the background sequence for the
// Browser or whether the session has already been loaded.
//
// As each Browser data is saved in a separate directory, it is safe to load
// synchronously from disk before any operation has been scheduled on the
// background sequence.
//
// There are two other operation that can still execute on the main sequence
// while task are in flight:
//  - LoadWebStateStorage(...)
//  - LoadWebStateSession(...)
//
// Those functions load the WebState's state and native session data from the
// disk. Since they both load a single file, it is safe to do it on the main
// sequence while operation are in flight on the background sequence as long
// as the write operations are atomic. This is the case of IORequest methods
// and of the Posix semantic.
//
// When closing a tab, the data is not deleted immediately as it would prevent
// re-opening the tab (e.g. to "undo" a close operation). Instead the data is
// only deleted when the session is loaded (since no WebState can reference
// the path to the file at this point).

SessionRestorationServiceImpl::SessionRestorationServiceImpl(
    base::TimeDelta save_delay,
    bool enable_pinned_web_states,
    bool enable_tab_groups,
    const base::FilePath& storage_path,
    scoped_refptr<base::SequencedTaskRunner> task_runner)
    : save_delay_(save_delay),
      enable_pinned_web_states_(enable_pinned_web_states),
      enable_tab_groups_(enable_tab_groups),
      storage_path_(storage_path.Append(kSessionRestorationDirname)),
      task_runner_(task_runner) {
  DCHECK(storage_path_.IsAbsolute());
  DCHECK(task_runner_);
}

SessionRestorationServiceImpl::~SessionRestorationServiceImpl() {}

void SessionRestorationServiceImpl::Shutdown() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(infos_.empty()) << "Disconnect() must be called for all Browser";
}

#pragma mark - SessionRestorationService

void SessionRestorationServiceImpl::AddObserver(
    SessionRestorationObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.AddObserver(observer);
}

void SessionRestorationServiceImpl::RemoveObserver(
    SessionRestorationObserver* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observers_.RemoveObserver(observer);
}

void SessionRestorationServiceImpl::SaveSessions() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SaveDirtySessions();
}

void SessionRestorationServiceImpl::ScheduleSaveSessions() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Nothing to do, the service automatically schedule a save as soon
  // as changes are detected.
}

void SessionRestorationServiceImpl::SetSessionID(
    Browser* browser,
    const std::string& identifier) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  WebStateList* web_state_list = browser->GetWebStateList();

  DCHECK(!base::Contains(infos_, web_state_list));
  DCHECK(!base::Contains(identifiers_, identifier));
  identifiers_.insert(identifier);

  // It is safe to use base::Unretained(this) as the callback is never called
  // after SessionRestorationWebStateListObserver is destroyed. Those objects
  // are owned by the current instance, and destroyed before `this`.
  infos_.insert(std::make_pair(
      web_state_list,
      std::make_unique<WebStateListInfo>(
          identifier, web_state_list, /*original_info=*/nullptr,
          base::BindRepeating(
              &SessionRestorationServiceImpl::MarkWebStateListDirty,
              base::Unretained(this)))));
}

void SessionRestorationServiceImpl::LoadSession(Browser* browser) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(base::Contains(infos_, browser->GetWebStateList()));
  WebStateListInfo& info = *infos_[browser->GetWebStateList()];
  DCHECK(!info.is_backup());

  const base::TimeTicks start_time = base::TimeTicks::Now();

  // Check that LoadSession is only called once, and before any asynchronous
  // operation where started on that Browser. Then mark the Browser as no
  // longer safe for synchrounous operations.
  DCHECK(info.can_load_synchronously())
      << "LoadSession() must only be called on startup.";
  info.set_cannot_load_synchronously();

  for (auto& observer : observers_) {
    observer.WillStartSessionRestoration(browser);
  }

  // Load the session for the Browser.
  const base::FilePath session_dir = storage_path_.Append(info.identifier());
  ios::proto::WebStateListStorage session =
      ios::sessions::LoadSessionStorage(session_dir);

  // Updates `info`'s WebStateMetadataMap from `session`.
  WebStateMetadataMap& metadata_map = info.metadata_map();
  metadata_map = MetadataMapFromStorage(session);

  // Since this is the first session load, it is safe to delete any
  // unreferenced files from the Browser's storage path.
  std::set<base::FilePath> files_to_keep;
  files_to_keep.insert(session_dir.Append(kSessionMetadataFilename));
  for (const auto& item : session.items()) {
    files_to_keep.insert(ios::sessions::WebStateDirectory(
        session_dir, web::WebStateID::FromSerializedValue(item.identifier())));
  }

  task_runner_->PostTask(FROM_HERE,
                         base::BindOnce(&DeleteUnknownContent, session_dir,
                                        std::move(files_to_keep)));

  // Deserialize the session from storage.
  const std::vector<web::WebState*> restored_web_states =
      DeserializeWebStateList(browser->GetWebStateList(), std::move(session),
                              enable_pinned_web_states_, enable_tab_groups_,
                              base::BindRepeating(&CreateWebState, session_dir,
                                                  browser->GetBrowserState()));

  // Loading the session may have dropped some items, so clean the metadata map.
  UpdateMetadataMap(metadata_map, browser->GetWebStateList());

  // Loading the session may have marked the Browser as dirty (unless the
  // session was empty). There is no need to serialize the WebStates that
  // have just been restored (and it is not possible for most of them as
  // they are still unrealized), so clear the observer.
  info.observer().ClearDirty();
  dirty_web_state_lists_.erase(browser->GetWebStateList());

  // If multiple windows are open, it is possible for some other Browsers
  // to be dirty. Check if this is the case or not. If there are no dirty
  // Browsers, cancel the timer.
  if (dirty_web_state_lists_.empty()) {
    if (timer_.IsRunning()) {
      timer_.Stop();
    }
  }

  for (auto& observer : observers_) {
    observer.SessionRestorationFinished(browser, restored_web_states);
  }

  // Record the time spent blocking the main thread to load the session.
  base::UmaHistogramTimes(kSessionHistogramLoadingTime,
                          base::TimeTicks::Now() - start_time);
}

void SessionRestorationServiceImpl::LoadWebStateStorage(
    Browser* browser,
    web::WebState* web_state,
    WebStateStorageCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto iterator = infos_.find(browser->GetWebStateList());
  if (iterator == infos_.end()) {
    return;
  }

  WebStateListInfo& info = *iterator->second;
  const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();
  const base::FilePath web_state_dir = ios::sessions::WebStateDirectory(
      storage_path_.Append(info.identifier()), web_state_id);
  const base::FilePath storage_path =
      web_state_dir.Append(kWebStateStorageFilename);

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE, base::BindOnce(&::LoadWebStateStorage, storage_path),
      std::move(callback));
}

void SessionRestorationServiceImpl::AttachBackup(Browser* browser,
                                                 Browser* backup) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  WebStateList* web_state_list = backup->GetWebStateList();

  DCHECK(!base::Contains(infos_, web_state_list));

  auto iterator = infos_.find(browser->GetWebStateList());
  DCHECK(iterator != infos_.end());
  WebStateListInfo* info = iterator->second.get();

  DCHECK(!info->is_backup());
  DCHECK(!info->has_backup());

  // It is safe to use base::Unretained(this) as the callback is never called
  // after SessionRestorationWebStateListObserver is destroyed. Those objects
  // are owned by the current instance, and destroyed before `this`.
  infos_.insert(std::make_pair(
      web_state_list,
      std::make_unique<WebStateListInfo>(
          info->identifier(), web_state_list, info,
          base::BindRepeating(
              &SessionRestorationServiceImpl::MarkWebStateListDirty,
              base::Unretained(this)))));
}

void SessionRestorationServiceImpl::Disconnect(Browser* browser) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  SaveDirtySessions();
  DCHECK(dirty_web_state_lists_.empty());

  auto iterator = infos_.find(browser->GetWebStateList());
  DCHECK(iterator != infos_.end());

  WebStateListInfo& info = *iterator->second;
  DCHECK(!info.has_backup());

  if (!info.is_backup()) {
    DCHECK(base::Contains(identifiers_, info.identifier()));
    identifiers_.erase(info.identifier());
  }

  infos_.erase(iterator);
}

std::unique_ptr<web::WebState>
SessionRestorationServiceImpl::CreateUnrealizedWebState(
    Browser* browser,
    web::proto::WebStateStorage storage) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto iterator = infos_.find(browser->GetWebStateList());
  DCHECK(iterator != infos_.end());

  // Create the unique identifier for the new WebState and mark it as
  // expected with the WebStateListInfo (since it cannot be adopted).
  const web::WebStateID web_state_id = web::WebStateID::NewUnique();

  WebStateListInfo& info = *iterator->second;
  info.add_expected_id(web_state_id);

  // Schedule saving the storage and metadata for the created WebState
  // to disk before creating it, to ensure the data is available after
  // the next application restart even if the WebState never transition
  // to the realised state.
  const base::FilePath web_state_dir = ios::sessions::WebStateDirectory(
      storage_path_.Append(info.identifier()), web_state_id);

  // Add the metadata to `info`'s WebStateMetadataMap. It will be saved when
  // the WebState is inserted in the WebStateList.
  DCHECK(storage.has_metadata());
  web::proto::WebStateMetadataStorage metadata;
  metadata.Swap(storage.mutable_metadata());

  DCHECK(!base::Contains(info.metadata_map(), web_state_id));
  info.metadata_map().insert(std::make_pair(web_state_id, metadata));

  // Create the request to serialize WebState storage and add it to the
  // list of pending requests (they will be scheduled once the WebState
  // is inserted in the Browser's WebStateList).
  pending_requests_.push_back(
      std::make_unique<ios::sessions::WriteProtoIORequest>(
          web_state_dir.Append(kWebStateStorageFilename),
          std::make_unique<web::proto::WebStateStorage>(storage)));

  // Create the WebState with callback that return the data from memory. This
  // ensure there is no race condition while trying to read the data from the
  // main thread while it is being written to disk on a background thread.
  return web::WebState::CreateWithStorage(
      browser->GetBrowserState(), web_state_id, std::move(metadata),
      base::ReturnValueOnce(std::move(storage)),
      base::ReturnValueOnce<NSData*>(nil));
}

void SessionRestorationServiceImpl::DeleteDataForDiscardedSessions(
    const std::set<std::string>& identifiers,
    base::OnceClosure closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!HasIntersection(identifiers, identifiers_));
  task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&DeleteDataForSessions, storage_path_, identifiers),
      std::move(closure));
}

void SessionRestorationServiceImpl::InvokeClosureWhenBackgroundProcessingDone(
    base::OnceClosure closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  task_runner_->PostTask(FROM_HERE, std::move(closure));
}

void SessionRestorationServiceImpl::PurgeUnassociatedData(
    base::OnceClosure closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
                                                           std::move(closure));
}

void SessionRestorationServiceImpl::ParseDataForBrowserAsync(
    Browser* browser,
    WebStateStorageIterationCallback iter_callback,
    WebStateStorageIterationCompleteCallback done) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  auto iter = infos_.find(browser->GetWebStateList());
  DCHECK(iter != infos_.end());

  const WebStateListInfo& info = *iter->second;
  task_runner_->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&IterateDataForSessionAtPath,
                     storage_path_.Append(info.identifier()),
                     std::move(iter_callback)),
      std::move(done));
}

#pragma mark - Private

void SessionRestorationServiceImpl::MarkWebStateListDirty(
    WebStateList* web_state_list) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  dirty_web_state_lists_.insert(web_state_list);
  if (!timer_.IsRunning()) {
    timer_.Start(
        FROM_HERE, save_delay_,
        base::BindRepeating(&SessionRestorationServiceImpl::SaveDirtySessions,
                            base::Unretained(this)));
  }
}

void SessionRestorationServiceImpl::SaveDirtySessions() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (timer_.IsRunning()) {
    timer_.Stop();
  }

  if (dirty_web_state_lists_.empty()) {
    return;
  }

  const base::TimeTicks start_time = base::TimeTicks::Now();

  // Initialize the list of requests with all pending request. This ensures
  // that any WebState created by CreateUnrealizedWebState(...) will have
  // its state saved.
  ios::sessions::IORequestList requests;
  std::swap(requests, pending_requests_);

  // Create a map of orphaned WebStates (i.e. "unrealized" WebStates detached
  // from a WebStateList).
  std::map<web::WebStateID, OrphanInfo> orphaned_map;
  for (WebStateList* web_state_list : dirty_web_state_lists_) {
    DCHECK(base::Contains(infos_, web_state_list));
    WebStateListInfo& info = *infos_[web_state_list];

    const auto& detached_web_states = info.observer().detached_web_states();
    if (!detached_web_states.empty()) {
      WebStateMetadataMap& metadata_map = info.metadata_map();
      const std::string& identifier = info.identifier();

      for (const auto web_state_id : detached_web_states) {
        // If a realized WebState is created, inserted into a Browser and
        // then moved to another Browser before its state could be saved,
        // the metadata will not be present in the metadata_map. See the
        // bug https://crbug.com/329219388 for more details.
        auto iter = metadata_map.find(web_state_id);
        if (iter == metadata_map.end()) {
          continue;
        }

        OrphanInfo orphan_info{
            .session_id = identifier,
            .metadata = std::move(iter->second),
        };

        DCHECK(!base::Contains(orphaned_map, web_state_id));
        orphaned_map.insert(
            std::make_pair(web_state_id, std::move(orphan_info)));

        metadata_map.erase(iter);
      }
    }
  }

  // Handle adopted WebStates (i.e. "unrealized" WebStates inserted into a
  // WebStateList).
  for (WebStateList* web_state_list : dirty_web_state_lists_) {
    DCHECK(base::Contains(infos_, web_state_list));
    WebStateListInfo& info = *infos_[web_state_list];

    const auto& inserted_web_states = info.observer().inserted_web_states();
    if (!inserted_web_states.empty()) {
      const base::FilePath dest_dir = storage_path_.Append(info.identifier());

      WebStateMetadataMap& metadata_map = info.metadata_map();
      for (const auto web_state_id : inserted_web_states) {
        // Check whether the `web_state_id` is expected. If this is the case,
        // then `CreateUnrealizedWebState()` took care of scheduling tasks to
        // save its state to disk and there is nothing to do here.
        if (info.is_id_expected(web_state_id)) {
          info.remove_expected_id(web_state_id);
          continue;
        }

        // If the `web_state_id` is not expected, then it must be adopted
        // from another Browser, thus needs to be in the `orphaned_map`.
        DCHECK(base::Contains(orphaned_map, web_state_id));
        auto iter = orphaned_map.find(web_state_id);
        OrphanInfo& orphan_info = iter->second;

        // Only unrealized WebState should be adopted, realized WebState
        // will instead be considered dirty. Thus the metadata should be
        // present in the orphaned_map.
        DCHECK(!base::Contains(metadata_map, web_state_id));
        metadata_map.insert(
            std::make_pair(web_state_id, std::move(orphan_info.metadata)));

        // No need to copy if this is moving to/from the backup.
        if (orphan_info.session_id == info.identifier()) {
          continue;
        }

        const base::FilePath from_dir =
            storage_path_.Append(orphan_info.session_id);

        // Create a request to copy the orphaned data.
        requests.push_back(std::make_unique<ios::sessions::CopyPathIORequest>(
            ios::sessions::WebStateDirectory(from_dir, web_state_id),
            ios::sessions::WebStateDirectory(dest_dir, web_state_id)));
      }
    }
  }

  // Handle dirty WebStateLists and WebStates.
  for (WebStateList* web_state_list : dirty_web_state_lists_) {
    DCHECK(base::Contains(infos_, web_state_list));
    WebStateListInfo& info = *infos_[web_state_list];
    WebStateMetadataMap& metadata_map = info.metadata_map();

    // Asynchronous operation will be scheduled for this Browser, so it is
    // no longer safe to perform synchronous operation on it anymore.
    info.set_cannot_load_synchronously();

    SessionRestorationWebStateListObserver& observer = info.observer();
    DCHECK(observer.is_web_state_list_dirty() ||
           !observer.dirty_web_states().empty());

    const base::FilePath dest_dir = storage_path_.Append(info.identifier());

    // Serialize the state of dirty WebState before serializing the metadata
    // for the WebStateList. This ensures that metadata is always referring
    // to WebStates that have been saved.
    const auto& dirty_web_states = observer.dirty_web_states();
    for (web::WebState* web_state : dirty_web_states) {
      const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();
      const base::FilePath web_state_dir =
          ios::sessions::WebStateDirectory(dest_dir, web_state_id);

      // Serialize the WebState to protobuf message.
      auto storage = std::make_unique<web::proto::WebStateStorage>();
      web_state->SerializeToProto(*storage);
      DCHECK(storage->has_metadata());

      // Extract the metadata from `storage` to save it in its own file.
      // The metadata must be non-null at this point (since at least the
      // creation time or last active time will be non-default).
      auto metadata = base::WrapUnique(storage->release_metadata());
      DCHECK(metadata);

      // Update the metadata in `info`'s WebStateMetadataMap. It will be
      // saved inside the WebStateListStorage.
      auto iter = metadata_map.find(web_state_id);
      if (iter == metadata_map.end()) {
        metadata_map.insert(std::make_pair(web_state_id, std::move(*metadata)));
      } else {
        iter->second = std::move(*metadata);
      }

      // Create a request to serialize the `storage`.
      requests.push_back(std::make_unique<ios::sessions::WriteProtoIORequest>(
          web_state_dir.Append(kWebStateStorageFilename), std::move(storage)));

      // Try to serialize the native session data, but abort if too large.
      // In that case, the old data is deleted (deleting a non-existing
      // file is not a failure).
      NSData* data = web_state->SessionStateData();
      if (data && data.length <= kMaxSessionState) {
        requests.push_back(std::make_unique<ios::sessions::WriteDataIORequest>(
            web_state_dir.Append(kWebStateSessionFilename), data));
      } else {
        requests.push_back(std::make_unique<ios::sessions::DeletePathIORequest>(
            web_state_dir.Append(kWebStateSessionFilename)));
      }
    }

    // Delete the storage of any WebState that has been closed (the data is
    // now unreachable, and thus can safely be deleted).
    const auto& closed_web_states = observer.closed_web_states();
    for (web::WebStateID web_state_id : closed_web_states) {
      // It is possible (though unlikely) for a WebState to be closed just
      // after being moved between Browser. Support that case by deleting
      // the data from the Browser that listed the WebState for adoption.
      base::FilePath browser_dir = dest_dir;

      auto iter = orphaned_map.find(web_state_id);
      if (iter != orphaned_map.end()) {
        const OrphanInfo& orphan_info = iter->second;
        browser_dir = storage_path_.Append(orphan_info.session_id);
      } else {
        metadata_map.erase(web_state_id);
      }

      requests.push_back(std::make_unique<ios::sessions::DeletePathIORequest>(
          ios::sessions::WebStateDirectory(browser_dir, web_state_id)));
    }

    // Clear the "dirty" bit.
    observer.ClearDirty();

    // No need to serialize if this is a backup.
    if (info.is_backup()) {
      continue;
    }

    // It has been found in production that the metadata map may be missing
    // data for some items (see https://crbug.com/332533665 for such crash).
    // As a workaround, update the metadata map from the WebStateList.
    UpdateMetadataMap(metadata_map, web_state_list);

    // Always serialize the WebStateList as it includes the WebStates'
    // metadata (and thus needs to be saved either the list or one of
    // the WebState is dirty).
    auto storage = std::make_unique<ios::proto::WebStateListStorage>();
    SerializeWebStateList(*web_state_list, metadata_map, *storage);

    requests.push_back(std::make_unique<ios::sessions::WriteProtoIORequest>(
        dest_dir.Append(kSessionMetadataFilename), std::move(storage)));
  }

  // Post the IORequests on the background sequence as writing to disk
  // can block.
  task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&ios::sessions::ExecuteIORequests, std::move(requests)));

  dirty_web_state_lists_.clear();

  // Record the time spent blocking the main thread to save the session.
  base::UmaHistogramTimes(kSessionHistogramSavingTime,
                          base::TimeTicks::Now() - start_time);
}