root/net/disk_cache/blockfile/eviction_v3.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. LowWaterAdjust
  2. FallingBehind
  3. ptr_factory_
  4. Init
  5. Stop
  6. TrimCache
  7. OnOpenEntry
  8. OnCreateEntry
  9. SetTestMode
  10. TrimDeletedList
  11. PostDelayedTrim
  12. DelayedTrim
  13. ShouldTrim
  14. ShouldTrimDeleted
  15. EvictEntry
  16. TrimCacheV2
  17. TrimDeleted
  18. ReportTrimTimes
  19. NodeIsOldEnough
  20. SelectListByLength
  21. ReportListStats

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// The eviction policy is a very simple pure LRU, so the elements at the end of
// the list are evicted until kCleanUpMargin free space is available. There is
// only one list in use (Rankings::NO_USE), and elements are sent to the front
// of the list whenever they are accessed.

// The new (in-development) eviction policy adds re-use as a factor to evict
// an entry. The story so far:

// Entries are linked on separate lists depending on how often they are used.
// When we see an element for the first time, it goes to the NO_USE list; if
// the object is reused later on, we move it to the LOW_USE list, until it is
// used kHighUse times, at which point it is moved to the HIGH_USE list.
// Whenever an element is evicted, we move it to the DELETED list so that if the
// element is accessed again, we remember the fact that it was already stored
// and maybe in the future we don't evict that element.

// When we have to evict an element, first we try to use the last element from
// the NO_USE list, then we move to the LOW_USE and only then we evict an entry
// from the HIGH_USE. We attempt to keep entries on the cache for at least
// kTargetTime hours (with frequently accessed items stored for longer periods),
// but if we cannot do that, we fall-back to keep each list roughly the same
// size so that we have a chance to see an element again and move it to another
// list.

#include "net/disk_cache/blockfile/eviction_v3.h"

#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "net/disk_cache/blockfile/backend_impl_v3.h"
#include "net/disk_cache/blockfile/entry_impl_v3.h"
#include "net/disk_cache/blockfile/experiments.h"
#include "net/disk_cache/blockfile/histogram_macros_v3.h"
#include "net/disk_cache/blockfile/trace.h"

#define CACHE_UMA_BACKEND_IMPL_OBJ backend_

using base::Time;
using base::TimeTicks;

namespace {

const int kCleanUpMargin = 1024 * 1024;

#if defined(V3_NOT_JUST_YET_READY)
const int kHighUse = 10;  // Reuse count to be on the HIGH_USE list.
const int kTargetTime = 24 * 7;  // Time to be evicted (hours since last use).
const int kMaxDelayedTrims = 60;
#endif  // defined(V3_NOT_JUST_YET_READY).

int LowWaterAdjust(int high_water) {
  if (high_water < kCleanUpMargin)
    return 0;

  return high_water - kCleanUpMargin;
}

#if defined(V3_NOT_JUST_YET_READY)
bool FallingBehind(int current_size, int max_size) {
  return current_size > max_size - kCleanUpMargin * 20;
}
#endif  // defined(V3_NOT_JUST_YET_READY).

}  // namespace

namespace disk_cache {

// The real initialization happens during Init(), init_ is the only member that
// has to be initialized here.
EvictionV3::EvictionV3()
    : backend_(NULL),
      index_(NULL),
      header_(NULL),
      init_(false),
      ptr_factory_(this) {
}

EvictionV3::~EvictionV3() {
}

void EvictionV3::Init(BackendImplV3* backend) {
  // We grab a bunch of info from the backend to make the code a little cleaner
  // when we're actually doing work.
  backend_ = backend;
  index_ = &backend_->index_;
  header_ = index_->header();
  max_size_ = LowWaterAdjust(backend_->max_size_);
  lru_ = backend->lru_eviction_;
  first_trim_ = true;
  trimming_ = false;
  delay_trim_ = false;
  trim_delays_ = 0;
  init_ = true;
  test_mode_ = false;
}

void EvictionV3::Stop() {
  // It is possible for the backend initialization to fail, in which case this
  // object was never initialized... and there is nothing to do.
  if (!init_)
    return;

  // We want to stop further evictions, so let's pretend that we are busy from
  // this point on.
  DCHECK(!trimming_);
  trimming_ = true;
  ptr_factory_.InvalidateWeakPtrs();
}

#if defined(V3_NOT_JUST_YET_READY)
void EvictionV3::TrimCache() {
  if (backend_->disabled_ || trimming_)
    return;

  if (!empty && !ShouldTrim())
    return PostDelayedTrim();

  if (new_eviction_)
    return TrimCacheV2(empty);

  Trace("*** Trim Cache ***");
  trimming_ = true;
  TimeTicks start = TimeTicks::Now();
  Rankings::ScopedRankingsBlock node(rankings_);
  Rankings::ScopedRankingsBlock next(
      rankings_, rankings_->GetPrev(node.get(), Rankings::NO_USE));
  int deleted_entries = 0;
  int target_size = empty ? 0 : max_size_;
  while ((header_->num_bytes > target_size || test_mode_) && next.get()) {
    // The iterator could be invalidated within EvictEntry().
    if (!next->HasData())
      break;
    node.reset(next.release());
    next.reset(rankings_->GetPrev(node.get(), Rankings::NO_USE));
    if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) {
      // This entry is not being used by anybody.
      // Do NOT use node as an iterator after this point.
      rankings_->TrackRankingsBlock(node.get(), false);
      if (EvictEntry(node.get(), empty, Rankings::NO_USE) && !test_mode_)
        deleted_entries++;

      if (!empty && test_mode_)
        break;
    }
    if (!empty && (deleted_entries > 20 ||
                   (TimeTicks::Now() - start).InMilliseconds() > 20)) {
      base::MessageLoop::current()->PostTask(
          FROM_HERE,
          base::Bind(&EvictionV3::TrimCache, ptr_factory_.GetWeakPtr(), false));
      break;
    }
  }

  if (empty) {
    CACHE_UMA(AGE_MS, "TotalClearTimeV1", 0, start);
  } else {
    CACHE_UMA(AGE_MS, "TotalTrimTimeV1", 0, start);
  }
  CACHE_UMA(COUNTS, "TrimItemsV1", 0, deleted_entries);

  trimming_ = false;
  Trace("*** Trim Cache end ***");
  return;
}

void EvictionV3::OnOpenEntry(EntryImplV3* entry) {
  EntryStore* info = entry->entry()->Data();
  DCHECK_EQ(ENTRY_NORMAL, info->state);

  if (info->reuse_count < kint32max) {
    info->reuse_count++;
    entry->entry()->set_modified();

    // We may need to move this to a new list.
    if (1 == info->reuse_count) {
      rankings_->Remove(entry->rankings(), Rankings::NO_USE, true);
      rankings_->Insert(entry->rankings(), false, Rankings::LOW_USE);
      entry->entry()->Store();
    } else if (kHighUse == info->reuse_count) {
      rankings_->Remove(entry->rankings(), Rankings::LOW_USE, true);
      rankings_->Insert(entry->rankings(), false, Rankings::HIGH_USE);
      entry->entry()->Store();
    }
  }
}

void EvictionV3::OnCreateEntry(EntryImplV3* entry) {
  EntryStore* info = entry->entry()->Data();
  switch (info->state) {
    case ENTRY_NORMAL: {
      DCHECK(!info->reuse_count);
      DCHECK(!info->refetch_count);
      break;
    };
    case ENTRY_EVICTED: {
      if (info->refetch_count < kint32max)
        info->refetch_count++;

      if (info->refetch_count > kHighUse && info->reuse_count < kHighUse) {
        info->reuse_count = kHighUse;
      } else {
        info->reuse_count++;
      }
      info->state = ENTRY_NORMAL;
      entry->entry()->Store();
      rankings_->Remove(entry->rankings(), Rankings::DELETED, true);
      break;
    };
    default:
      NOTREACHED();
  }

  rankings_->Insert(entry->rankings(), true, GetListForEntryV2(entry));
}

void EvictionV3::SetTestMode() {
  test_mode_ = true;
}

void EvictionV3::TrimDeletedList(bool empty) {
  DCHECK(test_mode_ && new_eviction_);
  TrimDeleted(empty);
}

// -----------------------------------------------------------------------

void EvictionV3::PostDelayedTrim() {
  // Prevent posting multiple tasks.
  if (delay_trim_)
    return;
  delay_trim_ = true;
  trim_delays_++;
  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&EvictionV3::DelayedTrim, ptr_factory_.GetWeakPtr()),
      base::TimeDelta::FromMilliseconds(1000));
}

void EvictionV3::DelayedTrim() {
  delay_trim_ = false;
  if (trim_delays_ < kMaxDelayedTrims && backend_->IsLoaded())
    return PostDelayedTrim();

  TrimCache(false);
}

bool EvictionV3::ShouldTrim() {
  if (!FallingBehind(header_->num_bytes, max_size_) &&
      trim_delays_ < kMaxDelayedTrims && backend_->IsLoaded()) {
    return false;
  }

  UMA_HISTOGRAM_COUNTS("DiskCache.TrimDelays", trim_delays_);
  trim_delays_ = 0;
  return true;
}

bool EvictionV3::ShouldTrimDeleted() {
  int index_load = header_->num_entries * 100 / index_size_;

  // If the index is not loaded, the deleted list will tend to double the size
  // of the other lists 3 lists (40% of the total). Otherwise, all lists will be
  // about the same size.
  int max_length = (index_load < 25) ? header_->num_entries * 2 / 5 :
                                       header_->num_entries / 4;
  return (!test_mode_ && header_->lru.sizes[Rankings::DELETED] > max_length);
}

bool Eviction::EvictEntry(CacheRankingsBlock* node, bool empty,
                          Rankings::List list) {
  EntryImplV3* entry = backend_->GetEnumeratedEntry(node, list);
  if (!entry) {
    Trace("NewEntry failed on Trim 0x%x", node->address().value());
    return false;
  }

  ReportTrimTimes(entry);
  if (empty || !new_eviction_) {
    entry->DoomImpl();
  } else {
    entry->DeleteEntryData(false);
    EntryStore* info = entry->entry()->Data();
    DCHECK_EQ(ENTRY_NORMAL, info->state);

    rankings_->Remove(entry->rankings(), GetListForEntryV2(entry), true);
    info->state = ENTRY_EVICTED;
    entry->entry()->Store();
    rankings_->Insert(entry->rankings(), true, Rankings::DELETED);
  }
  if (!empty)
    backend_->OnEvent(Stats::TRIM_ENTRY);

  entry->Release();

  return true;
}

void EvictionV3::TrimCacheV2(bool empty) {
  Trace("*** Trim Cache ***");
  trimming_ = true;
  TimeTicks start = TimeTicks::Now();

  const int kListsToSearch = 3;
  Rankings::ScopedRankingsBlock next[kListsToSearch];
  int list = Rankings::LAST_ELEMENT;

  // Get a node from each list.
  for (int i = 0; i < kListsToSearch; i++) {
    bool done = false;
    next[i].set_rankings(rankings_);
    if (done)
      continue;
    next[i].reset(rankings_->GetPrev(NULL, static_cast<Rankings::List>(i)));
    if (!empty && NodeIsOldEnough(next[i].get(), i)) {
      list = static_cast<Rankings::List>(i);
      done = true;
    }
  }

  // If we are not meeting the time targets lets move on to list length.
  if (!empty && Rankings::LAST_ELEMENT == list)
    list = SelectListByLength(next);

  if (empty)
    list = 0;

  Rankings::ScopedRankingsBlock node(rankings_);
  int deleted_entries = 0;
  int target_size = empty ? 0 : max_size_;

  for (; list < kListsToSearch; list++) {
    while ((header_->num_bytes > target_size || test_mode_) &&
           next[list].get()) {
      // The iterator could be invalidated within EvictEntry().
      if (!next[list]->HasData())
        break;
      node.reset(next[list].release());
      next[list].reset(rankings_->GetPrev(node.get(),
                                          static_cast<Rankings::List>(list)));
      if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) {
        // This entry is not being used by anybody.
        // Do NOT use node as an iterator after this point.
        rankings_->TrackRankingsBlock(node.get(), false);
        if (EvictEntry(node.get(), empty, static_cast<Rankings::List>(list)))
          deleted_entries++;

        if (!empty && test_mode_)
          break;
      }
      if (!empty && (deleted_entries > 20 ||
                     (TimeTicks::Now() - start).InMilliseconds() > 20)) {
        base::MessageLoop::current()->PostTask(
            FROM_HERE,
            base::Bind(&Eviction::TrimCache, ptr_factory_.GetWeakPtr(), false));
        break;
      }
    }
    if (!empty)
      list = kListsToSearch;
  }

  if (empty) {
    TrimDeleted(true);
  } else if (ShouldTrimDeleted()) {
    base::MessageLoop::current()->PostTask(
        FROM_HERE,
        base::Bind(&EvictionV3::TrimDeleted, ptr_factory_.GetWeakPtr(), empty));
  }

  if (empty) {
    CACHE_UMA(AGE_MS, "TotalClearTimeV2", 0, start);
  } else {
    CACHE_UMA(AGE_MS, "TotalTrimTimeV2", 0, start);
  }
  CACHE_UMA(COUNTS, "TrimItemsV2", 0, deleted_entries);

  Trace("*** Trim Cache end ***");
  trimming_ = false;
  return;
}

// This is a minimal implementation that just discards the oldest nodes.
// TODO(rvargas): Do something better here.
void EvictionV3::TrimDeleted(bool empty) {
  Trace("*** Trim Deleted ***");
  if (backend_->disabled_)
    return;

  TimeTicks start = TimeTicks::Now();
  Rankings::ScopedRankingsBlock node(rankings_);
  Rankings::ScopedRankingsBlock next(
    rankings_, rankings_->GetPrev(node.get(), Rankings::DELETED));
  int deleted_entries = 0;
  while (next.get() &&
         (empty || (deleted_entries < 20 &&
                    (TimeTicks::Now() - start).InMilliseconds() < 20))) {
    node.reset(next.release());
    next.reset(rankings_->GetPrev(node.get(), Rankings::DELETED));
    if (RemoveDeletedNode(node.get()))
      deleted_entries++;
    if (test_mode_)
      break;
  }

  if (deleted_entries && !empty && ShouldTrimDeleted()) {
    base::MessageLoop::current()->PostTask(
        FROM_HERE,
        base::Bind(&EvictionV3::TrimDeleted, ptr_factory_.GetWeakPtr(), false));
  }

  CACHE_UMA(AGE_MS, "TotalTrimDeletedTime", 0, start);
  CACHE_UMA(COUNTS, "TrimDeletedItems", 0, deleted_entries);
  Trace("*** Trim Deleted end ***");
  return;
}

void EvictionV3::ReportTrimTimes(EntryImplV3* entry) {
  if (first_trim_) {
    first_trim_ = false;
    if (backend_->ShouldReportAgain()) {
      CACHE_UMA(AGE, "TrimAge", 0, entry->GetLastUsed());
      ReportListStats();
    }

    if (header_->lru.filled)
      return;

    header_->lru.filled = 1;

    if (header_->create_time) {
      // This is the first entry that we have to evict, generate some noise.
      backend_->FirstEviction();
    } else {
      // This is an old file, but we may want more reports from this user so
      // lets save some create_time.
      Time::Exploded old = {0};
      old.year = 2009;
      old.month = 3;
      old.day_of_month = 1;
      header_->create_time = Time::FromLocalExploded(old).ToInternalValue();
    }
  }
}

bool EvictionV3::NodeIsOldEnough(CacheRankingsBlock* node, int list) {
  if (!node)
    return false;

  // If possible, we want to keep entries on each list at least kTargetTime
  // hours. Each successive list on the enumeration has 2x the target time of
  // the previous list.
  Time used = Time::FromInternalValue(node->Data()->last_used);
  int multiplier = 1 << list;
  return (Time::Now() - used).InHours() > kTargetTime * multiplier;
}

int EvictionV3::SelectListByLength(Rankings::ScopedRankingsBlock* next) {
  int data_entries = header_->num_entries -
                     header_->lru.sizes[Rankings::DELETED];
  // Start by having each list to be roughly the same size.
  if (header_->lru.sizes[0] > data_entries / 3)
    return 0;

  int list = (header_->lru.sizes[1] > data_entries / 3) ? 1 : 2;

  // Make sure that frequently used items are kept for a minimum time; we know
  // that this entry is not older than its current target, but it must be at
  // least older than the target for list 0 (kTargetTime), as long as we don't
  // exhaust list 0.
  if (!NodeIsOldEnough(next[list].get(), 0) &&
      header_->lru.sizes[0] > data_entries / 10)
    list = 0;

  return list;
}

void EvictionV3::ReportListStats() {
  if (!new_eviction_)
    return;

  Rankings::ScopedRankingsBlock last1(rankings_,
      rankings_->GetPrev(NULL, Rankings::NO_USE));
  Rankings::ScopedRankingsBlock last2(rankings_,
      rankings_->GetPrev(NULL, Rankings::LOW_USE));
  Rankings::ScopedRankingsBlock last3(rankings_,
      rankings_->GetPrev(NULL, Rankings::HIGH_USE));
  Rankings::ScopedRankingsBlock last4(rankings_,
      rankings_->GetPrev(NULL, Rankings::DELETED));

  if (last1.get())
    CACHE_UMA(AGE, "NoUseAge", 0,
              Time::FromInternalValue(last1.get()->Data()->last_used));
  if (last2.get())
    CACHE_UMA(AGE, "LowUseAge", 0,
              Time::FromInternalValue(last2.get()->Data()->last_used));
  if (last3.get())
    CACHE_UMA(AGE, "HighUseAge", 0,
              Time::FromInternalValue(last3.get()->Data()->last_used));
  if (last4.get())
    CACHE_UMA(AGE, "DeletedAge", 0,
              Time::FromInternalValue(last4.get()->Data()->last_used));
}
#endif  // defined(V3_NOT_JUST_YET_READY).

}  // namespace disk_cache

/* [<][>][^][v][top][bottom][index][help] */