2 Copyright (C) 1998 Lars Knoll (knoll@mpi-hd.mpg.de)
3 Copyright (C) 2001 Dirk Mueller (mueller@kde.org)
4 Copyright (C) 2002 Waldo Bastian (bastian@kde.org)
5 Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License as published by the Free Software Foundation; either
10 version 2 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Library General Public License for more details.
17 You should have received a copy of the GNU Library General Public License
18 along with this library; see the file COPYING.LIB. If not, write to
19 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 Boston, MA 02110-1301, USA.
24 #include "core/fetch/MemoryCache.h"
26 #include "core/dom/CrossThreadTask.h"
27 #include "core/dom/Document.h"
28 #include "core/fetch/ResourcePtr.h"
29 #include "core/frame/FrameView.h"
30 #include "core/workers/WorkerGlobalScope.h"
31 #include "core/workers/WorkerLoaderProxy.h"
32 #include "core/workers/WorkerThread.h"
33 #include "platform/Logging.h"
34 #include "platform/TraceEvent.h"
35 #include "platform/weborigin/SecurityOrigin.h"
36 #include "platform/weborigin/SecurityOriginHash.h"
37 #include "public/platform/Platform.h"
38 #include "wtf/Assertions.h"
39 #include "wtf/CurrentTime.h"
40 #include "wtf/MathExtras.h"
41 #include "wtf/TemporaryChange.h"
42 #include "wtf/text/CString.h"
46 static MemoryCache* gMemoryCache;
48 static const unsigned cDefaultCacheCapacity = 8192 * 1024;
49 static const unsigned cDeferredPruneDeadCapacityFactor = 2;
50 static const int cMinDelayBeforeLiveDecodedPrune = 1; // Seconds.
51 static const double cMaxPruneDeferralDelay = 0.5; // Seconds.
52 static const float cTargetPrunePercentage = .95f; // Percentage of capacity toward which we prune, to avoid immediately pruning again.
54 MemoryCache* memoryCache()
56 ASSERT(WTF::isMainThread());
58 gMemoryCache = new MemoryCache();
62 void setMemoryCacheForTesting(MemoryCache* memoryCache)
64 gMemoryCache = memoryCache;
67 MemoryCache::MemoryCache()
68 : m_inPruneResources(false)
69 , m_prunePending(false)
70 , m_maxPruneDeferralDelay(cMaxPruneDeferralDelay)
71 , m_capacity(cDefaultCacheCapacity)
72 , m_minDeadCapacity(0)
73 , m_maxDeadCapacity(cDefaultCacheCapacity)
74 , m_maxDeferredPruneDeadCapacity(cDeferredPruneDeadCapacityFactor * cDefaultCacheCapacity)
75 , m_delayBeforeLiveDecodedPrune(cMinDelayBeforeLiveDecodedPrune)
78 #ifdef MEMORY_CACHE_STATS
79 , m_statsTimer(this, &MemoryCache::dumpStats)
82 #ifdef MEMORY_CACHE_STATS
83 const double statsIntervalInSeconds = 15;
84 m_statsTimer.startRepeating(statsIntervalInSeconds);
86 m_pruneTimeStamp = m_pruneFrameTimeStamp = FrameView::currentFrameTimeStamp();
89 MemoryCache::~MemoryCache()
92 blink::Platform::current()->currentThread()->removeTaskObserver(this);
95 KURL MemoryCache::removeFragmentIdentifierIfNeeded(const KURL& originalURL)
97 if (!originalURL.hasFragmentIdentifier())
99 // Strip away fragment identifier from HTTP URLs.
100 // Data URLs must be unmodified. For file and custom URLs clients may expect resources
101 // to be unique even when they differ by the fragment identifier only.
102 if (!originalURL.protocolIsInHTTPFamily())
104 KURL url = originalURL;
105 url.removeFragmentIdentifier();
109 void MemoryCache::add(Resource* resource)
111 ASSERT(WTF::isMainThread());
112 m_resources.set(resource->url(), resource);
113 resource->setInCache(true);
114 resource->updateForAccess();
116 WTF_LOG(ResourceLoading, "MemoryCache::add Added '%s', resource %p\n", resource->url().string().latin1().data(), resource);
119 void MemoryCache::replace(Resource* newResource, Resource* oldResource)
122 ASSERT(!m_resources.get(newResource->url()));
123 m_resources.set(newResource->url(), newResource);
124 newResource->setInCache(true);
125 insertInLRUList(newResource);
126 int delta = newResource->size();
127 if (newResource->decodedSize() && newResource->hasClients())
128 insertInLiveDecodedResourcesList(newResource);
130 adjustSize(newResource->hasClients(), delta);
133 Resource* MemoryCache::resourceForURL(const KURL& resourceURL)
135 ASSERT(WTF::isMainThread());
136 KURL url = removeFragmentIdentifierIfNeeded(resourceURL);
137 Resource* resource = m_resources.get(url);
138 if (resource && !resource->lock()) {
139 ASSERT(!resource->hasClients());
140 bool didEvict = evict(resource);
141 ASSERT_UNUSED(didEvict, didEvict);
147 size_t MemoryCache::deadCapacity() const
149 // Dead resource capacity is whatever space is not occupied by live resources, bounded by an independent minimum and maximum.
150 size_t capacity = m_capacity - std::min(m_liveSize, m_capacity); // Start with available capacity.
151 capacity = std::max(capacity, m_minDeadCapacity); // Make sure it's above the minimum.
152 capacity = std::min(capacity, m_maxDeadCapacity); // Make sure it's below the maximum.
156 size_t MemoryCache::liveCapacity() const
158 // Live resource capacity is whatever is left over after calculating dead resource capacity.
159 return m_capacity - deadCapacity();
162 void MemoryCache::pruneLiveResources()
164 ASSERT(!m_prunePending);
165 size_t capacity = liveCapacity();
166 if (!m_liveSize || (capacity && m_liveSize <= capacity))
169 size_t targetSize = static_cast<size_t>(capacity * cTargetPrunePercentage); // Cut by a percentage to avoid immediately pruning again.
171 // Destroy any decoded data in live objects that we can.
172 // Start from the tail, since this is the lowest priority
173 // and least recently accessed of the objects.
175 // The list might not be sorted by the m_lastDecodedFrameTimeStamp. The impact
176 // of this weaker invariant is minor as the below if statement to check the
177 // elapsedTime will evaluate to false as the current time will be a lot
178 // greater than the current->m_lastDecodedFrameTimeStamp.
179 // For more details see: https://bugs.webkit.org/show_bug.cgi?id=30209
181 // Start pruning from the lowest priority list.
182 for (int priority = Resource::CacheLiveResourcePriorityLow; priority <= Resource::CacheLiveResourcePriorityHigh; ++priority) {
183 Resource* current = m_liveDecodedResources[priority].m_tail;
185 Resource* prev = current->m_prevInLiveResourcesList;
186 ASSERT(current->hasClients());
187 if (current->isLoaded() && current->decodedSize()) {
188 // Check to see if the remaining resources are too new to prune.
189 double elapsedTime = m_pruneFrameTimeStamp - current->m_lastDecodedAccessTime;
190 if (elapsedTime < m_delayBeforeLiveDecodedPrune)
193 // Destroy our decoded data if possible. This will remove us
194 // from m_liveDecodedResources, and possibly move us to a
195 // different LRU list in m_allResources.
198 if (targetSize && m_liveSize <= targetSize)
206 void MemoryCache::pruneDeadResources()
208 size_t capacity = deadCapacity();
209 if (!m_deadSize || (capacity && m_deadSize <= capacity))
212 size_t targetSize = static_cast<size_t>(capacity * cTargetPrunePercentage); // Cut by a percentage to avoid immediately pruning again.
214 int size = m_allResources.size();
216 // See if we have any purged resources we can evict.
217 for (int i = 0; i < size; i++) {
218 Resource* current = m_allResources[i].m_tail;
220 Resource* prev = current->m_prevInAllResourcesList;
221 if (current->wasPurged()) {
222 ASSERT(!current->hasClients());
223 ASSERT(!current->isPreloaded());
229 if (targetSize && m_deadSize <= targetSize)
232 bool canShrinkLRULists = true;
233 for (int i = size - 1; i >= 0; i--) {
234 // Remove from the tail, since this is the least frequently accessed of the objects.
235 Resource* current = m_allResources[i].m_tail;
237 // First flush all the decoded data in this queue.
239 // Protect 'previous' so it can't get deleted during destroyDecodedData().
240 ResourcePtr<Resource> previous = current->m_prevInAllResourcesList;
241 ASSERT(!previous || previous->inCache());
242 if (!current->hasClients() && !current->isPreloaded() && current->isLoaded()) {
243 // Destroy our decoded data if possible. This will remove us
244 // from m_liveDecodedResources, and possibly move us to a
245 // different LRU list in m_allResources.
248 if (targetSize && m_deadSize <= targetSize)
251 // Decoded data may reference other resources. Stop iterating if 'previous' somehow got
252 // kicked out of cache during destroyDecodedData().
253 if (previous && !previous->inCache())
255 current = previous.get();
258 // Now evict objects from this queue.
259 current = m_allResources[i].m_tail;
261 ResourcePtr<Resource> previous = current->m_prevInAllResourcesList;
262 ASSERT(!previous || previous->inCache());
263 if (!current->hasClients() && !current->isPreloaded() && !current->isCacheValidator()) {
265 if (targetSize && m_deadSize <= targetSize)
268 if (previous && !previous->inCache())
270 current = previous.get();
273 // Shrink the vector back down so we don't waste time inspecting
274 // empty LRU lists on future prunes.
275 if (m_allResources[i].m_head)
276 canShrinkLRULists = false;
277 else if (canShrinkLRULists)
278 m_allResources.resize(i);
282 void MemoryCache::setCapacities(size_t minDeadBytes, size_t maxDeadBytes, size_t totalBytes)
284 ASSERT(minDeadBytes <= maxDeadBytes);
285 ASSERT(maxDeadBytes <= totalBytes);
286 m_minDeadCapacity = minDeadBytes;
287 m_maxDeadCapacity = maxDeadBytes;
288 m_maxDeferredPruneDeadCapacity = cDeferredPruneDeadCapacityFactor * maxDeadBytes;
289 m_capacity = totalBytes;
293 bool MemoryCache::evict(Resource* resource)
295 ASSERT(WTF::isMainThread());
296 WTF_LOG(ResourceLoading, "Evicting resource %p for '%s' from cache", resource, resource->url().string().latin1().data());
297 // The resource may have already been removed by someone other than our caller,
298 // who needed a fresh copy for a reload. See <http://bugs.webkit.org/show_bug.cgi?id=12479#c6>.
299 if (resource->inCache()) {
300 // Remove from the resource map.
301 m_resources.remove(resource->url());
302 resource->setInCache(false);
304 // Remove from the appropriate LRU list.
305 removeFromLRUList(resource);
306 removeFromLiveDecodedResourcesList(resource);
307 adjustSize(resource->hasClients(), -static_cast<ptrdiff_t>(resource->size()));
309 ASSERT(m_resources.get(resource->url()) != resource);
312 return resource->deleteIfPossible();
315 MemoryCache::LRUList* MemoryCache::lruListFor(Resource* resource)
317 unsigned accessCount = std::max(resource->accessCount(), 1U);
318 unsigned queueIndex = WTF::fastLog2(resource->size() / accessCount);
320 resource->m_lruIndex = queueIndex;
322 if (m_allResources.size() <= queueIndex)
323 m_allResources.grow(queueIndex + 1);
324 return &m_allResources[queueIndex];
327 void MemoryCache::removeFromLRUList(Resource* resource)
329 // If we've never been accessed, then we're brand new and not in any list.
330 if (!resource->accessCount())
334 unsigned oldListIndex = resource->m_lruIndex;
337 LRUList* list = lruListFor(resource);
340 // Verify that the list we got is the list we want.
341 ASSERT(resource->m_lruIndex == oldListIndex);
343 // Verify that we are in fact in this list.
345 for (Resource* current = list->m_head; current; current = current->m_nextInAllResourcesList) {
346 if (current == resource) {
354 Resource* next = resource->m_nextInAllResourcesList;
355 Resource* prev = resource->m_prevInAllResourcesList;
357 if (!next && !prev && list->m_head != resource)
360 resource->m_nextInAllResourcesList = 0;
361 resource->m_prevInAllResourcesList = 0;
364 next->m_prevInAllResourcesList = prev;
365 else if (list->m_tail == resource)
369 prev->m_nextInAllResourcesList = next;
370 else if (list->m_head == resource)
374 void MemoryCache::insertInLRUList(Resource* resource)
376 // Make sure we aren't in some list already.
377 ASSERT(!resource->m_nextInAllResourcesList && !resource->m_prevInAllResourcesList);
378 ASSERT(resource->inCache());
379 ASSERT(resource->accessCount() > 0);
381 LRUList* list = lruListFor(resource);
383 resource->m_nextInAllResourcesList = list->m_head;
385 list->m_head->m_prevInAllResourcesList = resource;
386 list->m_head = resource;
388 if (!resource->m_nextInAllResourcesList)
389 list->m_tail = resource;
392 // Verify that we are in now in the list like we should be.
393 list = lruListFor(resource);
395 for (Resource* current = list->m_head; current; current = current->m_nextInAllResourcesList) {
396 if (current == resource) {
406 void MemoryCache::removeFromLiveDecodedResourcesList(Resource* resource)
408 // If we've never been accessed, then we're brand new and not in any list.
409 if (!resource->m_inLiveDecodedResourcesList)
411 resource->m_inLiveDecodedResourcesList = false;
413 LRUList* list = &m_liveDecodedResources[resource->cacheLiveResourcePriority()];
416 // Verify that we are in fact in this list.
418 for (Resource* current = list->m_head; current; current = current->m_nextInLiveResourcesList) {
419 if (current == resource) {
427 Resource* next = resource->m_nextInLiveResourcesList;
428 Resource* prev = resource->m_prevInLiveResourcesList;
430 if (!next && !prev && list->m_head != resource)
433 resource->m_nextInLiveResourcesList = 0;
434 resource->m_prevInLiveResourcesList = 0;
437 next->m_prevInLiveResourcesList = prev;
438 else if (list->m_tail == resource)
442 prev->m_nextInLiveResourcesList = next;
443 else if (list->m_head == resource)
447 void MemoryCache::insertInLiveDecodedResourcesList(Resource* resource)
449 // Make sure we aren't in the list already.
450 ASSERT(!resource->m_nextInLiveResourcesList && !resource->m_prevInLiveResourcesList && !resource->m_inLiveDecodedResourcesList);
451 resource->m_inLiveDecodedResourcesList = true;
453 LRUList* list = &m_liveDecodedResources[resource->cacheLiveResourcePriority()];
454 resource->m_nextInLiveResourcesList = list->m_head;
456 list->m_head->m_prevInLiveResourcesList = resource;
457 list->m_head = resource;
459 if (!resource->m_nextInLiveResourcesList)
460 list->m_tail = resource;
463 // Verify that we are in now in the list like we should be.
465 for (Resource* current = list->m_head; current; current = current->m_nextInLiveResourcesList) {
466 if (current == resource) {
476 void MemoryCache::addToLiveResourcesSize(Resource* resource)
478 ASSERT(m_deadSize >= resource->size());
479 m_liveSize += resource->size();
480 m_deadSize -= resource->size();
483 void MemoryCache::removeFromLiveResourcesSize(Resource* resource)
485 ASSERT(m_liveSize >= resource->size());
486 m_liveSize -= resource->size();
487 m_deadSize += resource->size();
490 void MemoryCache::adjustSize(bool live, ptrdiff_t delta)
493 ASSERT(delta >= 0 || m_liveSize >= static_cast<size_t>(-delta) );
496 ASSERT(delta >= 0 || m_deadSize >= static_cast<size_t>(-delta) );
501 void MemoryCache::removeURLFromCache(ExecutionContext* context, const KURL& url)
503 if (context->isWorkerGlobalScope()) {
504 WorkerGlobalScope* workerGlobalScope = toWorkerGlobalScope(context);
505 workerGlobalScope->thread()->workerLoaderProxy().postTaskToLoader(createCallbackTask(&removeURLFromCacheInternal, url));
508 removeURLFromCacheInternal(context, url);
511 void MemoryCache::removeURLFromCacheInternal(ExecutionContext*, const KURL& url)
513 if (Resource* resource = memoryCache()->resourceForURL(url))
514 memoryCache()->remove(resource);
517 void MemoryCache::TypeStatistic::addResource(Resource* o)
519 bool purged = o->wasPurged();
520 bool purgeable = o->isPurgeable() && !purged;
521 size_t pageSize = (o->encodedSize() + o->overheadSize() + 4095) & ~4095;
523 size += purged ? 0 : o->size();
524 liveSize += o->hasClients() ? o->size() : 0;
525 decodedSize += o->decodedSize();
526 encodedSize += o->encodedSize();
527 encodedSizeDuplicatedInDataURLs += o->url().protocolIsData() ? o->encodedSize() : 0;
528 purgeableSize += purgeable ? pageSize : 0;
529 purgedSize += purged ? pageSize : 0;
532 MemoryCache::Statistics MemoryCache::getStatistics()
535 ResourceMap::iterator e = m_resources.end();
536 for (ResourceMap::iterator i = m_resources.begin(); i != e; ++i) {
537 Resource* resource = i->value;
538 switch (resource->type()) {
539 case Resource::Image:
540 stats.images.addResource(resource);
542 case Resource::CSSStyleSheet:
543 stats.cssStyleSheets.addResource(resource);
545 case Resource::Script:
546 stats.scripts.addResource(resource);
548 case Resource::XSLStyleSheet:
549 stats.xslStyleSheets.addResource(resource);
552 stats.fonts.addResource(resource);
555 stats.other.addResource(resource);
562 void MemoryCache::evictResources()
565 ResourceMap::iterator i = m_resources.begin();
566 if (i == m_resources.end())
572 void MemoryCache::prune(Resource* justReleasedResource)
574 TRACE_EVENT0("renderer", "MemoryCache::prune()");
576 if (m_inPruneResources)
578 if (m_liveSize + m_deadSize <= m_capacity && m_maxDeadCapacity && m_deadSize <= m_maxDeadCapacity) // Fast path.
581 // To avoid burdening the current thread with repetitive pruning jobs,
582 // pruning is postponed until the end of the current task. If it has
583 // been more that m_maxPruneDeferralDelay since the last prune,
584 // then we prune immediately.
585 // If the current thread's run loop is not active, then pruning will happen
586 // immediately only if it has been over m_maxPruneDeferralDelay
587 // since the last prune.
588 double currentTime = WTF::currentTime();
589 if (m_prunePending) {
590 if (currentTime - m_pruneTimeStamp >= m_maxPruneDeferralDelay) {
591 pruneNow(currentTime);
594 if (currentTime - m_pruneTimeStamp >= m_maxPruneDeferralDelay) {
595 pruneNow(currentTime); // Delay exceeded, prune now.
598 blink::Platform::current()->currentThread()->addTaskObserver(this);
599 m_prunePending = true;
603 if (m_prunePending && m_deadSize > m_maxDeferredPruneDeadCapacity && justReleasedResource) {
604 // The following eviction does not respect LRU order, but it can be done
605 // immediately in constant time, as opposed to pruneDeadResources, which
606 // we would rather defer because it is O(N), which would make tear-down of N
607 // objects O(N^2) if we pruned immediately. This immediate eviction is a
608 // safeguard against runaway memory consumption by dead resources
609 // while a prune is pending.
610 evict(justReleasedResource);
612 // As a last resort, prune immediately
613 if (m_deadSize > m_maxDeferredPruneDeadCapacity)
614 pruneNow(currentTime);
618 void MemoryCache::willProcessTask()
622 void MemoryCache::didProcessTask()
624 // Perform deferred pruning
625 ASSERT(m_prunePending);
626 pruneNow(WTF::currentTime());
629 void MemoryCache::pruneNow(double currentTime)
631 if (m_prunePending) {
632 m_prunePending = false;
633 blink::Platform::current()->currentThread()->removeTaskObserver(this);
636 TemporaryChange<bool> reentrancyProtector(m_inPruneResources, true);
637 pruneDeadResources(); // Prune dead first, in case it was "borrowing" capacity from live.
638 pruneLiveResources();
639 m_pruneFrameTimeStamp = FrameView::currentFrameTimeStamp();
640 m_pruneTimeStamp = currentTime;
643 #ifdef MEMORY_CACHE_STATS
645 void MemoryCache::dumpStats(Timer<MemoryCache>*)
647 Statistics s = getStatistics();
648 printf("%-13s %-13s %-13s %-13s %-13s %-13s %-13s\n", "", "Count", "Size", "LiveSize", "DecodedSize", "PurgeableSize", "PurgedSize");
649 printf("%-13s %-13s %-13s %-13s %-13s %-13s %-13s\n", "-------------", "-------------", "-------------", "-------------", "-------------", "-------------", "-------------");
650 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "Images", s.images.count, s.images.size, s.images.liveSize, s.images.decodedSize, s.images.purgeableSize, s.images.purgedSize);
651 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "CSS", s.cssStyleSheets.count, s.cssStyleSheets.size, s.cssStyleSheets.liveSize, s.cssStyleSheets.decodedSize, s.cssStyleSheets.purgeableSize, s.cssStyleSheets.purgedSize);
652 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "XSL", s.xslStyleSheets.count, s.xslStyleSheets.size, s.xslStyleSheets.liveSize, s.xslStyleSheets.decodedSize, s.xslStyleSheets.purgeableSize, s.xslStyleSheets.purgedSize);
653 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "JavaScript", s.scripts.count, s.scripts.size, s.scripts.liveSize, s.scripts.decodedSize, s.scripts.purgeableSize, s.scripts.purgedSize);
654 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "Fonts", s.fonts.count, s.fonts.size, s.fonts.liveSize, s.fonts.decodedSize, s.fonts.purgeableSize, s.fonts.purgedSize);
655 printf("%-13s %13d %13d %13d %13d %13d %13d\n", "Other", s.other.count, s.other.size, s.other.liveSize, s.other.decodedSize, s.other.purgeableSize, s.other.purgedSize);
656 printf("%-13s %-13s %-13s %-13s %-13s %-13s %-13s\n\n", "-------------", "-------------", "-------------", "-------------", "-------------", "-------------", "-------------");
658 printf("Duplication of encoded data from data URLs\n");
659 printf("%-13s %13d of %13d\n", "Images", s.images.encodedSizeDuplicatedInDataURLs, s.images.encodedSize);
660 printf("%-13s %13d of %13d\n", "CSS", s.cssStyleSheets.encodedSizeDuplicatedInDataURLs, s.cssStyleSheets.encodedSize);
661 printf("%-13s %13d of %13d\n", "XSL", s.xslStyleSheets.encodedSizeDuplicatedInDataURLs, s.xslStyleSheets.encodedSize);
662 printf("%-13s %13d of %13d\n", "JavaScript", s.scripts.encodedSizeDuplicatedInDataURLs, s.scripts.encodedSize);
663 printf("%-13s %13d of %13d\n", "Fonts", s.fonts.encodedSizeDuplicatedInDataURLs, s.fonts.encodedSize);
664 printf("%-13s %13d of %13d\n", "Other", s.other.encodedSizeDuplicatedInDataURLs, s.other.encodedSize);
667 void MemoryCache::dumpLRULists(bool includeLive) const
669 printf("LRU-SP lists in eviction order (Kilobytes decoded, Kilobytes encoded, Access count, Referenced, isPurgeable, wasPurged):\n");
671 int size = m_allResources.size();
672 for (int i = size - 1; i >= 0; i--) {
673 printf("\n\nList %d: ", i);
674 Resource* current = m_allResources[i].m_tail;
676 Resource* prev = current->m_prevInAllResourcesList;
677 if (includeLive || !current->hasClients())
678 printf("(%.1fK, %.1fK, %uA, %dR, %d, %d); ", current->decodedSize() / 1024.0f, (current->encodedSize() + current->overheadSize()) / 1024.0f, current->accessCount(), current->hasClients(), current->isPurgeable(), current->wasPurged());
685 #endif // MEMORY_CACHE_STATS
687 } // namespace WebCore