Home | History | Annotate | Download | only in disk_cache
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 // The eviction policy is a very simple pure LRU, so the elements at the end of
      6 // the list are evicted until kCleanUpMargin free space is available. There is
      7 // only one list in use (Rankings::NO_USE), and elements are sent to the front
      8 // of the list whenever they are accessed.
      9 
     10 // The new (in-development) eviction policy adds re-use as a factor to evict
     11 // an entry. The story so far:
     12 
     13 // Entries are linked on separate lists depending on how often they are used.
     14 // When we see an element for the first time, it goes to the NO_USE list; if
     15 // the object is reused later on, we move it to the LOW_USE list, until it is
     16 // used kHighUse times, at which point it is moved to the HIGH_USE list.
     17 // Whenever an element is evicted, we move it to the DELETED list so that if the
     18 // element is accessed again, we remember the fact that it was already stored
     19 // and maybe in the future we don't evict that element.
     20 
     21 // When we have to evict an element, first we try to use the last element from
     22 // the NO_USE list, then we move to the LOW_USE and only then we evict an entry
     23 // from the HIGH_USE. We attempt to keep entries on the cache for at least
     24 // kTargetTime hours (with frequently accessed items stored for longer periods),
     25 // but if we cannot do that, we fall-back to keep each list roughly the same
     26 // size so that we have a chance to see an element again and move it to another
     27 // list.
     28 
     29 #include "net/disk_cache/eviction.h"
     30 
     31 #include "base/compiler_specific.h"
     32 #include "base/logging.h"
     33 #include "base/message_loop.h"
     34 #include "base/string_util.h"
     35 #include "base/time.h"
     36 #include "net/disk_cache/backend_impl.h"
     37 #include "net/disk_cache/entry_impl.h"
     38 #include "net/disk_cache/experiments.h"
     39 #include "net/disk_cache/histogram_macros.h"
     40 #include "net/disk_cache/trace.h"
     41 
     42 using base::Time;
     43 using base::TimeTicks;
     44 
     45 namespace {
     46 
     47 const int kCleanUpMargin = 1024 * 1024;
     48 const int kHighUse = 10;  // Reuse count to be on the HIGH_USE list.
     49 const int kTargetTime = 24 * 7;  // Time to be evicted (hours since last use).
     50 const int kMaxDelayedTrims = 60;
     51 
     52 int LowWaterAdjust(int high_water) {
     53   if (high_water < kCleanUpMargin)
     54     return 0;
     55 
     56   return high_water - kCleanUpMargin;
     57 }
     58 
     59 }  // namespace
     60 
     61 namespace disk_cache {
     62 
     63 Eviction::Eviction()
     64     : backend_(NULL),
     65       init_(false),
     66       ALLOW_THIS_IN_INITIALIZER_LIST(factory_(this)) {
     67 }
     68 
     69 Eviction::~Eviction() {
     70 }
     71 
     72 void Eviction::Init(BackendImpl* backend) {
     73   // We grab a bunch of info from the backend to make the code a little cleaner
     74   // when we're actually doing work.
     75   backend_ = backend;
     76   rankings_ = &backend->rankings_;
     77   header_ = &backend_->data_->header;
     78   max_size_ = LowWaterAdjust(backend_->max_size_);
     79   new_eviction_ = backend->new_eviction_;
     80   first_trim_ = true;
     81   trimming_ = false;
     82   delay_trim_ = false;
     83   trim_delays_ = 0;
     84   init_ = true;
     85   test_mode_ = false;
     86   in_experiment_ = (header_->experiment == EXPERIMENT_DELETED_LIST_IN);
     87 }
     88 
     89 void Eviction::Stop() {
     90   // It is possible for the backend initialization to fail, in which case this
     91   // object was never initialized... and there is nothing to do.
     92   if (!init_)
     93     return;
     94 
     95   // We want to stop further evictions, so let's pretend that we are busy from
     96   // this point on.
     97   DCHECK(!trimming_);
     98   trimming_ = true;
     99   factory_.RevokeAll();
    100 }
    101 
    102 void Eviction::TrimCache(bool empty) {
    103   if (backend_->disabled_ || trimming_)
    104     return;
    105 
    106   if (!empty && !ShouldTrim())
    107     return PostDelayedTrim();
    108 
    109   if (new_eviction_)
    110     return TrimCacheV2(empty);
    111 
    112   Trace("*** Trim Cache ***");
    113   trimming_ = true;
    114   TimeTicks start = TimeTicks::Now();
    115   Rankings::ScopedRankingsBlock node(rankings_);
    116   Rankings::ScopedRankingsBlock next(rankings_,
    117       rankings_->GetPrev(node.get(), Rankings::NO_USE));
    118   int target_size = empty ? 0 : max_size_;
    119   while ((header_->num_bytes > target_size || test_mode_) && next.get()) {
    120     // The iterator could be invalidated within EvictEntry().
    121     if (!next->HasData())
    122       break;
    123     node.reset(next.release());
    124     next.reset(rankings_->GetPrev(node.get(), Rankings::NO_USE));
    125     if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) {
    126       // This entry is not being used by anybody.
    127       // Do NOT use node as an iterator after this point.
    128       rankings_->TrackRankingsBlock(node.get(), false);
    129       if (!EvictEntry(node.get(), empty, Rankings::NO_USE) && !test_mode_)
    130         continue;
    131 
    132       if (!empty) {
    133         backend_->OnEvent(Stats::TRIM_ENTRY);
    134         if (test_mode_)
    135           break;
    136 
    137         if ((TimeTicks::Now() - start).InMilliseconds() > 20) {
    138           MessageLoop::current()->PostTask(FROM_HERE,
    139               factory_.NewRunnableMethod(&Eviction::TrimCache, false));
    140           break;
    141         }
    142       }
    143     }
    144   }
    145 
    146   if (empty) {
    147     CACHE_UMA(AGE_MS, "TotalClearTimeV1", 0, start);
    148   } else {
    149     CACHE_UMA(AGE_MS, "TotalTrimTimeV1", backend_->GetSizeGroup(), start);
    150   }
    151 
    152   trimming_ = false;
    153   Trace("*** Trim Cache end ***");
    154   return;
    155 }
    156 
    157 void Eviction::UpdateRank(EntryImpl* entry, bool modified) {
    158   if (new_eviction_)
    159     return UpdateRankV2(entry, modified);
    160 
    161   rankings_->UpdateRank(entry->rankings(), modified, GetListForEntry(entry));
    162 }
    163 
    164 void Eviction::OnOpenEntry(EntryImpl* entry) {
    165   if (new_eviction_)
    166     return OnOpenEntryV2(entry);
    167 }
    168 
    169 void Eviction::OnCreateEntry(EntryImpl* entry) {
    170   if (new_eviction_)
    171     return OnCreateEntryV2(entry);
    172 
    173   rankings_->Insert(entry->rankings(), true, GetListForEntry(entry));
    174 }
    175 
    176 void Eviction::OnDoomEntry(EntryImpl* entry) {
    177   if (new_eviction_)
    178     return OnDoomEntryV2(entry);
    179 
    180   if (entry->LeaveRankingsBehind())
    181     return;
    182 
    183   rankings_->Remove(entry->rankings(), GetListForEntry(entry), true);
    184 }
    185 
    186 void Eviction::OnDestroyEntry(EntryImpl* entry) {
    187   if (new_eviction_)
    188     return OnDestroyEntryV2(entry);
    189 }
    190 
    191 void Eviction::SetTestMode() {
    192   test_mode_ = true;
    193 }
    194 
    195 void Eviction::TrimDeletedList(bool empty) {
    196   DCHECK(test_mode_ && new_eviction_);
    197   TrimDeleted(empty);
    198 }
    199 
    200 void Eviction::PostDelayedTrim() {
    201   // Prevent posting multiple tasks.
    202   if (delay_trim_)
    203     return;
    204   delay_trim_ = true;
    205   trim_delays_++;
    206   MessageLoop::current()->PostDelayedTask(FROM_HERE,
    207       factory_.NewRunnableMethod(&Eviction::DelayedTrim), 1000);
    208 }
    209 
    210 void Eviction::DelayedTrim() {
    211   delay_trim_ = false;
    212   if (trim_delays_ < kMaxDelayedTrims && backend_->IsLoaded())
    213     return PostDelayedTrim();
    214 
    215   TrimCache(false);
    216 }
    217 
    218 bool Eviction::ShouldTrim() {
    219   if (trim_delays_ < kMaxDelayedTrims && backend_->IsLoaded())
    220     return false;
    221 
    222   UMA_HISTOGRAM_COUNTS("DiskCache.TrimDelays", trim_delays_);
    223   trim_delays_ = 0;
    224   return true;
    225 }
    226 
    227 void Eviction::ReportTrimTimes(EntryImpl* entry) {
    228   if (first_trim_) {
    229     first_trim_ = false;
    230     if (backend_->ShouldReportAgain()) {
    231       CACHE_UMA(AGE, "TrimAge", 0, entry->GetLastUsed());
    232       ReportListStats();
    233     }
    234 
    235     if (header_->lru.filled)
    236       return;
    237 
    238     header_->lru.filled = 1;
    239 
    240     if (header_->create_time) {
    241       // This is the first entry that we have to evict, generate some noise.
    242       backend_->FirstEviction();
    243     } else {
    244       // This is an old file, but we may want more reports from this user so
    245       // lets save some create_time.
    246       Time::Exploded old = {0};
    247       old.year = 2009;
    248       old.month = 3;
    249       old.day_of_month = 1;
    250       header_->create_time = Time::FromLocalExploded(old).ToInternalValue();
    251     }
    252   }
    253 }
    254 
    255 Rankings::List Eviction::GetListForEntry(EntryImpl* entry) {
    256   return Rankings::NO_USE;
    257 }
    258 
    259 bool Eviction::EvictEntry(CacheRankingsBlock* node, bool empty,
    260                           Rankings::List list) {
    261   EntryImpl* entry = backend_->GetEnumeratedEntry(node, list);
    262   if (!entry) {
    263     Trace("NewEntry failed on Trim 0x%x", node->address().value());
    264     return false;
    265   }
    266 
    267   ReportTrimTimes(entry);
    268   if (empty || !new_eviction_) {
    269     entry->DoomImpl();
    270   } else {
    271     entry->DeleteEntryData(false);
    272     EntryStore* info = entry->entry()->Data();
    273     DCHECK(ENTRY_NORMAL == info->state);
    274 
    275     rankings_->Remove(entry->rankings(), GetListForEntryV2(entry), true);
    276     info->state = ENTRY_EVICTED;
    277     entry->entry()->Store();
    278     rankings_->Insert(entry->rankings(), true, Rankings::DELETED);
    279     backend_->OnEvent(Stats::TRIM_ENTRY);
    280   }
    281   entry->Release();
    282 
    283   return true;
    284 }
    285 
    286 // -----------------------------------------------------------------------
    287 
    288 void Eviction::TrimCacheV2(bool empty) {
    289   Trace("*** Trim Cache ***");
    290   trimming_ = true;
    291   TimeTicks start = TimeTicks::Now();
    292 
    293   const int kListsToSearch = 3;
    294   Rankings::ScopedRankingsBlock next[kListsToSearch];
    295   int list = Rankings::LAST_ELEMENT;
    296 
    297   // Get a node from each list.
    298   for (int i = 0; i < kListsToSearch; i++) {
    299     bool done = false;
    300     next[i].set_rankings(rankings_);
    301     if (done)
    302       continue;
    303     next[i].reset(rankings_->GetPrev(NULL, static_cast<Rankings::List>(i)));
    304     if (!empty && NodeIsOldEnough(next[i].get(), i)) {
    305       list = static_cast<Rankings::List>(i);
    306       done = true;
    307     }
    308   }
    309 
    310   // If we are not meeting the time targets lets move on to list length.
    311   if (!empty && Rankings::LAST_ELEMENT == list)
    312     list = SelectListByLength(next);
    313 
    314   if (empty)
    315     list = 0;
    316 
    317   Rankings::ScopedRankingsBlock node(rankings_);
    318 
    319   int target_size = empty ? 0 : max_size_;
    320   for (; list < kListsToSearch; list++) {
    321     while ((header_->num_bytes > target_size || test_mode_) &&
    322         next[list].get()) {
    323       // The iterator could be invalidated within EvictEntry().
    324       if (!next[list]->HasData())
    325         break;
    326       node.reset(next[list].release());
    327       next[list].reset(rankings_->GetPrev(node.get(),
    328                                           static_cast<Rankings::List>(list)));
    329       if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) {
    330         // This entry is not being used by anybody.
    331         // Do NOT use node as an iterator after this point.
    332         rankings_->TrackRankingsBlock(node.get(), false);
    333         if (!EvictEntry(node.get(), empty, static_cast<Rankings::List>(list)) &&
    334             !test_mode_)
    335           continue;
    336 
    337         if (!empty && test_mode_)
    338           break;
    339 
    340         if (!empty && (TimeTicks::Now() - start).InMilliseconds() > 20) {
    341           MessageLoop::current()->PostTask(FROM_HERE,
    342               factory_.NewRunnableMethod(&Eviction::TrimCache, false));
    343           break;
    344         }
    345       }
    346     }
    347     if (!empty)
    348       list = kListsToSearch;
    349   }
    350 
    351   if (empty) {
    352     TrimDeleted(true);
    353   } else if (header_->lru.sizes[Rankings::DELETED] > header_->num_entries / 4 &&
    354              !test_mode_) {
    355     MessageLoop::current()->PostTask(FROM_HERE,
    356         factory_.NewRunnableMethod(&Eviction::TrimDeleted, empty));
    357   }
    358 
    359   if (empty) {
    360     CACHE_UMA(AGE_MS, "TotalClearTimeV2", 0, start);
    361   } else {
    362     CACHE_UMA(AGE_MS, "TotalTrimTimeV2", backend_->GetSizeGroup(), start);
    363   }
    364 
    365   Trace("*** Trim Cache end ***");
    366   trimming_ = false;
    367   return;
    368 }
    369 
    370 void Eviction::UpdateRankV2(EntryImpl* entry, bool modified) {
    371   rankings_->UpdateRank(entry->rankings(), modified, GetListForEntryV2(entry));
    372 }
    373 
    374 void Eviction::OnOpenEntryV2(EntryImpl* entry) {
    375   EntryStore* info = entry->entry()->Data();
    376   DCHECK(ENTRY_NORMAL == info->state);
    377 
    378   if (info->reuse_count < kint32max) {
    379     info->reuse_count++;
    380     entry->entry()->set_modified();
    381 
    382     // We may need to move this to a new list.
    383     if (1 == info->reuse_count) {
    384       rankings_->Remove(entry->rankings(), Rankings::NO_USE, true);
    385       rankings_->Insert(entry->rankings(), false, Rankings::LOW_USE);
    386       entry->entry()->Store();
    387     } else if (kHighUse == info->reuse_count) {
    388       rankings_->Remove(entry->rankings(), Rankings::LOW_USE, true);
    389       rankings_->Insert(entry->rankings(), false, Rankings::HIGH_USE);
    390       entry->entry()->Store();
    391     }
    392   }
    393 }
    394 
    395 void Eviction::OnCreateEntryV2(EntryImpl* entry) {
    396   EntryStore* info = entry->entry()->Data();
    397   switch (info->state) {
    398     case ENTRY_NORMAL: {
    399       DCHECK(!info->reuse_count);
    400       DCHECK(!info->refetch_count);
    401       break;
    402     };
    403     case ENTRY_EVICTED: {
    404       if (info->refetch_count < kint32max)
    405         info->refetch_count++;
    406 
    407       if (info->refetch_count > kHighUse && info->reuse_count < kHighUse) {
    408         info->reuse_count = kHighUse;
    409       } else {
    410         info->reuse_count++;
    411       }
    412       info->state = ENTRY_NORMAL;
    413       entry->entry()->Store();
    414       rankings_->Remove(entry->rankings(), Rankings::DELETED, true);
    415       break;
    416     };
    417     default:
    418       NOTREACHED();
    419   }
    420 
    421   rankings_->Insert(entry->rankings(), true, GetListForEntryV2(entry));
    422 }
    423 
    424 void Eviction::OnDoomEntryV2(EntryImpl* entry) {
    425   EntryStore* info = entry->entry()->Data();
    426   if (ENTRY_NORMAL != info->state)
    427     return;
    428 
    429   if (entry->LeaveRankingsBehind()) {
    430     info->state = ENTRY_DOOMED;
    431     entry->entry()->Store();
    432     return;
    433   }
    434 
    435   rankings_->Remove(entry->rankings(), GetListForEntryV2(entry), true);
    436 
    437   info->state = ENTRY_DOOMED;
    438   entry->entry()->Store();
    439   rankings_->Insert(entry->rankings(), true, Rankings::DELETED);
    440 }
    441 
    442 void Eviction::OnDestroyEntryV2(EntryImpl* entry) {
    443   if (entry->LeaveRankingsBehind())
    444     return;
    445 
    446   rankings_->Remove(entry->rankings(), Rankings::DELETED, true);
    447 }
    448 
    449 Rankings::List Eviction::GetListForEntryV2(EntryImpl* entry) {
    450   EntryStore* info = entry->entry()->Data();
    451   DCHECK(ENTRY_NORMAL == info->state);
    452 
    453   if (!info->reuse_count)
    454     return Rankings::NO_USE;
    455 
    456   if (info->reuse_count < kHighUse)
    457     return Rankings::LOW_USE;
    458 
    459   return Rankings::HIGH_USE;
    460 }
    461 
    462 // This is a minimal implementation that just discards the oldest nodes.
    463 // TODO(rvargas): Do something better here.
    464 void Eviction::TrimDeleted(bool empty) {
    465   Trace("*** Trim Deleted ***");
    466   if (backend_->disabled_)
    467     return;
    468 
    469   TimeTicks start = TimeTicks::Now();
    470   Rankings::ScopedRankingsBlock node(rankings_);
    471   Rankings::ScopedRankingsBlock next(rankings_,
    472     rankings_->GetPrev(node.get(), Rankings::DELETED));
    473   bool deleted = false;
    474   while (next.get() &&
    475       (empty || (TimeTicks::Now() - start).InMilliseconds() < 20)) {
    476     node.reset(next.release());
    477     next.reset(rankings_->GetPrev(node.get(), Rankings::DELETED));
    478     deleted |= RemoveDeletedNode(node.get());
    479     if (test_mode_)
    480       break;
    481   }
    482 
    483   // Normally we use 25% for each list. The experiment doubles the number of
    484   // deleted entries, so the total number of entries increases by 25%. Using
    485   // 40% of that value for deleted entries leaves the size of the other three
    486   // lists intact.
    487   int max_length = in_experiment_ ? header_->num_entries * 2 / 5 :
    488                                     header_->num_entries / 4;
    489   if (deleted && !empty && !test_mode_ &&
    490       header_->lru.sizes[Rankings::DELETED] > max_length) {
    491     MessageLoop::current()->PostTask(FROM_HERE,
    492         factory_.NewRunnableMethod(&Eviction::TrimDeleted, false));
    493   }
    494 
    495   CACHE_UMA(AGE_MS, "TotalTrimDeletedTime", 0, start);
    496   Trace("*** Trim Deleted end ***");
    497   return;
    498 }
    499 
    500 bool Eviction::RemoveDeletedNode(CacheRankingsBlock* node) {
    501   EntryImpl* entry = backend_->GetEnumeratedEntry(node, Rankings::DELETED);
    502   if (!entry) {
    503     Trace("NewEntry failed on Trim 0x%x", node->address().value());
    504     return false;
    505   }
    506 
    507   bool doomed = (entry->entry()->Data()->state == ENTRY_DOOMED);
    508   entry->entry()->Data()->state = ENTRY_DOOMED;
    509   entry->DoomImpl();
    510   entry->Release();
    511   return !doomed;
    512 }
    513 
    514 bool Eviction::NodeIsOldEnough(CacheRankingsBlock* node, int list) {
    515   if (!node)
    516     return false;
    517 
    518   // If possible, we want to keep entries on each list at least kTargetTime
    519   // hours. Each successive list on the enumeration has 2x the target time of
    520   // the previous list.
    521   Time used = Time::FromInternalValue(node->Data()->last_used);
    522   int multiplier = 1 << list;
    523   return (Time::Now() - used).InHours() > kTargetTime * multiplier;
    524 }
    525 
    526 int Eviction::SelectListByLength(Rankings::ScopedRankingsBlock* next) {
    527   int data_entries = header_->num_entries -
    528                      header_->lru.sizes[Rankings::DELETED];
    529   // Start by having each list to be roughly the same size.
    530   if (header_->lru.sizes[0] > data_entries / 3)
    531     return 0;
    532 
    533   int list = (header_->lru.sizes[1] > data_entries / 3) ? 1 : 2;
    534 
    535   // Make sure that frequently used items are kept for a minimum time; we know
    536   // that this entry is not older than its current target, but it must be at
    537   // least older than the target for list 0 (kTargetTime), as long as we don't
    538   // exhaust list 0.
    539   if (!NodeIsOldEnough(next[list].get(), 0) &&
    540       header_->lru.sizes[0] > data_entries / 10)
    541     list = 0;
    542 
    543   return list;
    544 }
    545 
    546 void Eviction::ReportListStats() {
    547   if (!new_eviction_)
    548     return;
    549 
    550   Rankings::ScopedRankingsBlock last1(rankings_,
    551       rankings_->GetPrev(NULL, Rankings::NO_USE));
    552   Rankings::ScopedRankingsBlock last2(rankings_,
    553       rankings_->GetPrev(NULL, Rankings::LOW_USE));
    554   Rankings::ScopedRankingsBlock last3(rankings_,
    555       rankings_->GetPrev(NULL, Rankings::HIGH_USE));
    556   Rankings::ScopedRankingsBlock last4(rankings_,
    557       rankings_->GetPrev(NULL, Rankings::DELETED));
    558 
    559   if (last1.get())
    560     CACHE_UMA(AGE, "NoUseAge", 0,
    561               Time::FromInternalValue(last1.get()->Data()->last_used));
    562   if (last2.get())
    563     CACHE_UMA(AGE, "LowUseAge", 0,
    564               Time::FromInternalValue(last2.get()->Data()->last_used));
    565   if (last3.get())
    566     CACHE_UMA(AGE, "HighUseAge", 0,
    567               Time::FromInternalValue(last3.get()->Data()->last_used));
    568   if (last4.get())
    569     CACHE_UMA(AGE, "DeletedAge", 0,
    570               Time::FromInternalValue(last4.get()->Data()->last_used));
    571 }
    572 
    573 }  // namespace disk_cache
    574