1 // Copyright (c) 2006-2008 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 ads 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/logging.h" 32 #include "base/message_loop.h" 33 #include "base/string_util.h" 34 #include "base/time.h" 35 #include "net/disk_cache/backend_impl.h" 36 #include "net/disk_cache/entry_impl.h" 37 #include "net/disk_cache/histogram_macros.h" 38 #include "net/disk_cache/trace.h" 39 40 using base::Time; 41 42 namespace { 43 44 const int kCleanUpMargin = 1024 * 1024; 45 const int kHighUse = 10; // Reuse count to be on the HIGH_USE list. 46 const int kTargetTime = 24 * 7; // Time to be evicted (hours since last use). 47 48 int LowWaterAdjust(int high_water) { 49 if (high_water < kCleanUpMargin) 50 return 0; 51 52 return high_water - kCleanUpMargin; 53 } 54 55 } // namespace 56 57 namespace disk_cache { 58 59 void Eviction::Init(BackendImpl* backend) { 60 // We grab a bunch of info from the backend to make the code a little cleaner 61 // when we're actually doing work. 62 backend_ = backend; 63 rankings_ = &backend->rankings_; 64 header_ = &backend_->data_->header; 65 max_size_ = LowWaterAdjust(backend_->max_size_); 66 new_eviction_ = backend->new_eviction_; 67 first_trim_ = true; 68 trimming_ = false; 69 delay_trim_ = false; 70 } 71 72 void Eviction::TrimCache(bool empty) { 73 if (new_eviction_) 74 return TrimCacheV2(empty); 75 76 if (backend_->disabled_ || trimming_) 77 return; 78 79 if (!empty && backend_->IsLoaded()) 80 return PostDelayedTrim(); 81 82 Trace("*** Trim Cache ***"); 83 trimming_ = true; 84 Time start = Time::Now(); 85 Rankings::ScopedRankingsBlock node(rankings_); 86 Rankings::ScopedRankingsBlock next(rankings_, 87 rankings_->GetPrev(node.get(), Rankings::NO_USE)); 88 int target_size = empty ? 0 : max_size_; 89 while (header_->num_bytes > target_size && next.get()) { 90 // The iterator could be invalidated within EvictEntry(). 91 if (!next->HasData()) 92 break; 93 node.reset(next.release()); 94 next.reset(rankings_->GetPrev(node.get(), Rankings::NO_USE)); 95 if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) { 96 // This entry is not being used by anybody. 97 // Do NOT use node as an iterator after this point. 98 rankings_->TrackRankingsBlock(node.get(), false); 99 if (!EvictEntry(node.get(), empty)) 100 continue; 101 102 if (!empty) { 103 backend_->OnEvent(Stats::TRIM_ENTRY); 104 105 if ((Time::Now() - start).InMilliseconds() > 20) { 106 MessageLoop::current()->PostTask(FROM_HERE, 107 factory_.NewRunnableMethod(&Eviction::TrimCache, false)); 108 break; 109 } 110 } 111 } 112 } 113 114 CACHE_UMA(AGE_MS, "TotalTrimTime", backend_->GetSizeGroup(), start); 115 trimming_ = false; 116 Trace("*** Trim Cache end ***"); 117 return; 118 } 119 120 void Eviction::UpdateRank(EntryImpl* entry, bool modified) { 121 if (new_eviction_) 122 return UpdateRankV2(entry, modified); 123 124 rankings_->UpdateRank(entry->rankings(), modified, GetListForEntry(entry)); 125 } 126 127 void Eviction::OnOpenEntry(EntryImpl* entry) { 128 if (new_eviction_) 129 return OnOpenEntryV2(entry); 130 } 131 132 void Eviction::OnCreateEntry(EntryImpl* entry) { 133 if (new_eviction_) 134 return OnCreateEntryV2(entry); 135 136 rankings_->Insert(entry->rankings(), true, GetListForEntry(entry)); 137 } 138 139 void Eviction::OnDoomEntry(EntryImpl* entry) { 140 if (new_eviction_) 141 return OnDoomEntryV2(entry); 142 143 rankings_->Remove(entry->rankings(), GetListForEntry(entry)); 144 } 145 146 void Eviction::OnDestroyEntry(EntryImpl* entry) { 147 if (new_eviction_) 148 return OnDestroyEntryV2(entry); 149 } 150 151 void Eviction::PostDelayedTrim() { 152 // Prevent posting multiple tasks. 153 if (delay_trim_) 154 return; 155 delay_trim_ = true; 156 MessageLoop::current()->PostDelayedTask(FROM_HERE, 157 factory_.NewRunnableMethod(&Eviction::DelayedTrim), 1000); 158 } 159 160 void Eviction::DelayedTrim() { 161 delay_trim_ = false; 162 TrimCache(false); 163 } 164 165 void Eviction::ReportTrimTimes(EntryImpl* entry) { 166 if (first_trim_) { 167 first_trim_ = false; 168 if (backend_->ShouldReportAgain()) { 169 CACHE_UMA(AGE, "TrimAge", 0, entry->GetLastUsed()); 170 ReportListStats(); 171 } 172 173 if (header_->lru.filled) 174 return; 175 176 header_->lru.filled = 1; 177 178 if (header_->create_time) { 179 // This is the first entry that we have to evict, generate some noise. 180 backend_->FirstEviction(); 181 } else { 182 // This is an old file, but we may want more reports from this user so 183 // lets save some create_time. 184 Time::Exploded old = {0}; 185 old.year = 2009; 186 old.month = 3; 187 old.day_of_month = 1; 188 header_->create_time = Time::FromLocalExploded(old).ToInternalValue(); 189 } 190 } 191 } 192 193 Rankings::List Eviction::GetListForEntry(EntryImpl* entry) { 194 return Rankings::NO_USE; 195 } 196 197 bool Eviction::EvictEntry(CacheRankingsBlock* node, bool empty) { 198 EntryImpl* entry = backend_->GetEnumeratedEntry(node, true); 199 if (!entry) { 200 Trace("NewEntry failed on Trim 0x%x", node->address().value()); 201 return false; 202 } 203 204 ReportTrimTimes(entry); 205 if (empty || !new_eviction_) { 206 entry->Doom(); 207 } else { 208 entry->DeleteEntryData(false); 209 EntryStore* info = entry->entry()->Data(); 210 DCHECK(ENTRY_NORMAL == info->state); 211 212 rankings_->Remove(entry->rankings(), GetListForEntryV2(entry)); 213 info->state = ENTRY_EVICTED; 214 entry->entry()->Store(); 215 rankings_->Insert(entry->rankings(), true, Rankings::DELETED); 216 backend_->OnEvent(Stats::TRIM_ENTRY); 217 } 218 entry->Release(); 219 220 return true; 221 } 222 223 // ----------------------------------------------------------------------- 224 225 void Eviction::TrimCacheV2(bool empty) { 226 if (backend_->disabled_ || trimming_) 227 return; 228 229 if (!empty && backend_->IsLoaded()) 230 return PostDelayedTrim(); 231 232 Trace("*** Trim Cache ***"); 233 trimming_ = true; 234 Time start = Time::Now(); 235 236 const int kListsToSearch = 3; 237 Rankings::ScopedRankingsBlock next[kListsToSearch]; 238 int list = Rankings::LAST_ELEMENT; 239 240 // Get a node from each list. 241 for (int i = 0; i < kListsToSearch; i++) { 242 bool done = false; 243 next[i].set_rankings(rankings_); 244 if (done) 245 continue; 246 next[i].reset(rankings_->GetPrev(NULL, static_cast<Rankings::List>(i))); 247 if (!empty && NodeIsOldEnough(next[i].get(), i)) { 248 list = static_cast<Rankings::List>(i); 249 done = true; 250 } 251 } 252 253 // If we are not meeting the time targets lets move on to list length. 254 if (!empty && Rankings::LAST_ELEMENT == list) { 255 list = SelectListByLenght(); 256 // Make sure that frequently used items are kept for a minimum time; we know 257 // that this entry is not older than its current target, but it must be at 258 // least older than the target for list 0 (kTargetTime). 259 if ((Rankings::HIGH_USE == list || Rankings::LOW_USE == list) && 260 !NodeIsOldEnough(next[list].get(), 0)) 261 list = 0; 262 } 263 264 if (empty) 265 list = 0; 266 267 Rankings::ScopedRankingsBlock node(rankings_); 268 269 int target_size = empty ? 0 : max_size_; 270 for (; list < kListsToSearch; list++) { 271 while (header_->num_bytes > target_size && next[list].get()) { 272 // The iterator could be invalidated within EvictEntry(). 273 if (!next[list]->HasData()) 274 break; 275 node.reset(next[list].release()); 276 next[list].reset(rankings_->GetPrev(node.get(), 277 static_cast<Rankings::List>(list))); 278 if (node->Data()->dirty != backend_->GetCurrentEntryId() || empty) { 279 // This entry is not being used by anybody. 280 // Do NOT use node as an iterator after this point. 281 rankings_->TrackRankingsBlock(node.get(), false); 282 if (!EvictEntry(node.get(), empty)) 283 continue; 284 285 if (!empty && (Time::Now() - start).InMilliseconds() > 20) { 286 MessageLoop::current()->PostTask(FROM_HERE, 287 factory_.NewRunnableMethod(&Eviction::TrimCache, false)); 288 break; 289 } 290 } 291 } 292 if (!empty) 293 list = kListsToSearch; 294 } 295 296 if (empty) { 297 TrimDeleted(true); 298 } else if (header_->lru.sizes[Rankings::DELETED] > header_->num_entries / 4) { 299 MessageLoop::current()->PostTask(FROM_HERE, 300 factory_.NewRunnableMethod(&Eviction::TrimDeleted, empty)); 301 } 302 303 CACHE_UMA(AGE_MS, "TotalTrimTime", backend_->GetSizeGroup(), start); 304 Trace("*** Trim Cache end ***"); 305 trimming_ = false; 306 return; 307 } 308 309 void Eviction::UpdateRankV2(EntryImpl* entry, bool modified) { 310 rankings_->UpdateRank(entry->rankings(), modified, GetListForEntryV2(entry)); 311 } 312 313 void Eviction::OnOpenEntryV2(EntryImpl* entry) { 314 EntryStore* info = entry->entry()->Data(); 315 DCHECK(ENTRY_NORMAL == info->state); 316 317 if (info->reuse_count < kint32max) { 318 info->reuse_count++; 319 entry->entry()->set_modified(); 320 321 // We may need to move this to a new list. 322 if (1 == info->reuse_count) { 323 rankings_->Remove(entry->rankings(), Rankings::NO_USE); 324 rankings_->Insert(entry->rankings(), false, Rankings::LOW_USE); 325 entry->entry()->Store(); 326 } else if (kHighUse == info->reuse_count) { 327 rankings_->Remove(entry->rankings(), Rankings::LOW_USE); 328 rankings_->Insert(entry->rankings(), false, Rankings::HIGH_USE); 329 entry->entry()->Store(); 330 } 331 } 332 } 333 334 void Eviction::OnCreateEntryV2(EntryImpl* entry) { 335 EntryStore* info = entry->entry()->Data(); 336 switch (info->state) { 337 case ENTRY_NORMAL: { 338 DCHECK(!info->reuse_count); 339 DCHECK(!info->refetch_count); 340 break; 341 }; 342 case ENTRY_EVICTED: { 343 if (info->refetch_count < kint32max) 344 info->refetch_count++; 345 346 if (info->refetch_count > kHighUse && info->reuse_count < kHighUse) { 347 info->reuse_count = kHighUse; 348 } else { 349 info->reuse_count++; 350 } 351 info->state = ENTRY_NORMAL; 352 entry->entry()->Store(); 353 rankings_->Remove(entry->rankings(), Rankings::DELETED); 354 break; 355 }; 356 default: 357 NOTREACHED(); 358 } 359 360 rankings_->Insert(entry->rankings(), true, GetListForEntryV2(entry)); 361 } 362 363 void Eviction::OnDoomEntryV2(EntryImpl* entry) { 364 EntryStore* info = entry->entry()->Data(); 365 if (ENTRY_NORMAL != info->state) 366 return; 367 368 rankings_->Remove(entry->rankings(), GetListForEntryV2(entry)); 369 370 info->state = ENTRY_DOOMED; 371 entry->entry()->Store(); 372 rankings_->Insert(entry->rankings(), true, Rankings::DELETED); 373 } 374 375 void Eviction::OnDestroyEntryV2(EntryImpl* entry) { 376 rankings_->Remove(entry->rankings(), Rankings::DELETED); 377 } 378 379 Rankings::List Eviction::GetListForEntryV2(EntryImpl* entry) { 380 EntryStore* info = entry->entry()->Data(); 381 DCHECK(ENTRY_NORMAL == info->state); 382 383 if (!info->reuse_count) 384 return Rankings::NO_USE; 385 386 if (info->reuse_count < kHighUse) 387 return Rankings::LOW_USE; 388 389 return Rankings::HIGH_USE; 390 } 391 392 // This is a minimal implementation that just discards the oldest nodes. 393 // TODO(rvargas): Do something better here. 394 void Eviction::TrimDeleted(bool empty) { 395 Trace("*** Trim Deleted ***"); 396 if (backend_->disabled_) 397 return; 398 399 Time start = Time::Now(); 400 Rankings::ScopedRankingsBlock node(rankings_); 401 Rankings::ScopedRankingsBlock next(rankings_, 402 rankings_->GetPrev(node.get(), Rankings::DELETED)); 403 for (int i = 0; (i < 4 || empty) && next.get(); i++) { 404 node.reset(next.release()); 405 next.reset(rankings_->GetPrev(node.get(), Rankings::DELETED)); 406 RemoveDeletedNode(node.get()); 407 } 408 409 if (header_->lru.sizes[Rankings::DELETED] > header_->num_entries / 4) 410 MessageLoop::current()->PostTask(FROM_HERE, 411 factory_.NewRunnableMethod(&Eviction::TrimDeleted, false)); 412 413 CACHE_UMA(AGE_MS, "TotalTrimDeletedTime", 0, start); 414 Trace("*** Trim Deleted end ***"); 415 return; 416 } 417 418 bool Eviction::RemoveDeletedNode(CacheRankingsBlock* node) { 419 EntryImpl* entry; 420 bool dirty; 421 if (backend_->NewEntry(Addr(node->Data()->contents), &entry, &dirty)) { 422 Trace("NewEntry failed on Trim 0x%x", node->address().value()); 423 return false; 424 } 425 426 // TODO(rvargas): figure out how to deal with corruption at this point (dirty 427 // entries that live in this list). 428 if (node->Data()->dirty) { 429 // We ignore the failure; we're removing the entry anyway. 430 entry->Update(); 431 } 432 entry->entry()->Data()->state = ENTRY_DOOMED; 433 entry->Doom(); 434 entry->Release(); 435 return true; 436 } 437 438 bool Eviction::NodeIsOldEnough(CacheRankingsBlock* node, int list) { 439 if (!node) 440 return false; 441 442 // If possible, we want to keep entries on each list at least kTargetTime 443 // hours. Each successive list on the enumeration has 2x the target time of 444 // the previous list. 445 Time used = Time::FromInternalValue(node->Data()->last_used); 446 int multiplier = 1 << list; 447 return (Time::Now() - used).InHours() > kTargetTime * multiplier; 448 } 449 450 int Eviction::SelectListByLenght() { 451 int data_entries = header_->num_entries - 452 header_->lru.sizes[Rankings::DELETED]; 453 // Start by having each list to be roughly the same size. 454 if (header_->lru.sizes[0] > data_entries / 3) 455 return 0; 456 if (header_->lru.sizes[1] > data_entries / 3) 457 return 1; 458 return 2; 459 } 460 461 void Eviction::ReportListStats() { 462 if (!new_eviction_) 463 return; 464 465 Rankings::ScopedRankingsBlock last1(rankings_, 466 rankings_->GetPrev(NULL, Rankings::NO_USE)); 467 Rankings::ScopedRankingsBlock last2(rankings_, 468 rankings_->GetPrev(NULL, Rankings::LOW_USE)); 469 Rankings::ScopedRankingsBlock last3(rankings_, 470 rankings_->GetPrev(NULL, Rankings::HIGH_USE)); 471 Rankings::ScopedRankingsBlock last4(rankings_, 472 rankings_->GetPrev(NULL, Rankings::DELETED)); 473 474 if (last1.get()) 475 CACHE_UMA(AGE, "NoUseAge", 0, 476 Time::FromInternalValue(last1.get()->Data()->last_used)); 477 if (last2.get()) 478 CACHE_UMA(AGE, "LowUseAge", 0, 479 Time::FromInternalValue(last2.get()->Data()->last_used)); 480 if (last3.get()) 481 CACHE_UMA(AGE, "HighUseAge", 0, 482 Time::FromInternalValue(last3.get()->Data()->last_used)); 483 if (last4.get()) 484 CACHE_UMA(AGE, "DeletedAge", 0, 485 Time::FromInternalValue(last4.get()->Data()->last_used)); 486 } 487 488 } // namespace disk_cache 489