1 /* 2 * Copyright 2013 Google Inc. 3 * 4 * Use of this source code is governed by a BSD-style license that can be 5 * found in the LICENSE file. 6 */ 7 8 #include "SkChecksum.h" 9 #include "SkMessageBus.h" 10 #include "SkMipMap.h" 11 #include "SkMutex.h" 12 #include "SkPixelRef.h" 13 #include "SkResourceCache.h" 14 #include "SkTraceMemoryDump.h" 15 16 #include <stddef.h> 17 #include <stdlib.h> 18 19 DECLARE_SKMESSAGEBUS_MESSAGE(SkResourceCache::PurgeSharedIDMessage) 20 21 // This can be defined by the caller's build system 22 //#define SK_USE_DISCARDABLE_SCALEDIMAGECACHE 23 24 #ifndef SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT 25 # define SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT 1024 26 #endif 27 28 #ifndef SK_DEFAULT_IMAGE_CACHE_LIMIT 29 #define SK_DEFAULT_IMAGE_CACHE_LIMIT (32 * 1024 * 1024) 30 #endif 31 32 void SkResourceCache::Key::init(void* nameSpace, uint64_t sharedID, size_t dataSize) { 33 SkASSERT(SkAlign4(dataSize) == dataSize); 34 35 // fCount32 and fHash are not hashed 36 static const int kUnhashedLocal32s = 2; // fCache32 + fHash 37 static const int kSharedIDLocal32s = 2; // fSharedID_lo + fSharedID_hi 38 static const int kHashedLocal32s = kSharedIDLocal32s + (sizeof(fNamespace) >> 2); 39 static const int kLocal32s = kUnhashedLocal32s + kHashedLocal32s; 40 41 static_assert(sizeof(Key) == (kLocal32s << 2), "unaccounted_key_locals"); 42 static_assert(sizeof(Key) == offsetof(Key, fNamespace) + sizeof(fNamespace), 43 "namespace_field_must_be_last"); 44 45 fCount32 = SkToS32(kLocal32s + (dataSize >> 2)); 46 fSharedID_lo = (uint32_t)sharedID; 47 fSharedID_hi = (uint32_t)(sharedID >> 32); 48 fNamespace = nameSpace; 49 // skip unhashed fields when computing the murmur 50 fHash = SkChecksum::Murmur3(this->as32() + kUnhashedLocal32s, 51 (fCount32 - kUnhashedLocal32s) << 2); 52 } 53 54 #include "SkTDynamicHash.h" 55 56 class SkResourceCache::Hash : 57 public SkTDynamicHash<SkResourceCache::Rec, SkResourceCache::Key> {}; 58 59 60 /////////////////////////////////////////////////////////////////////////////// 61 62 void SkResourceCache::init() { 63 fHead = nullptr; 64 fTail = nullptr; 65 fHash = new Hash; 66 fTotalBytesUsed = 0; 67 fCount = 0; 68 fSingleAllocationByteLimit = 0; 69 fAllocator = nullptr; 70 71 // One of these should be explicit set by the caller after we return. 72 fTotalByteLimit = 0; 73 fDiscardableFactory = nullptr; 74 } 75 76 #include "SkDiscardableMemory.h" 77 78 class SkOneShotDiscardablePixelRef : public SkPixelRef { 79 public: 80 81 // Ownership of the discardablememory is transfered to the pixelref 82 // The pixelref will ref() the colortable (if not NULL), and unref() in destructor 83 SkOneShotDiscardablePixelRef(const SkImageInfo&, SkDiscardableMemory*, size_t rowBytes, 84 SkColorTable*); 85 ~SkOneShotDiscardablePixelRef(); 86 87 protected: 88 bool onNewLockPixels(LockRec*) override; 89 void onUnlockPixels() override; 90 size_t getAllocatedSizeInBytes() const override; 91 92 SkDiscardableMemory* diagnostic_only_getDiscardable() const override { return fDM; } 93 94 private: 95 SkDiscardableMemory* fDM; 96 size_t fRB; 97 bool fFirstTime; 98 SkColorTable* fCTable; 99 100 typedef SkPixelRef INHERITED; 101 }; 102 103 SkOneShotDiscardablePixelRef::SkOneShotDiscardablePixelRef(const SkImageInfo& info, 104 SkDiscardableMemory* dm, 105 size_t rowBytes, 106 SkColorTable* ctable) 107 : INHERITED(info) 108 , fDM(dm) 109 , fRB(rowBytes) 110 , fCTable(ctable) 111 { 112 SkASSERT(dm->data()); 113 fFirstTime = true; 114 SkSafeRef(ctable); 115 } 116 117 SkOneShotDiscardablePixelRef::~SkOneShotDiscardablePixelRef() { 118 delete fDM; 119 SkSafeUnref(fCTable); 120 } 121 122 bool SkOneShotDiscardablePixelRef::onNewLockPixels(LockRec* rec) { 123 if (fFirstTime) { 124 // we're already locked 125 SkASSERT(fDM->data()); 126 fFirstTime = false; 127 goto SUCCESS; 128 } 129 130 // A previous call to onUnlock may have deleted our DM, so check for that 131 if (nullptr == fDM) { 132 return false; 133 } 134 135 if (!fDM->lock()) { 136 // since it failed, we delete it now, to free-up the resource 137 delete fDM; 138 fDM = nullptr; 139 return false; 140 } 141 142 SUCCESS: 143 rec->fPixels = fDM->data(); 144 rec->fColorTable = fCTable; 145 rec->fRowBytes = fRB; 146 return true; 147 } 148 149 void SkOneShotDiscardablePixelRef::onUnlockPixels() { 150 SkASSERT(!fFirstTime); 151 fDM->unlock(); 152 } 153 154 size_t SkOneShotDiscardablePixelRef::getAllocatedSizeInBytes() const { 155 return this->info().getSafeSize(fRB); 156 } 157 158 class SkResourceCacheDiscardableAllocator : public SkBitmap::Allocator { 159 public: 160 SkResourceCacheDiscardableAllocator(SkResourceCache::DiscardableFactory factory) { 161 SkASSERT(factory); 162 fFactory = factory; 163 } 164 165 bool allocPixelRef(SkBitmap*, SkColorTable*) override; 166 167 private: 168 SkResourceCache::DiscardableFactory fFactory; 169 }; 170 171 bool SkResourceCacheDiscardableAllocator::allocPixelRef(SkBitmap* bitmap, SkColorTable* ctable) { 172 size_t size = bitmap->getSize(); 173 uint64_t size64 = bitmap->computeSize64(); 174 if (0 == size || size64 > (uint64_t)size) { 175 return false; 176 } 177 178 if (kIndex_8_SkColorType == bitmap->colorType()) { 179 if (!ctable) { 180 return false; 181 } 182 } else { 183 ctable = nullptr; 184 } 185 186 SkDiscardableMemory* dm = fFactory(size); 187 if (nullptr == dm) { 188 return false; 189 } 190 191 SkImageInfo info = bitmap->info(); 192 bitmap->setPixelRef(new SkOneShotDiscardablePixelRef(info, dm, bitmap->rowBytes(), 193 ctable))->unref(); 194 bitmap->lockPixels(); 195 return bitmap->readyToDraw(); 196 } 197 198 SkResourceCache::SkResourceCache(DiscardableFactory factory) { 199 this->init(); 200 fDiscardableFactory = factory; 201 202 fAllocator = new SkResourceCacheDiscardableAllocator(factory); 203 } 204 205 SkResourceCache::SkResourceCache(size_t byteLimit) { 206 this->init(); 207 fTotalByteLimit = byteLimit; 208 } 209 210 SkResourceCache::~SkResourceCache() { 211 SkSafeUnref(fAllocator); 212 213 Rec* rec = fHead; 214 while (rec) { 215 Rec* next = rec->fNext; 216 delete rec; 217 rec = next; 218 } 219 delete fHash; 220 } 221 222 //////////////////////////////////////////////////////////////////////////////// 223 224 bool SkResourceCache::find(const Key& key, FindVisitor visitor, void* context) { 225 this->checkMessages(); 226 227 Rec* rec = fHash->find(key); 228 if (rec) { 229 if (visitor(*rec, context)) { 230 this->moveToHead(rec); // for our LRU 231 return true; 232 } else { 233 this->remove(rec); // stale 234 return false; 235 } 236 } 237 return false; 238 } 239 240 static void make_size_str(size_t size, SkString* str) { 241 const char suffix[] = { 'b', 'k', 'm', 'g', 't', 0 }; 242 int i = 0; 243 while (suffix[i] && (size > 1024)) { 244 i += 1; 245 size >>= 10; 246 } 247 str->printf("%zu%c", size, suffix[i]); 248 } 249 250 static bool gDumpCacheTransactions; 251 252 void SkResourceCache::add(Rec* rec) { 253 this->checkMessages(); 254 255 SkASSERT(rec); 256 // See if we already have this key (racy inserts, etc.) 257 Rec* existing = fHash->find(rec->getKey()); 258 if (existing) { 259 delete rec; 260 return; 261 } 262 263 this->addToHead(rec); 264 fHash->add(rec); 265 266 if (gDumpCacheTransactions) { 267 SkString bytesStr, totalStr; 268 make_size_str(rec->bytesUsed(), &bytesStr); 269 make_size_str(fTotalBytesUsed, &totalStr); 270 SkDebugf("RC: add %5s %12p key %08x -- total %5s, count %d\n", 271 bytesStr.c_str(), rec, rec->getHash(), totalStr.c_str(), fCount); 272 } 273 274 // since the new rec may push us over-budget, we perform a purge check now 275 this->purgeAsNeeded(); 276 } 277 278 void SkResourceCache::remove(Rec* rec) { 279 size_t used = rec->bytesUsed(); 280 SkASSERT(used <= fTotalBytesUsed); 281 282 this->detach(rec); 283 fHash->remove(rec->getKey()); 284 285 fTotalBytesUsed -= used; 286 fCount -= 1; 287 288 if (gDumpCacheTransactions) { 289 SkString bytesStr, totalStr; 290 make_size_str(used, &bytesStr); 291 make_size_str(fTotalBytesUsed, &totalStr); 292 SkDebugf("RC: remove %5s %12p key %08x -- total %5s, count %d\n", 293 bytesStr.c_str(), rec, rec->getHash(), totalStr.c_str(), fCount); 294 } 295 296 delete rec; 297 } 298 299 void SkResourceCache::purgeAsNeeded(bool forcePurge) { 300 size_t byteLimit; 301 int countLimit; 302 303 if (fDiscardableFactory) { 304 countLimit = SK_DISCARDABLEMEMORY_SCALEDIMAGECACHE_COUNT_LIMIT; 305 byteLimit = SK_MaxU32; // no limit based on bytes 306 } else { 307 countLimit = SK_MaxS32; // no limit based on count 308 byteLimit = fTotalByteLimit; 309 } 310 311 Rec* rec = fTail; 312 while (rec) { 313 if (!forcePurge && fTotalBytesUsed < byteLimit && fCount < countLimit) { 314 break; 315 } 316 317 Rec* prev = rec->fPrev; 318 this->remove(rec); 319 rec = prev; 320 } 321 } 322 323 //#define SK_TRACK_PURGE_SHAREDID_HITRATE 324 325 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 326 static int gPurgeCallCounter; 327 static int gPurgeHitCounter; 328 #endif 329 330 void SkResourceCache::purgeSharedID(uint64_t sharedID) { 331 if (0 == sharedID) { 332 return; 333 } 334 335 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 336 gPurgeCallCounter += 1; 337 bool found = false; 338 #endif 339 // go backwards, just like purgeAsNeeded, just to make the code similar. 340 // could iterate either direction and still be correct. 341 Rec* rec = fTail; 342 while (rec) { 343 Rec* prev = rec->fPrev; 344 if (rec->getKey().getSharedID() == sharedID) { 345 // SkDebugf("purgeSharedID id=%llx rec=%p\n", sharedID, rec); 346 this->remove(rec); 347 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 348 found = true; 349 #endif 350 } 351 rec = prev; 352 } 353 354 #ifdef SK_TRACK_PURGE_SHAREDID_HITRATE 355 if (found) { 356 gPurgeHitCounter += 1; 357 } 358 359 SkDebugf("PurgeShared calls=%d hits=%d rate=%g\n", gPurgeCallCounter, gPurgeHitCounter, 360 gPurgeHitCounter * 100.0 / gPurgeCallCounter); 361 #endif 362 } 363 364 void SkResourceCache::visitAll(Visitor visitor, void* context) { 365 // go backwards, just like purgeAsNeeded, just to make the code similar. 366 // could iterate either direction and still be correct. 367 Rec* rec = fTail; 368 while (rec) { 369 visitor(*rec, context); 370 rec = rec->fPrev; 371 } 372 } 373 374 /////////////////////////////////////////////////////////////////////////////////////////////////// 375 376 size_t SkResourceCache::setTotalByteLimit(size_t newLimit) { 377 size_t prevLimit = fTotalByteLimit; 378 fTotalByteLimit = newLimit; 379 if (newLimit < prevLimit) { 380 this->purgeAsNeeded(); 381 } 382 return prevLimit; 383 } 384 385 SkCachedData* SkResourceCache::newCachedData(size_t bytes) { 386 this->checkMessages(); 387 388 if (fDiscardableFactory) { 389 SkDiscardableMemory* dm = fDiscardableFactory(bytes); 390 return dm ? new SkCachedData(bytes, dm) : nullptr; 391 } else { 392 return new SkCachedData(sk_malloc_throw(bytes), bytes); 393 } 394 } 395 396 /////////////////////////////////////////////////////////////////////////////// 397 398 void SkResourceCache::detach(Rec* rec) { 399 Rec* prev = rec->fPrev; 400 Rec* next = rec->fNext; 401 402 if (!prev) { 403 SkASSERT(fHead == rec); 404 fHead = next; 405 } else { 406 prev->fNext = next; 407 } 408 409 if (!next) { 410 fTail = prev; 411 } else { 412 next->fPrev = prev; 413 } 414 415 rec->fNext = rec->fPrev = nullptr; 416 } 417 418 void SkResourceCache::moveToHead(Rec* rec) { 419 if (fHead == rec) { 420 return; 421 } 422 423 SkASSERT(fHead); 424 SkASSERT(fTail); 425 426 this->validate(); 427 428 this->detach(rec); 429 430 fHead->fPrev = rec; 431 rec->fNext = fHead; 432 fHead = rec; 433 434 this->validate(); 435 } 436 437 void SkResourceCache::addToHead(Rec* rec) { 438 this->validate(); 439 440 rec->fPrev = nullptr; 441 rec->fNext = fHead; 442 if (fHead) { 443 fHead->fPrev = rec; 444 } 445 fHead = rec; 446 if (!fTail) { 447 fTail = rec; 448 } 449 fTotalBytesUsed += rec->bytesUsed(); 450 fCount += 1; 451 452 this->validate(); 453 } 454 455 /////////////////////////////////////////////////////////////////////////////// 456 457 #ifdef SK_DEBUG 458 void SkResourceCache::validate() const { 459 if (nullptr == fHead) { 460 SkASSERT(nullptr == fTail); 461 SkASSERT(0 == fTotalBytesUsed); 462 return; 463 } 464 465 if (fHead == fTail) { 466 SkASSERT(nullptr == fHead->fPrev); 467 SkASSERT(nullptr == fHead->fNext); 468 SkASSERT(fHead->bytesUsed() == fTotalBytesUsed); 469 return; 470 } 471 472 SkASSERT(nullptr == fHead->fPrev); 473 SkASSERT(fHead->fNext); 474 SkASSERT(nullptr == fTail->fNext); 475 SkASSERT(fTail->fPrev); 476 477 size_t used = 0; 478 int count = 0; 479 const Rec* rec = fHead; 480 while (rec) { 481 count += 1; 482 used += rec->bytesUsed(); 483 SkASSERT(used <= fTotalBytesUsed); 484 rec = rec->fNext; 485 } 486 SkASSERT(fCount == count); 487 488 rec = fTail; 489 while (rec) { 490 SkASSERT(count > 0); 491 count -= 1; 492 SkASSERT(used >= rec->bytesUsed()); 493 used -= rec->bytesUsed(); 494 rec = rec->fPrev; 495 } 496 497 SkASSERT(0 == count); 498 SkASSERT(0 == used); 499 } 500 #endif 501 502 void SkResourceCache::dump() const { 503 this->validate(); 504 505 SkDebugf("SkResourceCache: count=%d bytes=%d %s\n", 506 fCount, fTotalBytesUsed, fDiscardableFactory ? "discardable" : "malloc"); 507 } 508 509 size_t SkResourceCache::setSingleAllocationByteLimit(size_t newLimit) { 510 size_t oldLimit = fSingleAllocationByteLimit; 511 fSingleAllocationByteLimit = newLimit; 512 return oldLimit; 513 } 514 515 size_t SkResourceCache::getSingleAllocationByteLimit() const { 516 return fSingleAllocationByteLimit; 517 } 518 519 size_t SkResourceCache::getEffectiveSingleAllocationByteLimit() const { 520 // fSingleAllocationByteLimit == 0 means the caller is asking for our default 521 size_t limit = fSingleAllocationByteLimit; 522 523 // if we're not discardable (i.e. we are fixed-budget) then cap the single-limit 524 // to our budget. 525 if (nullptr == fDiscardableFactory) { 526 if (0 == limit) { 527 limit = fTotalByteLimit; 528 } else { 529 limit = SkTMin(limit, fTotalByteLimit); 530 } 531 } 532 return limit; 533 } 534 535 void SkResourceCache::checkMessages() { 536 SkTArray<PurgeSharedIDMessage> msgs; 537 fPurgeSharedIDInbox.poll(&msgs); 538 for (int i = 0; i < msgs.count(); ++i) { 539 this->purgeSharedID(msgs[i].fSharedID); 540 } 541 } 542 543 /////////////////////////////////////////////////////////////////////////////// 544 545 SK_DECLARE_STATIC_MUTEX(gMutex); 546 static SkResourceCache* gResourceCache = nullptr; 547 static void cleanup_gResourceCache() { 548 // We'll clean this up in our own tests, but disable for clients. 549 // Chrome seems to have funky multi-process things going on in unit tests that 550 // makes this unsafe to delete when the main process atexit()s. 551 // SkLazyPtr does the same sort of thing. 552 #if SK_DEVELOPER 553 delete gResourceCache; 554 #endif 555 } 556 557 /** Must hold gMutex when calling. */ 558 static SkResourceCache* get_cache() { 559 // gMutex is always held when this is called, so we don't need to be fancy in here. 560 gMutex.assertHeld(); 561 if (nullptr == gResourceCache) { 562 #ifdef SK_USE_DISCARDABLE_SCALEDIMAGECACHE 563 gResourceCache = new SkResourceCache(SkDiscardableMemory::Create); 564 #else 565 gResourceCache = new SkResourceCache(SK_DEFAULT_IMAGE_CACHE_LIMIT); 566 #endif 567 atexit(cleanup_gResourceCache); 568 } 569 return gResourceCache; 570 } 571 572 size_t SkResourceCache::GetTotalBytesUsed() { 573 SkAutoMutexAcquire am(gMutex); 574 return get_cache()->getTotalBytesUsed(); 575 } 576 577 size_t SkResourceCache::GetTotalByteLimit() { 578 SkAutoMutexAcquire am(gMutex); 579 return get_cache()->getTotalByteLimit(); 580 } 581 582 size_t SkResourceCache::SetTotalByteLimit(size_t newLimit) { 583 SkAutoMutexAcquire am(gMutex); 584 return get_cache()->setTotalByteLimit(newLimit); 585 } 586 587 SkResourceCache::DiscardableFactory SkResourceCache::GetDiscardableFactory() { 588 SkAutoMutexAcquire am(gMutex); 589 return get_cache()->discardableFactory(); 590 } 591 592 SkBitmap::Allocator* SkResourceCache::GetAllocator() { 593 SkAutoMutexAcquire am(gMutex); 594 return get_cache()->allocator(); 595 } 596 597 SkCachedData* SkResourceCache::NewCachedData(size_t bytes) { 598 SkAutoMutexAcquire am(gMutex); 599 return get_cache()->newCachedData(bytes); 600 } 601 602 void SkResourceCache::Dump() { 603 SkAutoMutexAcquire am(gMutex); 604 get_cache()->dump(); 605 } 606 607 size_t SkResourceCache::SetSingleAllocationByteLimit(size_t size) { 608 SkAutoMutexAcquire am(gMutex); 609 return get_cache()->setSingleAllocationByteLimit(size); 610 } 611 612 size_t SkResourceCache::GetSingleAllocationByteLimit() { 613 SkAutoMutexAcquire am(gMutex); 614 return get_cache()->getSingleAllocationByteLimit(); 615 } 616 617 size_t SkResourceCache::GetEffectiveSingleAllocationByteLimit() { 618 SkAutoMutexAcquire am(gMutex); 619 return get_cache()->getEffectiveSingleAllocationByteLimit(); 620 } 621 622 void SkResourceCache::PurgeAll() { 623 SkAutoMutexAcquire am(gMutex); 624 return get_cache()->purgeAll(); 625 } 626 627 bool SkResourceCache::Find(const Key& key, FindVisitor visitor, void* context) { 628 SkAutoMutexAcquire am(gMutex); 629 return get_cache()->find(key, visitor, context); 630 } 631 632 void SkResourceCache::Add(Rec* rec) { 633 SkAutoMutexAcquire am(gMutex); 634 get_cache()->add(rec); 635 } 636 637 void SkResourceCache::VisitAll(Visitor visitor, void* context) { 638 SkAutoMutexAcquire am(gMutex); 639 get_cache()->visitAll(visitor, context); 640 } 641 642 void SkResourceCache::PostPurgeSharedID(uint64_t sharedID) { 643 if (sharedID) { 644 SkMessageBus<PurgeSharedIDMessage>::Post(PurgeSharedIDMessage(sharedID)); 645 } 646 } 647 648 /////////////////////////////////////////////////////////////////////////////// 649 650 #include "SkGraphics.h" 651 #include "SkImageFilter.h" 652 653 size_t SkGraphics::GetResourceCacheTotalBytesUsed() { 654 return SkResourceCache::GetTotalBytesUsed(); 655 } 656 657 size_t SkGraphics::GetResourceCacheTotalByteLimit() { 658 return SkResourceCache::GetTotalByteLimit(); 659 } 660 661 size_t SkGraphics::SetResourceCacheTotalByteLimit(size_t newLimit) { 662 return SkResourceCache::SetTotalByteLimit(newLimit); 663 } 664 665 size_t SkGraphics::GetResourceCacheSingleAllocationByteLimit() { 666 return SkResourceCache::GetSingleAllocationByteLimit(); 667 } 668 669 size_t SkGraphics::SetResourceCacheSingleAllocationByteLimit(size_t newLimit) { 670 return SkResourceCache::SetSingleAllocationByteLimit(newLimit); 671 } 672 673 void SkGraphics::PurgeResourceCache() { 674 SkImageFilter::PurgeCache(); 675 return SkResourceCache::PurgeAll(); 676 } 677 678 ///////////// 679 680 static void dump_visitor(const SkResourceCache::Rec& rec, void*) { 681 SkDebugf("RC: %12s bytes %9lu discardable %p\n", 682 rec.getCategory(), rec.bytesUsed(), rec.diagnostic_only_getDiscardable()); 683 } 684 685 void SkResourceCache::TestDumpMemoryStatistics() { 686 VisitAll(dump_visitor, nullptr); 687 } 688 689 static void sk_trace_dump_visitor(const SkResourceCache::Rec& rec, void* context) { 690 SkTraceMemoryDump* dump = static_cast<SkTraceMemoryDump*>(context); 691 SkString dumpName = SkStringPrintf("skia/sk_resource_cache/%s_%p", rec.getCategory(), &rec); 692 SkDiscardableMemory* discardable = rec.diagnostic_only_getDiscardable(); 693 if (discardable) { 694 dump->setDiscardableMemoryBacking(dumpName.c_str(), *discardable); 695 696 // The discardable memory size will be calculated by dumper, but we also dump what we think 697 // the size of object in memory is irrespective of whether object is live or dead. 698 dump->dumpNumericValue(dumpName.c_str(), "discardable_size", "bytes", rec.bytesUsed()); 699 } else { 700 dump->dumpNumericValue(dumpName.c_str(), "size", "bytes", rec.bytesUsed()); 701 dump->setMemoryBacking(dumpName.c_str(), "malloc", nullptr); 702 } 703 } 704 705 void SkResourceCache::DumpMemoryStatistics(SkTraceMemoryDump* dump) { 706 // Since resource could be backed by malloc or discardable, the cache always dumps detailed 707 // stats to be accurate. 708 VisitAll(sk_trace_dump_visitor, dump); 709 } 710