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