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