From a8a4e3c851d3e12ad27ac25493b6b690b867e39c Mon Sep 17 00:00:00 2001
From: rdsmith <rdsmith@chromium.org>
Date: Thu, 8 Jan 2015 12:18:17 -0800
Subject: Add an eviction mechanism for SDCH dictionaries.

Implemented policy:
* If space is needed for a newly loaded dictionary, evict any dictionaries
  that haven't been used in a day to make room, oldest first.
* If the system signals memory pressure, flush all dictionaries.

Also moved chrome_sdch_policy.* -> net/sdch/sdch_owner.*.

BUG=387883
BUG=374916

Review URL: https://codereview.chromium.org/841883002

Cr-Commit-Position: refs/heads/master@{#310584}
---
 net/sdch/sdch_owner.cc          | 265 ++++++++++++++++++++++
 net/sdch/sdch_owner.h           |  97 ++++++++
 net/sdch/sdch_owner_unittest.cc | 491 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 853 insertions(+)
 create mode 100644 net/sdch/sdch_owner.cc
 create mode 100644 net/sdch/sdch_owner.h
 create mode 100644 net/sdch/sdch_owner_unittest.cc

(limited to 'net/sdch')

diff --git a/net/sdch/sdch_owner.cc b/net/sdch/sdch_owner.cc
new file mode 100644
index 0000000..fc1c57b
--- /dev/null
+++ b/net/sdch/sdch_owner.cc
@@ -0,0 +1,265 @@
+// Copyright 2014 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 "net/sdch/sdch_owner.h"
+
+#include "base/bind.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/time/default_clock.h"
+#include "net/base/sdch_manager.h"
+#include "net/base/sdch_net_log_params.h"
+
+namespace {
+
+enum DictionaryFate {
+  // A Get-Dictionary header wasn't acted on.
+  DICTIONARY_FATE_GET_IGNORED = 1,
+
+  // A fetch was attempted, but failed.
+  // TODO(rdsmith): Actually record this case.
+  DICTIONARY_FATE_FETCH_FAILED = 2,
+
+  // A successful fetch was dropped on the floor, no space.
+  DICTIONARY_FATE_FETCH_IGNORED_NO_SPACE = 3,
+
+  // A successful fetch was refused by the SdchManager.
+  DICTIONARY_FATE_FETCH_MANAGER_REFUSED = 4,
+
+  // A dictionary was successfully added.
+  DICTIONARY_FATE_ADDED = 5,
+
+  // A dictionary was evicted by an incoming dict.
+  DICTIONARY_FATE_EVICT_FOR_DICT = 6,
+
+  // A dictionary was evicted by memory pressure.
+  DICTIONARY_FATE_EVICT_FOR_MEMORY = 7,
+
+  // A dictionary was evicted on destruction.
+  DICTIONARY_FATE_EVICT_FOR_DESTRUCTION = 8,
+
+  DICTIONARY_FATE_MAX = 9
+};
+
+void RecordDictionaryFate(enum DictionaryFate fate) {
+  UMA_HISTOGRAM_ENUMERATION("Sdch3.DictionaryFate", fate, DICTIONARY_FATE_MAX);
+}
+
+void RecordDictionaryEviction(int use_count, DictionaryFate fate) {
+  DCHECK(fate == DICTIONARY_FATE_EVICT_FOR_DICT ||
+         fate == DICTIONARY_FATE_EVICT_FOR_MEMORY ||
+         fate == DICTIONARY_FATE_EVICT_FOR_DESTRUCTION);
+
+  UMA_HISTOGRAM_COUNTS_100("Sdch3.DictionaryUseCount", use_count);
+  RecordDictionaryFate(fate);
+}
+
+}  // namespace
+
+namespace net {
+
+// Adjust SDCH limits downwards for mobile.
+#if defined(OS_ANDROID) || defined(OS_IOS)
+// static
+const size_t SdchOwner::kMaxTotalDictionarySize = 500 * 1000;
+#else
+// static
+const size_t SdchOwner::kMaxTotalDictionarySize = 20 * 1000 * 1000;
+#endif
+
+// Somewhat arbitrary, but we assume a dictionary smaller than
+// 50K isn't going to do anyone any good.  Note that this still doesn't
+// prevent download and addition unless there is less than this
+// amount of space available in storage.
+const size_t SdchOwner::kMinSpaceForDictionaryFetch = 50 * 1000;
+
+SdchOwner::SdchOwner(net::SdchManager* sdch_manager,
+                     net::URLRequestContext* context)
+    : manager_(sdch_manager),
+      fetcher_(context,
+               base::Bind(&SdchOwner::OnDictionaryFetched,
+                          // Because |fetcher_| is owned by SdchOwner, the
+                          // SdchOwner object will be available for the lifetime
+                          // of |fetcher_|.
+                          base::Unretained(this))),
+      total_dictionary_bytes_(0),
+      clock_(new base::DefaultClock),
+      max_total_dictionary_size_(kMaxTotalDictionarySize),
+      min_space_for_dictionary_fetch_(kMinSpaceForDictionaryFetch),
+      memory_pressure_listener_(
+          base::Bind(&SdchOwner::OnMemoryPressure,
+                     // Because |memory_pressure_listener_| is owned by
+                     // SdchOwner, the SdchOwner object will be available
+                     // for the lifetime of |memory_pressure_listener_|.
+                     base::Unretained(this))) {
+  manager_->AddObserver(this);
+}
+
+SdchOwner::~SdchOwner() {
+  for (auto it = local_dictionary_info_.begin();
+       it != local_dictionary_info_.end(); ++it) {
+    RecordDictionaryEviction(it->second.use_count,
+                             DICTIONARY_FATE_EVICT_FOR_DESTRUCTION);
+  }
+  manager_->RemoveObserver(this);
+}
+
+void SdchOwner::SetMaxTotalDictionarySize(size_t max_total_dictionary_size) {
+  max_total_dictionary_size_ = max_total_dictionary_size;
+}
+
+void SdchOwner::SetMinSpaceForDictionaryFetch(
+    size_t min_space_for_dictionary_fetch) {
+  min_space_for_dictionary_fetch_ = min_space_for_dictionary_fetch;
+}
+
+void SdchOwner::OnDictionaryFetched(const std::string& dictionary_text,
+                                    const GURL& dictionary_url,
+                                    const net::BoundNetLog& net_log) {
+  struct DictionaryItem {
+    base::Time last_used;
+    std::string server_hash;
+    int use_count;
+    size_t dictionary_size;
+
+    DictionaryItem() : use_count(0), dictionary_size(0) {}
+    DictionaryItem(const base::Time& last_used,
+                   const std::string& server_hash,
+                   int use_count,
+                   size_t dictionary_size)
+        : last_used(last_used),
+          server_hash(server_hash),
+          use_count(use_count),
+          dictionary_size(dictionary_size) {}
+    DictionaryItem(const DictionaryItem& rhs) = default;
+    DictionaryItem& operator=(const DictionaryItem& rhs) = default;
+    bool operator<(const DictionaryItem& rhs) const {
+      return last_used < rhs.last_used;
+    }
+  };
+
+  std::vector<DictionaryItem> stale_dictionary_list;
+  size_t recoverable_bytes = 0;
+  base::Time stale_boundary(clock_->Now() - base::TimeDelta::FromDays(1));
+  for (auto used_it = local_dictionary_info_.begin();
+       used_it != local_dictionary_info_.end(); ++used_it) {
+    if (used_it->second.last_used < stale_boundary) {
+      stale_dictionary_list.push_back(
+          DictionaryItem(used_it->second.last_used, used_it->first,
+                         used_it->second.use_count, used_it->second.size));
+      recoverable_bytes += used_it->second.size;
+    }
+  }
+
+  if (total_dictionary_bytes_ + dictionary_text.size() - recoverable_bytes >
+      max_total_dictionary_size_) {
+    RecordDictionaryFate(DICTIONARY_FATE_FETCH_IGNORED_NO_SPACE);
+    net::SdchManager::SdchErrorRecovery(SDCH_DICTIONARY_NO_ROOM);
+    net_log.AddEvent(net::NetLog::TYPE_SDCH_DICTIONARY_ERROR,
+                     base::Bind(&net::NetLogSdchDictionaryFetchProblemCallback,
+                                SDCH_DICTIONARY_NO_ROOM, dictionary_url, true));
+    return;
+  }
+
+  // Evict from oldest to youngest until we have space.
+  std::sort(stale_dictionary_list.begin(), stale_dictionary_list.end());
+  size_t avail_bytes = max_total_dictionary_size_ - total_dictionary_bytes_;
+  auto stale_it = stale_dictionary_list.begin();
+  while (avail_bytes < dictionary_text.size() &&
+         stale_it != stale_dictionary_list.end()) {
+    manager_->RemoveSdchDictionary(stale_it->server_hash);
+    RecordDictionaryEviction(stale_it->use_count,
+                             DICTIONARY_FATE_EVICT_FOR_DICT);
+    local_dictionary_info_.erase(stale_it->server_hash);
+    avail_bytes += stale_it->dictionary_size;
+    ++stale_it;
+  }
+  DCHECK(avail_bytes >= dictionary_text.size());
+
+  std::string server_hash;
+  net::SdchProblemCode rv = manager_->AddSdchDictionary(
+      dictionary_text, dictionary_url, &server_hash);
+  if (rv != net::SDCH_OK) {
+    RecordDictionaryFate(DICTIONARY_FATE_FETCH_MANAGER_REFUSED);
+    net::SdchManager::SdchErrorRecovery(rv);
+    net_log.AddEvent(net::NetLog::TYPE_SDCH_DICTIONARY_ERROR,
+                     base::Bind(&net::NetLogSdchDictionaryFetchProblemCallback,
+                                rv, dictionary_url, true));
+    return;
+  }
+
+  RecordDictionaryFate(DICTIONARY_FATE_ADDED);
+
+  DCHECK(local_dictionary_info_.end() ==
+         local_dictionary_info_.find(server_hash));
+  total_dictionary_bytes_ += dictionary_text.size();
+  local_dictionary_info_[server_hash] = DictionaryInfo(
+      // Set the time last used to something to avoid thrashing, but not recent,
+      // to avoid taking too much time/space with useless dictionaries/one-off
+      // visits to web sites.
+      clock_->Now() - base::TimeDelta::FromHours(23), dictionary_text.size());
+}
+
+void SdchOwner::OnDictionaryUsed(SdchManager* manager,
+                                 const std::string& server_hash) {
+  auto it = local_dictionary_info_.find(server_hash);
+  DCHECK(local_dictionary_info_.end() != it);
+
+  it->second.last_used = clock_->Now();
+  it->second.use_count++;
+}
+
+void SdchOwner::OnGetDictionary(net::SdchManager* manager,
+                                const GURL& request_url,
+                                const GURL& dictionary_url) {
+  base::Time stale_boundary(clock_->Now() - base::TimeDelta::FromDays(1));
+  size_t avail_bytes = 0;
+  for (auto it = local_dictionary_info_.begin();
+       it != local_dictionary_info_.end(); ++it) {
+    if (it->second.last_used < stale_boundary)
+      avail_bytes += it->second.size;
+  }
+
+  // Don't initiate the fetch if we wouldn't be able to store any
+  // reasonable dictionary.
+  // TODO(rdsmith): Maybe do a HEAD request to figure out how much
+  // storage we'd actually need?
+  if (max_total_dictionary_size_ < (total_dictionary_bytes_ - avail_bytes +
+                                    min_space_for_dictionary_fetch_)) {
+    RecordDictionaryFate(DICTIONARY_FATE_GET_IGNORED);
+    // TODO(rdsmith): Log a net-internals error.  This requires
+    // SdchManager to forward the URLRequest that detected the
+    // Get-Dictionary header to its observers, which is tricky
+    // because SdchManager is layered underneath URLRequest.
+    return;
+  }
+
+  fetcher_.Schedule(dictionary_url);
+}
+
+void SdchOwner::OnClearDictionaries(net::SdchManager* manager) {
+  total_dictionary_bytes_ = 0;
+  local_dictionary_info_.clear();
+  fetcher_.Cancel();
+}
+
+void SdchOwner::SetClockForTesting(scoped_ptr<base::Clock> clock) {
+  clock_ = clock.Pass();
+}
+
+void SdchOwner::OnMemoryPressure(
+    base::MemoryPressureListener::MemoryPressureLevel level) {
+  DCHECK_NE(base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE, level);
+
+  for (auto it = local_dictionary_info_.begin();
+       it != local_dictionary_info_.end(); ++it) {
+    RecordDictionaryEviction(it->second.use_count,
+                             DICTIONARY_FATE_EVICT_FOR_MEMORY);
+  }
+
+  // TODO(rdsmith): Make a distinction between moderate and critical
+  // memory pressure.
+  manager_->ClearData();
+}
+
+}  // namespace net
diff --git a/net/sdch/sdch_owner.h b/net/sdch/sdch_owner.h
new file mode 100644
index 0000000..9620326
--- /dev/null
+++ b/net/sdch/sdch_owner.h
@@ -0,0 +1,97 @@
+// Copyright 2014 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.
+
+#ifndef NET_SDCH_SDCH_OWNER_H_
+#define NET_SDCH_SDCH_OWNER_H_
+
+#include <string>
+
+#include "base/memory/memory_pressure_listener.h"
+#include "net/base/sdch_observer.h"
+#include "net/url_request/sdch_dictionary_fetcher.h"
+
+class GURL;
+
+namespace base {
+class Clock;
+}
+
+namespace net {
+class SdchManager;
+class URLRequestContext;
+
+// This class owns the SDCH objects not owned as part of URLRequestContext, and
+// exposes interface for setting SDCH policy.  It should be instantiated by
+// the net/ embedder.
+// TODO(rdsmith): Implement dictionary prioritization.
+class NET_EXPORT SdchOwner : public net::SdchObserver {
+ public:
+  static const size_t kMaxTotalDictionarySize;
+  static const size_t kMinSpaceForDictionaryFetch;
+
+  // Consumer must guarantee that |sdch_manager| and |context| outlive
+  // this object.
+  SdchOwner(net::SdchManager* sdch_manager, net::URLRequestContext* context);
+  ~SdchOwner() override;
+
+  // Defaults to kMaxTotalDictionarySize.
+  void SetMaxTotalDictionarySize(size_t max_total_dictionary_size);
+
+  // Defaults to kMinSpaceForDictionaryFetch.
+  void SetMinSpaceForDictionaryFetch(size_t min_space_for_dictionary_fetch);
+
+  // SdchObserver implementation.
+  void OnDictionaryUsed(SdchManager* manager,
+                        const std::string& server_hash) override;
+  void OnGetDictionary(net::SdchManager* manager,
+                       const GURL& request_url,
+                       const GURL& dictionary_url) override;
+  void OnClearDictionaries(net::SdchManager* manager) override;
+
+  // Implementation detail--this is the pathway through which the
+  // fetcher informs the SdchOwner that it's gotten the dictionary.
+  // Public for testing.
+  void OnDictionaryFetched(const std::string& dictionary_text,
+                           const GURL& dictionary_url,
+                           const net::BoundNetLog& net_log);
+
+  void SetClockForTesting(scoped_ptr<base::Clock> clock);
+
+ private:
+  // For each active dictionary, stores local info.
+  // Indexed by server hash.
+  struct DictionaryInfo {
+    base::Time last_used;
+    int use_count;
+    size_t size;
+
+    DictionaryInfo() : use_count(0), size(0) {}
+    DictionaryInfo(const base::Time& last_used, size_t size)
+        : last_used(last_used), use_count(0), size(size) {}
+    DictionaryInfo(const DictionaryInfo& rhs) = default;
+    DictionaryInfo& operator=(const DictionaryInfo& rhs) = default;
+  };
+
+  void OnMemoryPressure(
+      base::MemoryPressureListener::MemoryPressureLevel level);
+
+  net::SdchManager* manager_;
+  net::SdchDictionaryFetcher fetcher_;
+
+  std::map<std::string, DictionaryInfo> local_dictionary_info_;
+  size_t total_dictionary_bytes_;
+
+  scoped_ptr<base::Clock> clock_;
+
+  size_t max_total_dictionary_size_;
+  size_t min_space_for_dictionary_fetch_;
+
+  base::MemoryPressureListener memory_pressure_listener_;
+
+  DISALLOW_COPY_AND_ASSIGN(SdchOwner);
+};
+
+}  // namespace net
+
+#endif  // NET_SDCH_SDCH_OWNER_H_
diff --git a/net/sdch/sdch_owner_unittest.cc b/net/sdch/sdch_owner_unittest.cc
new file mode 100644
index 0000000..e89533a
--- /dev/null
+++ b/net/sdch/sdch_owner_unittest.cc
@@ -0,0 +1,491 @@
+// Copyright 2014 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 "base/memory/memory_pressure_listener.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/simple_test_clock.h"
+#include "net/base/net_log.h"
+#include "net/base/sdch_manager.h"
+#include "net/sdch/sdch_owner.h"
+#include "net/url_request/url_request.h"
+#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_error_job.h"
+#include "net/url_request/url_request_job.h"
+#include "net/url_request/url_request_job_factory.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+static const char generic_url[] = "http://www.example.com";
+static const char generic_domain[] = "www.example.com";
+
+static std::string NewSdchDictionary(size_t dictionary_size) {
+  std::string dictionary;
+  dictionary.append("Domain: ");
+  dictionary.append(generic_domain);
+  dictionary.append("\n");
+  dictionary.append("\n");
+
+  size_t original_dictionary_size = dictionary.size();
+  dictionary.resize(dictionary_size);
+  for (size_t i = original_dictionary_size; i < dictionary_size; ++i)
+    dictionary[i] = static_cast<char>((i % 127) + 1);
+
+  return dictionary;
+}
+
+int outstanding_url_request_error_counting_jobs = 0;
+base::Closure* empty_url_request_jobs_callback = 0;
+
+// Variation of URLRequestErrorJob to count number of outstanding
+// instances and notify when that goes to zero.
+class URLRequestErrorCountingJob : public URLRequestJob {
+ public:
+  URLRequestErrorCountingJob(URLRequest* request,
+                             NetworkDelegate* network_delegate,
+                             int error)
+      : URLRequestJob(request, network_delegate),
+        error_(error),
+        weak_factory_(this) {
+    ++outstanding_url_request_error_counting_jobs;
+  }
+
+  void Start() override {
+    base::MessageLoop::current()->PostTask(
+        FROM_HERE, base::Bind(&URLRequestErrorCountingJob::StartAsync,
+                              weak_factory_.GetWeakPtr()));
+  }
+
+ private:
+  ~URLRequestErrorCountingJob() override {
+    --outstanding_url_request_error_counting_jobs;
+    if (0 == outstanding_url_request_error_counting_jobs &&
+        empty_url_request_jobs_callback) {
+      empty_url_request_jobs_callback->Run();
+    }
+  }
+
+  void StartAsync() {
+    NotifyStartError(URLRequestStatus(URLRequestStatus::FAILED, error_));
+  }
+
+  int error_;
+
+  base::WeakPtrFactory<URLRequestErrorCountingJob> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(URLRequestErrorCountingJob);
+};
+
+static int error_jobs_created = 0;
+
+class MockURLRequestJobFactory : public URLRequestJobFactory {
+ public:
+  MockURLRequestJobFactory() {}
+
+  ~MockURLRequestJobFactory() override {}
+
+  URLRequestJob* MaybeCreateJobWithProtocolHandler(
+      const std::string& scheme,
+      URLRequest* request,
+      NetworkDelegate* network_delegate) const override {
+    ++error_jobs_created;
+    return new URLRequestErrorCountingJob(request, network_delegate,
+                                          ERR_INTERNET_DISCONNECTED);
+  }
+
+  URLRequestJob* MaybeInterceptRedirect(URLRequest* request,
+                                        NetworkDelegate* network_delegate,
+                                        const GURL& location) const override {
+    return nullptr;
+  }
+
+  URLRequestJob* MaybeInterceptResponse(
+      URLRequest* request,
+      NetworkDelegate* network_delegate) const override {
+    return nullptr;
+  }
+
+  bool IsHandledProtocol(const std::string& scheme) const override {
+    return scheme == "http";
+  };
+
+  bool IsHandledURL(const GURL& url) const override {
+    return url.SchemeIs("http");
+  }
+
+  bool IsSafeRedirectTarget(const GURL& location) const override {
+    return false;
+  }
+};
+
+class SdchOwnerTest : public testing::Test {
+ public:
+  static const size_t kMaxSizeForTesting = 1000 * 50;
+  static const size_t kMinFetchSpaceForTesting = 500;
+
+  SdchOwnerTest()
+      : last_jobs_created_(error_jobs_created),
+        dictionary_creation_index_(0),
+        sdch_owner_(&sdch_manager_, &url_request_context_) {
+    // Any jobs created on this context will immediately error,
+    // which leaves the test in control of signals to SdchOwner.
+    url_request_context_.set_job_factory(&job_factory_);
+
+    // Reduce sizes to reduce time for string operations.
+    sdch_owner_.SetMaxTotalDictionarySize(kMaxSizeForTesting);
+    sdch_owner_.SetMinSpaceForDictionaryFetch(kMinFetchSpaceForTesting);
+  }
+
+  SdchManager& sdch_manager() { return sdch_manager_; }
+  SdchOwner& sdch_owner() { return sdch_owner_; }
+  BoundNetLog& bound_net_log() { return net_log_; }
+
+  int JobsRecentlyCreated() {
+    int result = error_jobs_created - last_jobs_created_;
+    last_jobs_created_ = error_jobs_created;
+    return result;
+  }
+
+  bool DictionaryPresentInManager(const std::string& server_hash) {
+    // Presumes all tests use generic url.
+    SdchProblemCode tmp;
+    scoped_ptr<SdchManager::DictionarySet> set(
+        sdch_manager_.GetDictionarySetByHash(GURL(generic_url), server_hash,
+                                             &tmp));
+    return !!set.get();
+  }
+
+  void SignalGetDictionaryAndClearJobs(GURL request_url, GURL dictionary_url) {
+    sdch_owner().OnGetDictionary(&sdch_manager_, request_url, dictionary_url);
+    if (outstanding_url_request_error_counting_jobs == 0)
+      return;
+    base::RunLoop run_loop;
+    base::Closure quit_closure(run_loop.QuitClosure());
+    empty_url_request_jobs_callback = &quit_closure;
+    run_loop.Run();
+    empty_url_request_jobs_callback = NULL;
+  }
+
+  // Create a unique (by hash) dictionary of the given size,
+  // associate it with a unique URL, add it to the manager through
+  // SdchOwner::OnDictionaryFetched(), and return whether that
+  // addition was successful or not.
+  bool CreateAndAddDictionary(size_t size, std::string* server_hash_p) {
+    GURL dictionary_url(
+        base::StringPrintf("%s/d%d", generic_url, dictionary_creation_index_));
+    std::string dictionary_text(NewSdchDictionary(size - 4));
+    dictionary_text += base::StringPrintf("%04d", dictionary_creation_index_);
+    ++dictionary_creation_index_;
+    std::string client_hash;
+    std::string server_hash;
+    SdchManager::GenerateHash(dictionary_text, &client_hash, &server_hash);
+
+    if (DictionaryPresentInManager(server_hash))
+      return false;
+    sdch_owner().OnDictionaryFetched(dictionary_text, dictionary_url, net_log_);
+    if (server_hash_p)
+      *server_hash_p = server_hash;
+    return DictionaryPresentInManager(server_hash);
+  }
+
+ private:
+  int last_jobs_created_;
+  BoundNetLog net_log_;
+  int dictionary_creation_index_;
+
+  // The dependencies of these objects (sdch_owner_ -> {sdch_manager_,
+  // url_request_context_}, url_request_context_->job_factory_) require
+  // this order for correct destruction semantics.
+  MockURLRequestJobFactory job_factory_;
+  URLRequestContext url_request_context_;
+  SdchManager sdch_manager_;
+  SdchOwner sdch_owner_;
+
+  DISALLOW_COPY_AND_ASSIGN(SdchOwnerTest);
+};
+
+// Does OnGetDictionary result in a fetch when there's enough space, and not
+// when there's not?
+TEST_F(SdchOwnerTest, OnGetDictionary_Fetching) {
+  GURL request_url(std::string(generic_url) + "/r1");
+
+  // Fetch generated when empty.
+  GURL dict_url1(std::string(generic_url) + "/d1");
+  EXPECT_EQ(0, JobsRecentlyCreated());
+  SignalGetDictionaryAndClearJobs(request_url, dict_url1);
+  EXPECT_EQ(1, JobsRecentlyCreated());
+
+  // Fetch generated when half full.
+  GURL dict_url2(std::string(generic_url) + "/d2");
+  std::string dictionary1(NewSdchDictionary(kMaxSizeForTesting / 2));
+  sdch_owner().OnDictionaryFetched(dictionary1, dict_url1, bound_net_log());
+  EXPECT_EQ(0, JobsRecentlyCreated());
+  SignalGetDictionaryAndClearJobs(request_url, dict_url2);
+  EXPECT_EQ(1, JobsRecentlyCreated());
+
+  // Fetch not generated when close to completely full.
+  GURL dict_url3(std::string(generic_url) + "/d3");
+  std::string dictionary2(NewSdchDictionary(
+      (kMaxSizeForTesting / 2 - kMinFetchSpaceForTesting / 2)));
+  sdch_owner().OnDictionaryFetched(dictionary2, dict_url2, bound_net_log());
+  EXPECT_EQ(0, JobsRecentlyCreated());
+  SignalGetDictionaryAndClearJobs(request_url, dict_url3);
+  EXPECT_EQ(0, JobsRecentlyCreated());
+}
+
+// Make sure attempts to add dictionaries do what they should.
+TEST_F(SdchOwnerTest, OnDictionaryFetched_Fetching) {
+  GURL request_url(std::string(generic_url) + "/r1");
+  std::string client_hash;
+  std::string server_hash;
+
+  // Add successful when empty.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr));
+  EXPECT_EQ(0, JobsRecentlyCreated());
+
+  // Add successful when half full.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr));
+  EXPECT_EQ(0, JobsRecentlyCreated());
+
+  // Add unsuccessful when full.
+  EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr));
+  EXPECT_EQ(0, JobsRecentlyCreated());
+}
+
+// Confirm auto-eviction happens if space is needed.
+TEST_F(SdchOwnerTest, ConfirmAutoEviction) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+  std::string server_hash_d3;
+
+  // Add two dictionaries, one recent, one more than a day in the past.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d1));
+
+  scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(2));
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d2));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+
+  // The addition of a new dictionary should succeed, evicting the old one.
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now());
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d3));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+}
+
+// Confirm auto-eviction happens if space is needed, with a more complicated
+// situation
+TEST_F(SdchOwnerTest, ConfirmAutoEviction_2) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+  std::string server_hash_d3;
+
+  // Add dictionaries, one recent, two more than a day in the past that
+  // between them add up to the space needed.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d1));
+
+  scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(2));
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2));
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+
+  // The addition of a new dictionary should succeed, evicting the old one.
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now());
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  std::string server_hash_d4;
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
+}
+
+// Confirm if only one dictionary needs to be evicted it's the oldest.
+TEST_F(SdchOwnerTest, ConfirmAutoEviction_Oldest) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+  std::string server_hash_d3;
+
+  // Add dictionaries, one recent, one two days in the past, and one
+  // four days in the past.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1));
+
+  scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(2));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2));
+
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(4));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+
+  // The addition of a new dictionary should succeed, evicting only the
+  // oldest one.
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now());
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  std::string server_hash_d4;
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
+}
+
+// Confirm using a dictionary changes eviction behavior properly.
+TEST_F(SdchOwnerTest, UseChangesEviction) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+  std::string server_hash_d3;
+
+  // Add dictionaries, one recent, one two days in the past, and one
+  // four days in the past.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1));
+
+  scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(2));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2));
+
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(4));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now());
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  // Use the oldest dictionary.
+  sdch_owner().OnDictionaryUsed(&sdch_manager(), server_hash_d3);
+
+  // The addition of a new dictionary should succeed, evicting only the
+  // newer stale one.
+  std::string server_hash_d4;
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4));
+}
+
+// Confirm using a dictionary can prevent the addition of a new dictionary.
+TEST_F(SdchOwnerTest, UsePreventsAddition) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+  std::string server_hash_d3;
+
+  // Add dictionaries, one recent, one two days in the past, and one
+  // four days in the past.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1));
+
+  scoped_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(2));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2));
+
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now() - base::TimeDelta::FromDays(4));
+  sdch_owner().SetClockForTesting(clock.Pass());
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+
+  clock.reset(new base::SimpleTestClock);
+  clock->SetNow(base::Time::Now());
+  sdch_owner().SetClockForTesting(clock.Pass());
+
+  // Use the older dictionaries.
+  sdch_owner().OnDictionaryUsed(&sdch_manager(), server_hash_d2);
+  sdch_owner().OnDictionaryUsed(&sdch_manager(), server_hash_d3);
+
+  // The addition of a new dictionary should fail, not evicting anything.
+  std::string server_hash_d4;
+  EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2));
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d4));
+}
+
+// Confirm clear gets all the space back.
+TEST_F(SdchOwnerTest, ClearReturnsSpace) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+
+  // Take up all the space.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d1));
+
+  // Addition should fail.
+  EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d2));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+
+  sdch_manager().ClearData();
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+
+  // Addition should now succeed.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, nullptr));
+}
+
+// Confirm memory pressure gets all the space back.
+TEST_F(SdchOwnerTest, MemoryPressureReturnsSpace) {
+  std::string server_hash_d1;
+  std::string server_hash_d2;
+
+  // Take up all the space.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d1));
+
+  // Addition should fail.
+  EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d2));
+
+  EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+
+  base::MemoryPressureListener::NotifyMemoryPressure(
+      base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE);
+  // The notification may have (implementation note: does :-}) use a PostTask,
+  // so we drain the local message queue.  This should be safe (i.e. not have
+  // an inifinite number of messages) in a unit test.
+  base::RunLoop().RunUntilIdle();
+
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1));
+  EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2));
+
+  // Addition should now succeed.
+  EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, nullptr));
+}
+
+}  // namespace net
-- 
cgit v1.1